大师博客网 https://www.sxdeveloper.com/ zh-CN 大师博客网(https://www.sxdeveloper.com)是一个专注于技术交流的平台,对各类编程进行一个更加细致的划分和归类,这个平台可以让更多喜欢技术的人来这个平台进行交流和技术研究,会不定时的更新最新的技术文章和其他科技新闻。 Wed, 10 Apr 2019 10:23:00 +0800 Wed, 10 Apr 2019 10:23:00 +0800 令人困惑的print https://www.sxdeveloper.com/confused-print.html https://www.sxdeveloper.com/confused-print.html Wed, 10 Apr 2019 10:23:00 +0800 明天,你好 这个源自于一个看似很诡异的问题:

if (print("1\n") && print("2\n") && print("3\n") && print("4\n")) {
    ;
}

你期待这段代码输出什么呢?

实际上的输出是:

4
111

很多时候我们会忽略了print是一个语法结构(language constructs), 他并不是一个函数, 参数的list并不要求有括号(即使你写了括号, 括号也会在语法分析阶段被忽略), 他只是一个永远返回结果是1的”表达式(expr)”:

expr :
   T_PRINT expr
 | '(' expr ')'
;

所以其实上面的代码在php看来是:

if (print ("1\n" && print ("2\n" && print ("3\n" && print "4\n")))) {
  ;
}

所以就是, 输出4, 然后输出 "3n" && print的结果1 , 然后输出 "2n" && 1, 最后是 "1n" && 1

而如果想要达到上面代码的本身想要的意图, 我们应该这么写:

if ((print "1\n") && (print "2\n") && (print "3\n") && (print "4\n")) {
    ;
}
]]>
0 https://www.sxdeveloper.com/confused-print.html#comments https://www.sxdeveloper.com/feed/confused-print.html
图解Go select语句原理 https://www.sxdeveloper.com/go-select-princple.html https://www.sxdeveloper.com/go-select-princple.html Wed, 03 Apr 2019 17:27:00 +0800 明天,你好

Go 的select语句是一种仅能用于channl发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中没有case语句的时候,会阻塞当前的groutine。所以,有人也会说select是用来阻塞监听goroutine的。
还有人说:select是Golang在语言层面提供的I/O多路复用的机制,其专门用来检测多个channel是否准备完毕:可读或可写。

以上说法都正确。
I/O多路复用
我们来回顾一下是什么是I/O多路复用。
普通多线程(或进程)I/O

每来一个进程,都会建立连接,然后阻塞,直到接收到数据返回响应。
普通这种方式的缺点其实很明显:系统需要创建和维护额外的线程或进程。因为大多数时候,大部分阻塞的线程或进程是处于等待状态,只有少部分会接收并处理响应,而其余的都在等待。系统为此还需要多做很多额外的线程或者进程的管理工作。

为了解决图中这些多余的线程或者进程,于是有了"I/O多路复用"

I/O多路复用

每个线程或者进程都先到图中”装置“中注册,然后阻塞,然后只有一个线程在”运输“,当注册的线程或者进程准备好数据后,”装置“会根据注册的信息得到相应的数据。从始至终kernel只会使用图中这个黄黄的线程,无需再对额外的线程或者进程进行管理,提升了效率。
select组成结构
select的实现经历了多个版本的修改,当前版本为:1.11
select这个语句底层实现实际上主要由两部分组成:case语句和执行函数。
源码地址为:/go/src/runtime/select.go
每个case语句,单独抽象出以下结构体:

type scase struct {
    c           *hchan         // chan
    elem        unsafe.Pointer // 读或者写的缓冲区地址
    kind        uint16   //case语句的类型,是default、传值写数据(channel <-) 还是  取值读数据(<- channel)
    pc          uintptr // race pc (for race detector / msan)
    releasetime int64
}

复制代码结构体可以用下图表示:

其中比较关键的是:hchan,它是channel的指针。 在一个select中,所有的case语句会构成一个scase结构体的数组。

然后执行select语句实际上就是调用func selectgo(cas0 scase, order0 uint16, ncases int) (int, bool)函数。

func selectgo(cas0 scase, order0 uint16, ncases int) (int, bool)函数参数:

cas0 为上文提到的case语句抽象出的结构体scase数组的第一个元素地址
order0为一个两倍cas0数组长度的buffer,保存scase随机序列pollorder和scase中channel地址序列lockorder。
nncases表示scase数组的长度

selectgo返回所选scase的索引(该索引与其各自的select {recv,send,default}调用的序号位置相匹配)。此外,如果选择的scase是接收操作(recv),则返回是否接收到值。
谁负责调用func selectgo(cas0 scase, order0 uint16, ncases int) (int, bool)函数呢?
在/reflect/value.go中有个func rselect([]runtimeSelect) (chosen int, recvOK bool)函数,此函数的实现在/runtime/select.go文件中的func reflect_rselect(cases []runtimeSelect) (int, bool)函数中:

func reflect_rselect(cases []runtimeSelect) (int, bool) { 
    //如果cases语句为空,则阻塞当前groutine
    if len(cases) == 0 {
        block()
    }
    //实例化case的结构体
    sel := make([]scase, len(cases))
    order := make([]uint16, 2*len(cases))
    for i := range cases {
        rc := &cases[i]
        switch rc.dir {
        case selectDefault:
            sel[i] = scase{kind: caseDefault}
        case selectSend:
            sel[i] = scase{kind: caseSend, c: rc.ch, elem: rc.val}
        case selectRecv:
            sel[i] = scase{kind: caseRecv, c: rc.ch, elem: rc.val}
        }
        if raceenabled || msanenabled {
            selectsetpc(&sel[i])
        }
    }
    return selectgo(&sel[0], &order[0], len(cases))
}

复制代码那谁调用的func rselect([]runtimeSelect) (chosen int, recvOK bool)呢?
在/refect/value.go中,有一个func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)的函数,其调用了rselect函数,并将最终Go中select语句的返回值的返回。
以上这三个函数的调用栈按顺序如下:

func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)
func rselect([]runtimeSelect) (chosen int, recvOK bool)
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)

这仨函数中无论是返回值还是参数都大同小异,可以简单粗暴的认为:函数参数传入的是case语句,返回值返回被选中的case语句。
那谁调用了func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)呢?
可以简单的认为是系统了。
来个简单的图:

前两个函数Select和rselect都是做了简单的初始化参数,调用下一个函数的操作。select真正的核心功能,是在最后一个函数func selectgo(cas0 scase, order0 uint16, ncases int) (int, bool)中实现的。
selectgo函数做了什么
打乱传入的case结构体顺序

锁住其中的所有的channel

遍历所有的channel,查看其是否可读或者可写

如果其中的channel可读或者可写,则解锁所有channel,并返回对应的channel数据


假如没有channel可读或者可写,但是有default语句,则同上:返回default语句对应的scase并解锁所有的channel。

假如既没有channel可读或者可写,也没有default语句,则将当前运行的groutine阻塞,并加入到当前所有channel的等待队列中去。

然后解锁所有channel,等待被唤醒。

此时如果有个channel可读或者可写ready了,则唤醒,并再次加锁所有channel,

遍历所有channel找到那个对应的channel和G,唤醒G,并将没有成功的G从所有channel的等待队列中移除。

如果对应的scase值不为空,则返回需要的值,并解锁所有channel

如果对应的scase为空,则循环此过程。

select和channel之间的关系
在想想select和channel做了什么事儿,我觉得和多路复用是一回事儿

]]>
0 https://www.sxdeveloper.com/go-select-princple.html#comments https://www.sxdeveloper.com/feed/go-select-princple.html
令人困惑的strtotime https://www.sxdeveloper.com/confused-strtotime-function.html https://www.sxdeveloper.com/confused-strtotime-function.html Mon, 26 Nov 2018 14:33:00 +0800 明天,你好

经常会有人被strtotime结合-1 month+1 monthnext month的时候搞得很困惑,然后就会觉得这个函数有点不那么靠谱,动不动就出问题,用的时候就会很慌。

<?php
//假设当前时间为2018-07-31
echo date("Y-m-d",strtotime("-1 month"));

怎么输出是2018-07-01?虽然这个问题看起来很迷惑,但从内部逻辑上来说呢,其实是”对”的。
可以来模拟下date内部对于这种事情的处理逻辑:

  1. 先做-1 month,那么当前是07-31,减去一以后就是06-31
  2. 再做日期规范化,因为6月没有31号,所以就好像2点60等于3点一样,6月31就等于了7月1

是不是逻辑很”清晰”呢? 也可以手动验证第二个步骤,比如:

<?php

echo date("Y-m-d", strtotime("2017-06-31"));
//输出2017-07-01

也就是说,只要涉及到大小月的最后一天,都可能会有这个迷惑,也可以很轻松的验证类似的其他月份,印证这个结论:

<?php

echo date("Y-m-d", strtotime("-1 month", strtotime("2017-03-31")));
//输出2017-03-03
echo date("Y-m-d", strtotime("+1 month", strtotime("2017-08-31")));
//输出2017-10-01
echo date("Y-m-d", strtotime("next month", strtotime("2017-01-31")));
//输出2017-03-03
echo date("Y-m-d", strtotime("last month", strtotime("2017-03-31")));
//输出2017-03-03

那怎么办呢?
从PHP5.3开始呢,date新增了一系列修正短语,来明确这个问题, 那就是first day oflast day of,也就是可以限定好不要让date自动规范化:

<?php

echo date("Y-m-d", strtotime("last day of -1 month", strtotime("2017-03-31")));
//输出2017-02-28
echo date("Y-m-d", strtotime("first day of +1 month", strtotime("2017-08-31")));
//输出2017-09-01
echo date("Y-m-d", strtotime("first day of next month", strtotime("2017-01-31")));
//输出2017-02-01
echo date("Y-m-d", strtotime("last day of last month", strtotime("2017-03-31")));
//输出2017-02-28

那如果是5.3之前的版本(还有人用么?),可以使用mktime之类的,把所有的日子忽略掉,比如都限定为每月1号就可以了,只不过就不如直接用first day of 来的更加优雅。
现在,搞清楚了内部原理,是不是就不慌了?

]]>
0 https://www.sxdeveloper.com/confused-strtotime-function.html#comments https://www.sxdeveloper.com/feed/confused-strtotime-function.html
PHP设计原则之依赖倒置原则 https://www.sxdeveloper.com/php-design-patterns-dip.html https://www.sxdeveloper.com/php-design-patterns-dip.html Thu, 15 Nov 2018 16:53:00 +0800 明天,你好 需要解决的问题

类A直接依赖于类B,假如要将类A修改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑。类B和类C是底层模块,负责基本的原子操作。假如修改类A,将会给程序带来不必要的风险。

什么是依赖倒置原则

英文缩写DIP(Dependence Inversion Principle)
主要有三层含义:

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
  2. 抽象不应该依赖细节;
  3. 细节应该依赖抽象;

关键名词解释:

  • 抽象:即抽象类或接口,两者是不能够实例化的。
  • 细节:即具体的实现类,实现接口或者继承抽象类所产生的类,两者可以通过关键字new直接被实例化。

实例

现在通过实例还原开篇问题的场景,以便更好的来理解。下面代码描述了一个简单的场景,Jim作为人,有吃的方法,苹果有取得自己名字的方法,然后实现Jim去吃苹果。
代码如下:

<?php

//具体Jim人类
class Jim
{
    public function eat(Apple $apple)
    {
        echo 'Jim eat ' . $apple->getName() . PHP_EOL;
    }
}

//具体苹果类
class Apple
{
    public function getName()
    {
        return 'apple';
    }
}

$jim = new Jim();
$apple = new Apple();
$jim->eat($apple);

运行结果:

Jim eat apple

上面代码看起来比较简单,但其实是一个非常脆弱的设计。现在Jim可以吃苹果了,但是不能只吃苹果而不吃别的水果啊,这样下去肯定会造成营养失衡。现在想让Jim吃香蕉了(好像香蕉里含钾元素比较多,吃点比较有益),突然发现Jim是吃不了香蕉的,那怎么办呢?看来只有修改代码了啊,由于上面代码中Jim类依赖于Apple类,所以导致不得不去改动Jim类里面的代码。那如果下次Jim又要吃别的水果了呢?继续修改代码?这种处理方式显然是不可取的,频繁修改会带来很大的系统风险,改着改着可能就发现Jim不会吃水果了。

上面的代码之所以会出现上述难堪的问题,就是因为Jim类依赖于Apple类,两者是紧耦合的关系,其导致的结果就是系统的可维护性大大降低。要增加香蕉类却要去修改Jim类代码,这是不可忍受的,你改你的代码为什么要动我的啊,显然Jim不乐意了。我们常说要设计一个健壮稳定的系统,而这里只是增加了一个香蕉类,就要去修改Jim类,健壮和稳定还从何谈起。

而根据依赖倒置原则,我们可以对上述代码做些修改,提取抽象的部分。首先我们提取出两个接口:People和Fruit,都提供各自必需的抽象方法,这样以后无论是增加Jim人类,还是增加Apple、Banana等各种水果,都只需要增加自己的实现类就可以了。由于遵循依赖倒置原则,只依赖于抽象,而不依赖于细节,所以增加类无需修改其他类。

代码如下:

<?php

//人接口
interface People
{

    public function eat(Fruit $fruit);//人都有吃的方法,不然都饿死了
}

//水果接口
interface Fruit
{
    public function getName();//水果都是有名字的
}

//具体Jim人类
class Jim implements People
{
    public function eat(Fruit $fruit)
    {
        echo 'Jim eat ' . $fruit->getName() . PHP_EOL;
    }
}

//具体苹果类
class Apple implements Fruit
{
    public function getName()
    {
        return "apple";
    }
}

//具体香蕉类
class Banana implements Fruit
{
    public function getName()
    {
        return "banana";
    }
}

$jim = new Jim();
$apple = new Apple();
$banana = new Banana();
$jim->eat($apple);
$jim->eat($banana);

运行结果:

Jim eat apple
Jim eat banana

People类是复杂的业务逻辑,属于高层模块,而Fruit是原子模块,属于低层模块。People依赖于抽象的Fruit接口,这就做到了:高层模块不应该依赖低层模块,两者都应该依赖于抽象(抽象类或接口)。

People和Fruit接口与各自的实现类没有关系,增加实现类不会影响接口,这就做到了:抽象(抽象类或接口)不应该依赖于细节(具体实现类)。

Jim、Apple、Banana实现类都要去实现各自的接口所定义的抽象方法,所以是依赖于接口的。这就做到了:细节(具体实现类)应该依赖抽象。

什么是倒置

到了这里,我们对依赖倒置原则的“依赖”就很好理解了,但是什么是“倒置”呢。是这样子的,刚开始按照正常人的一般思维方式,我想吃香蕉就是吃香蕉,想吃苹果就吃苹果,编程也是这样,都是按照面向实现的思维方式来设计。而现在要倒置思维,提取公共的抽象,面向接口(抽象类)编程。不再依赖于具体实现了,而是依赖于接口或抽象类,这就是依赖的思维方式“倒置”了。

依赖的三种实现方式

对象的依赖关系有三种方式来传递:

  1. 接口方法中声明依赖对象,就是上面代码所展示的那样。
  2. 构造方法传递依赖对象,在构造函数中的需要传递的参数是抽象类或接口的方式实现,代码如下:
<?php

//人接口
interface People
{
    public function eat();//人都有吃的方法,不然都饿死了
}

//水果接口
interface Fruit
{
    public function getName();//水果都是有名字的
}

//具体Jim人类
class Jim implements People
{

    private $fruit;

    //构造方法传递依赖对象
    public function __construct(Fruit $fruit)
    {
        $this->fruit = $fruit;
    }

    public function eat()
    {
        echo 'Jim eat ' . $this->fruit->getName() . PHP_EOL;
    }
}

//具体苹果类
class Apple implements Fruit
{
    public function getName()
    {
        return "apple";
    }
}

//具体香蕉类
class Banana implements Fruit
{
    public function getName()
    {
        return "banana";
    }
}

$apple = new Apple();
$jim = new Jim($apple);
$jim->eat();

$banana = new Banana();
$jim = new Jim($banana);
$jim->eat();
  1. Setter方法传递依赖对象,在我们设置的setXXX方法中的参数为抽象类或接口,来实现传递依赖对象。代码如下:
<?php

//人接口
interface People
{
    public function eat();//人都有吃的方法,不然都饿死了
}

//水果接口
interface Fruit
{
    public function getName();//水果都是有名字的
}

//具体Jim人类
class Jim implements People
{

    /**
     * @var  Fruit
     */
    private $fruit;


    public function eat()
    {
        echo 'Jim eat ' . $this->fruit->getName() . PHP_EOL;
    }


    public function setFruit(Fruit $fruit)
    {
        //setter方式传递依赖对象
        $this->fruit = $fruit;
    }
}

//具体苹果类
class Apple implements Fruit
{
    public function getName()
    {
        return "apple";
    }
}

//具体香蕉类
class Banana implements Fruit
{
    public function getName()
    {
        return "banana";
    }
}

$apple = new Apple();
$jim = new Jim();
$jim->setFruit($apple);
$jim->eat();

$banana = new Banana();
$jim = new Jim();
$jim->setFruit($banana);
$jim->eat();

总结

从上面的代码修改过程中,可以看到由于类之间松耦合的设计,面向接口编程依赖抽象而不依赖细节,所以在修改某个类的代码时,不会牵涉到其他类的修改,显著降低系统风险,提高系统健壮性。
说到底,依赖倒置原则的核心就是面向接口编程的思想,尽量对每个实现类都提取抽象和公共接口形成接口或抽象类,依赖于抽象而不要依赖于具体实现。依赖倒置原则的本质其实就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。但是这个原则也是设计原则中最难以实现的了,如果没有实现这个原则,那么也就意味着开闭原则(对扩展开放,对修改关闭)也无法实现。

]]>
0 https://www.sxdeveloper.com/php-design-patterns-dip.html#comments https://www.sxdeveloper.com/feed/php-design-patterns-dip.html
PHP中魔术方法__set和__get的使用 https://www.sxdeveloper.com/php-magic-method-get-set.html https://www.sxdeveloper.com/php-magic-method-get-set.html Wed, 14 Nov 2018 15:07:00 +0800 明天,你好 简介

在php中,有很多魔术方法,魔术方法都是以两个下划线 __ 开头的保留方法,这些魔术方法都是针对类而存在的。

  • __set: 给类不存在或者不可访问的属性赋值时会被自动调用
  • __get: 读取类不存在或者不可访问的属性时会被自动调用

二者的原型如下

public mixed __get ( string $name )
public void __set ( string $name , mixed $value )

例子

class A
{
    public $name;
}

$a = new A;
$a->name = 'zhangsan';
echo $a->name;

正常情况下,上面的代码会正确的输出zhangsan的结果,没有问题。
此时,假如输出对象a不存在的age属性,页面上会提示A::$age未定义。

echo $a->age;

Notice: Undefined property: A::$age

当然,有人可能会说,我声明一个public $a; 不就行了吗?可以呀,不过像你这样砸场子的话,我就没有继续说下去的必要了。

对A类增加一个 __get 方法,看看这个会被自动调用的方法是啥效果

class A
{
    public $name;

    public function __get($name)
    {
        var_dump($name);
    }
}

依然访问 $a->age 属性

echo $a->age;

其结果瞬间由刚才的Notice提醒变成了

string(3) "age"

你可以再写一个protectedprivate类型的属性,其结果跟不存在的age属性一样,会自动调用__get方法。但是 public类型的name属性不会调用该方法。

现在,再为A类增加一个__set方法

class A
{
    public $name;

    public function __get($name)
    {
        // var_dump($name);
    }

    public function __set($name, $value)
    {
        var_dump($name, $value);
    }
}

为不存在的属性age赋值,看看效果

$a = new A;
$a->age = 20;

结果如下

string(3) "age" int(20)

当然,protectedprivate类型的属性被赋值时也可以得到相同的结果,可以自己尝试下。
so easy,那么,我讲这些的意义是什么?
别急别急,鉴于以上,再看一个问题:
假如有一个B类以及一个配置数组如下

class B
{
    public $name;
}

$config = [
    'class' => 'B',
    'name' => 'zhangsan',
    'age' => 20,
];

如何根据$config的配置,来创建一个B对象并为B的对象属性赋值?
先思考一下再继续阅读哦。

B类的实现

class B
{
    public $name;

    private $_definitions = [];

    public function __get($name)
    {
        return isset($this->_definitions[$name]) ? $this->_definitions[$name] : null;
    }

    public function __set($name, $value)
    {
        $this->_definitions[$name] = $value;
    }
}

实例化上述类并通过$config为对象b赋值

$config = [
    'class' => 'B',
    'name' => 'zhangsan',
    'age' => 20,
];

$class = $config['class'];
unset($config['class']);

$object = new $class;

foreach ($config as $k => $v) {
    $object->$k = $v;
}

var_dump($object->name);
var_dump($object->age);

结果如下

string(8) "zhangsan" int(20)

这有点像什么?这是不是有点像yii2中的配置项?

]]>
0 https://www.sxdeveloper.com/php-magic-method-get-set.html#comments https://www.sxdeveloper.com/feed/php-magic-method-get-set.html
深入理解OAuth 2.0 https://www.sxdeveloper.com/deeply-think-oauth2.html https://www.sxdeveloper.com/deeply-think-oauth2.html Fri, 14 Sep 2018 18:38:00 +0800 明天,你好

OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版

应用场景

为了理解OAuth的适用场合,让我举一个假设的例子。
有一个"云冲印"的网站,可以将用户储存在Google的照片,冲印出来。用户为了使用该服务,必须让"云冲印"读取自己储存在Google上的照片。
问题是只有得到用户的授权,Google才会同意"云冲印"读取这些照片。那么,"云冲印"怎样获得用户的授权呢?
传统方法是,用户将自己的Google用户名和密码,告诉"云冲印",后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点。

(1)"云冲印"为了后续的服务,会保存用户的密码,这样很不安全。
(2)Google不得不部署密码登录,而我们知道,单纯的密码登录并不安全。
(3)"云冲印"拥有了获取用户储存在Google所有资料的权力,用户没法限制"云冲印"获得授权的范围和有效期。
(4)用户只有修改密码,才能收回赋予"云冲印"的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。
(5)只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。
OAuth就是为了解决上面这些问题而诞生的。

名词定义

在详细讲解OAuth 2.0之前,需要了解几个专用名词。它们对读懂后面的讲解,尤其是几张图,至关重要。

(1) Third-party application:第三方应用程序,本文中又称"客户端"(client),即上一节例子中的"云冲印"。
(2)HTTP service:HTTP服务提供商,本文中简称"服务提供商",即上一节例子中的Google。
(3)Resource Owner:资源所有者,本文中又称"用户"(user)。
(4)User Agent:用户代理,本文中就是指浏览器。
(5)Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
知道了上面这些名词,就不难理解,OAuth的作用就是让"客户端"安全可控地获取"用户"的授权,与"服务商提供商"进行互动。

OAuth的思路

OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。

运行流程

oauth.png
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
不难看出来,上面六个步骤之中,B是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源。

下面一一讲解客户端获取授权的四种模式。

客户端的授权模式

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。

authorization-code.png

它的步骤如下:
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

下面是上面这些步骤所需要的参数。
A步骤中,客户端申请认证的URI,包含以下参数:
response_type:表示授权类型,必选项,此处的值固定为"code"
client_id:表示客户端的ID,必选项
redirect_uri:表示重定向URI,可选项
scope:表示申请的权限范围,可选项
state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
下面是一个例子。

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
        &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

C步骤中,服务器回应客户端的URI,包含以下参数:
code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
下面是一个例子。

HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
          &state=xyz

D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
code:表示上一步获得的授权码,必选项。
redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
client_id:表示客户端ID,必选项。
下面是一个例子。

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

E步骤中,认证服务器发送的HTTP回复,包含以下参数:
access_token:表示访问令牌,必选项。
token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
下面是一个例子。


     HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache

     {
       "access_token":"2YotnFZFEjr1zCsicMWpAA",
       "token_type":"example",
       "expires_in":3600,
       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
       "example_parameter":"example_value"
     }

从上面代码可以看到,相关参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存。

简化模式

简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

implicit-grant-type.png

它的步骤如下:
(A)客户端将用户导向认证服务器。
(B)用户决定是否给于客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端。

下面是上面这些步骤所需要的参数。
A步骤中,客户端发出的HTTP请求,包含以下参数:
response_type:表示授权类型,此处的值固定为"token",必选项。
client_id:表示客户端的ID,必选项。
redirect_uri:表示重定向的URI,可选项。
scope:表示权限范围,可选项。
state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
下面是一个例子。


    GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz
        &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
    Host: server.example.com

C步骤中,认证服务器回应客户端的URI,包含以下参数:
access_token:表示访问令牌,必选项。
token_type:表示令牌类型,该值大小写不敏感,必选项。
expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
下面是一个例子。

     HTTP/1.1 302 Found
     Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
               &state=xyz&token_type=example&expires_in=3600

在上面的例子中,认证服务器用HTTP头信息的Location栏,指定浏览器重定向的网址。注意,在这个网址的Hash部分包含了令牌。
根据上面的D步骤,下一步浏览器会访问Location指定的网址,但是Hash部分不会发送。接下来的E步骤,服务提供商的资源服务器发送过来的代码,会提取出Hash中的令牌。

密码模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

Password-Credentials-Grant.png

它的步骤如下:
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
B步骤中,客户端发出的HTTP请求,包含以下参数:
grant_type:表示授权类型,此处的值固定为"password",必选项。
username:表示用户名,必选项。
password:表示用户的密码,必选项。
scope:表示权限范围,可选项。
下面是一个例子。

     POST /token HTTP/1.1
     Host: server.example.com
     Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
     Content-Type: application/x-www-form-urlencoded

     grant_type=password&username=johndoe&password=A3ddj3w

C步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。

     HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache

     {
       "access_token":"2YotnFZFEjr1zCsicMWpAA",
       "token_type":"example",
       "expires_in":3600,
       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
       "example_parameter":"example_value"
     }

整个过程中,客户端不得保存用户的密码。

客户端模式

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

Client-Credentials-Grant.png

它的步骤如下:

(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。

A步骤中,客户端发出的HTTP请求,包含以下参数:

granttype:表示授权类型,此处的值固定为"clientcredentials",必选项。
scope:表示权限范围,可选项。

     POST /token HTTP/1.1
     Host: server.example.com
     Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
     Content-Type: application/x-www-form-urlencoded

     grant_type=client_credentials

认证服务器必须以某种方式,验证客户端身份。

B步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。

     HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache

     {
       "access_token":"2YotnFZFEjr1zCsicMWpAA",
       "token_type":"example",
       "expires_in":3600,
       "example_parameter":"example_value"
     }

上面代码中,各个参数的含义参见《授权码模式》一节。

更新令牌

如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用"更新令牌"申请一个新的访问令牌。
客户端发出更新令牌的HTTP请求,包含以下参数:

granttype:表示使用的授权模式,此处的值固定为"refreshtoken",必选项。
refresh_token:表示早前收到的更新令牌,必选项。
scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。
下面是一个例子。

     POST /token HTTP/1.1
     Host: server.example.com
     Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
     Content-Type: application/x-www-form-urlencoded

     grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
]]>
0 https://www.sxdeveloper.com/deeply-think-oauth2.html#comments https://www.sxdeveloper.com/feed/deeply-think-oauth2.html
YAML语言教程 https://www.sxdeveloper.com/yaml-intro.html https://www.sxdeveloper.com/yaml-intro.html Fri, 24 Aug 2018 14:11:24 +0800 明天,你好 编程免不了要写配置文件,怎么写配置也是一门学问

YAML 是专门用来写配置文件的语言,非常简洁和强大,远比 JSON 格式方便

bg2016070403.png

一、简介

YAML 语言(发音 /ˈjæməl/ )的设计目标,就是方便人类读写。它实质上是一种通用的数据串行化格式

它的基本语法规则如下。

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进时不允许使用Tab键,只允许使用空格
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • #表示注释,从这个字符一直到行尾,都会被解析器忽略

YAML 支持的数据结构有三种。

  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
  • 纯量(scalars):单个的、不可再分的值

以下分别介绍这三种数据结构

二、对象

对象的一组键值对,使用冒号结构表示

animal: pets

转为 JavaScript 如下。

{ animal: 'pets' }

Yaml 也允许另一种写法,将所有键值对写成一个行内对象

hash: { name: Steve, foo: bar } 

转为 JavaScript 如下。

{ hash: { name: 'Steve', foo: 'bar' } }

三、数组

一组连词线开头的行,构成一个数组

- Cat
- Dog
- Goldfish

转为 JavaScript 如下

[ 'Cat', 'Dog', 'Goldfish' ]

数据结构的子成员是一个数组,则可以在该项下面缩进一个空格

-
 - Cat
 - Dog
 - Goldfish

转为 JavaScript 如下

[ [ 'Cat', 'Dog', 'Goldfish' ] ]

数组也可以采用行内表示法。

animal: [Cat, Dog]

转为 JavaScript 如下

{ animal: [ 'Cat', 'Dog' ] }

四、复合结构

对象和数组可以结合使用,形成复合结构

languages:
 - Ruby
 - Perl
 - Python 
websites:
 YAML: yaml.org 
 Ruby: ruby-lang.org 
 Python: python.org 
 Perl: use.perl.org 

转为 JavaScript 如下

{ languages: [ 'Ruby', 'Perl', 'Python' ],
  websites: 
   { YAML: 'yaml.org',
     Ruby: 'ruby-lang.org',
     Python: 'python.org',
     Perl: 'use.perl.org' } }

五、纯量

纯量是最基本的、不可再分的值。以下数据类型都属于 JavaScript 的纯量。

  • 字符串
  • 布尔值
  • 整数
  • 浮点数
  • Null
  • 时间
  • 日期

数值直接以字面量的形式表示

number: 12.30

转为 JavaScript 如下

{ number: 12.30 }

布尔值用true和false表示

isSet: true

转为 JavaScript 如下

{ isSet: true }

null用~表示。

parent: ~ 

转为 JavaScript 如下

{ parent: null }

时间采用 ISO8601 格式

iso8601: 2001-12-14t21:59:43.10-05:00 

转为 JavaScript 如下

{ iso8601: new Date('2001-12-14t21:59:43.10-05:00') }

日期采用复合 iso8601 格式的年、月、日表示

date: 1976-07-31

转为 JavaScript 如下

{ date: new Date('1976-07-31') }

YAML 允许使用两个感叹号,强制转换数据类型

e: !!str 123
f: !!str true

转为 JavaScript 如下

{ e: '123', f: 'true' }

六、字符串

字符串是最常见,也是最复杂的一种数据类型

字符串默认不使用引号表示。

str: 这是一行字符串

转为 JavaScript 如下

{ str: '这是一行字符串' }

如果字符串之中包含空格或特殊字符,需要放在引号之中

str: '内容: 字符串'

转为 JavaScript 如下

{ str: '内容: 字符串' }

单引号和双引号都可以使用,双引号不会对特殊字符转义

s1: '内容\n字符串'
s2: "内容\n字符串"

转为 JavaScript 如下

{ s1: '内容\\n字符串', s2: '内容\n字符串' }

单引号之中如果还有单引号,必须连续使用两个单引号转义

str: 'labor''s day' 

转为 JavaScript 如下

{ str: 'labor\'s day' }

字符串可以写成多行,从第二行开始,必须有一个单空格缩进,换行符会被转为空格

str: 这是一段
  多行
  字符串

转为 JavaScript 如下

{ str: '这是一段 多行 字符串' }

多行字符串可以使用|保留换行符,也可以使用>折叠换行

this: |
  Foo
  Bar
that: >
  Foo
  Bar

转为 JavaScript 代码如下

{ this: 'Foo\nBar\n', that: 'Foo Bar\n' }

+表示保留文字块末尾的换行,-表示删除字符串末尾的换行

s1: |
  Foo

s2: |+
  Foo


s3: |-
  Foo

转为 JavaScript 代码如下

{ s1: 'Foo\n', s2: 'Foo\n\n\n', s3: 'Foo' }

字符串之中可以插入 HTML 标记

message: |

  <p style="color: red">
    段落
  </p>

转为 JavaScript 如下

{ message: '\n<p style="color: red">\n  段落\n</p>\n' }
]]>
0 https://www.sxdeveloper.com/yaml-intro.html#comments https://www.sxdeveloper.com/feed/yaml-intro.html
初识Docker https://www.sxdeveloper.com/first-meet-docker.html https://www.sxdeveloper.com/first-meet-docker.html Mon, 04 Jun 2018 18:08:10 +0800 明天,你好 2013年发布至今, Docker 一直广受瞩目,被认为可能会改变软件行业。

但是,许多人并不清楚 Docker 到底是什么,要解决什么问题,好处又在哪里?本文就来详细解释,帮助大家理解它,还带有简单易懂的实例,教你如何将它用于日常开发。

bg2018020901.png

环境配置的难题

软件开发最大的麻烦事之一,就是环境配置。用户计算机的环境都不相同,你怎么知道自家的软件,能在那些机器跑起来?

用户必须保证两件事:操作系统的设置,各种库和组件的安装。只有它们都正确,软件才能运行。举例来说,安装一个 Python 应用,计算机必须有 Python 引擎,还必须有各种依赖,可能还要配置环境变量。

如果某些老旧的模块与当前环境不兼容,那就麻烦了。开发者常常会说:"它在我的机器可以跑了"(It works on my machine),言下之意就是,其他机器很可能跑不了。

环境配置如此麻烦,换一台机器,就要重来一次,旷日费时。很多人想到,能不能从根本上解决问题,软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。

虚拟机

虚拟机(virtual machine)就是带环境安装的一种解决方案。它可以在一种操作系统里面运行另一种操作系统,比如在 Windows 系统里面运行 Linux 系统。应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其他部分毫无影响。

虽然用户可以通过虚拟机还原软件的原始环境。但是,这个方案有几个缺点。

(1)资源占用多

虚拟机会独占一部分内存和硬盘空间。它运行的时候,其他程序就不能使用这些资源了。哪怕虚拟机里面的应用程序,真正使用的内存只有 1MB,虚拟机依然需要几百 MB 的内存才能运行。

(2)冗余步骤多

虚拟机是完整的操作系统,一些系统级别的操作步骤,往往无法跳过,比如用户登录。

(3)启动慢

启动操作系统需要多久,启动虚拟机就需要多久。可能要等几分钟,应用程序才能真正运行。

Linux 容器

由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。

Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。

由于容器是进程级别的,相比虚拟机有很多优势。

(1)启动快

容器里面的应用,直接就是底层系统的一个进程,而不是虚拟机内部的进程。所以,启动容器相当于启动本机的一个进程,而不是启动一个操作系统,速度就快很多。

(2)资源占用少

容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。另外,多个容器可以共享资源,虚拟机都是独享资源。

(3)体积小

容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。

总之,容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多。

Docker 是什么

Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

Docker 的用途

Docker 的主要用途,目前有三大类。

(1)提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。

(2)提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。

(3)组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。

Docker安装

参照官方文档,此处不作具体安装说明。

image 文件

Docker 把应用程序及其依赖,打包在 image 文件里面。只有通过这个文件,才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image。

  • 列出本机的所有 image 文件。
$ docker image ls
  • 删除 image 文件
$ docker image rm [imageName]

image 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。

为了方便共享,image 文件制作完成后,可以上传到网上的仓库。Docker 的官方仓库 Docker Hub 是最重要、最常用的 image 仓库。此外,出售自己制作的 image 文件也是可以的。

实例

下面,我们通过最简单的 image 文件"hello world",感受一下 Docker。

需要说明的是,国内连接 Docker 的官方仓库很慢,还会断线,需要将默认仓库改成国内的镜像网站,具体的修改方法在下一篇文章的第一节。有需要的朋友,可以先看一下。

首先,运行下面的命令,将 image 文件从仓库抓取到本地。

$ docker image pull library/hello-world

上面代码中,docker image pull是抓取 image 文件的命令。library/hello-world是 image 文件在仓库里面的位置,其中library是 image 文件所在的组,hello-world是 image 文件的名字。

由于 Docker 官方提供的 image 文件,都放在library组里面,所以它的是默认组,可以省略。因此,上面的命令可以写成下面这样。

$ docker image pull hello-world

抓取成功以后,就可以在本机看到这个 image 文件了。

$ docker image ls

现在,运行这个 image 文件。

$ docker container run hello-world

docker container run命令会从 image 文件,生成一个正在运行的容器实例。

注意,docker container run命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。因此,前面的docker image pull命令并不是必需的步骤。

如果运行成功,你会在屏幕上读到下面的输出。

$ docker container run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.
... ...

输出这段提示以后,hello world就会停止运行,容器自动终止。

有些容器不会自动终止,因为提供的是服务。比如,安装运行 Ubuntu 的 image,就可以在命令行体验 Ubuntu 系统。

$ docker container run -it ubuntu bash

对于那些不会自动终止的容器,必须使用docker container kill 命令手动终止。

$ docker container kill [containID]

容器文件

image 文件生成的容器实例,本身也是一个文件,称为容器文件。也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。而且关闭容器并不会删除容器文件,只是容器停止运行而已。

  • 列出本机正在运行的容器
$ docker container ls
  • 列出本机所有容器,包括终止运行的容器
$ docker container ls --all

上面命令的输出结果之中,包括容器的 ID。很多地方都需要提供这个 ID,比如上一节终止容器运行的docker container kill命令。

终止运行的容器文件,依然会占据硬盘空间,可以使用docker container rm命令删除。

$ docker container rm [containerID]

运行上面的命令之后,再使用docker container ls --all命令,就会发现被删除的容器文件已经消失了。

Dockerfile 文件

学会使用 image 文件以后,接下来的问题就是,如何可以生成 image 文件?如果你要推广自己的软件,势必要自己制作 image 文件。

这就需要用到 Dockerfile 文件。它是一个文本文件,用来配置 image。Docker 根据 该文件生成二进制的 image 文件。

下面通过一个实例,演示如何编写 Dockerfile 文件。

制作自己的 Docker 容器

下面我以 koa-demos 项目为例,介绍怎么写 Dockerfile 文件,实现让用户在 Docker 容器里面运行 Koa 框架。

作为准备工作,请先下载源码。

$ git clone https://github.com/ruanyf/koa-demos.git
$ cd koa-demos

编写 Dockerfile 文件

首先,在项目的根目录下,新建一个文本文件.dockerignore,写入下面的内容。

.git
node_modules
npm-debug.log

上面代码表示,这三个路径要排除,不要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。

然后,在项目的根目录下,新建一个文本文件 Dockerfile,写入下面的内容:

FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000

上面代码一共五行,含义如下。

FROM node:8.4:该 image 文件继承官方的 node image,冒号表示标签,这里标签是8.4,即8.4版本的 node。
COPY . /app:将当前目录下的所有文件(除了.dockerignore排除的路径),都拷贝进入 image 文件的/app目录。
WORKDIR /app:指定接下来的工作路径为/app。
RUN npm install:在/app目录下,运行npm install命令安装依赖。注意,安装后所有的依赖,都将打包进入 image 文件。
EXPOSE 3000:将容器 3000 端口暴露出来, 允许外部连接这个端口。
10.2 创建 image 文件
有了 Dockerfile 文件以后,就可以使用docker image build命令创建 image 文件了。

$ docker image build -t koa-demo .

其他方式

$ docker image build -t koa-demo:0.0.1 .

上面代码中,-t参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是latest。最后的那个点表示 Dockerfile 文件所在的路径,上例是当前路径,所以是一个点。

如果运行成功,就可以看到新生成的 image 文件koa-demo了。

$ docker image ls

生成容器

$ docker container run -p 8000:3000 -it koa-demo /bin/bash

上面命令的各个参数含义如下:

-p参数:容器的 3000 端口映射到本机的 8000 端口。
-it参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
koa-demo:0.0.1:image 文件的名字(如果有标签,还需要提供标签,默认是 latest 标签)。
/bin/bash:容器启动以后,内部第一个执行的命令。这里是启动 Bash,保证用户可以使用 Shell。
如果一切正常,运行上面的命令以后,就会返回一个命令行提示符。

root@66d80f4aaf1e:/app#
这表示你已经在容器里面了,返回的提示符就是容器内部的 Shell 提示符。执行下面的命令。

root@66d80f4aaf1e:/app# node demos/01.js
这时,Koa 框架已经运行起来了。打开本机的浏览器,访问 http://127.0.0.1:8000,网页显示"Not Found",这是因为这个 demo 没有写路由。

这个例子中,Node 进程运行在 Docker 容器的虚拟环境里面,进程接触到的文件系统和网络接口都是虚拟的,与本机的文件系统和网络接口是隔离的,因此需要定义容器与物理机的端口映射(map)。

现在,在容器的命令行,按下 Ctrl + c 停止 Node 进程,然后按下 Ctrl + d (或者输入 exit)退出容器。此外,也可以用docker container kill终止容器运行。

在本机的另一个终端窗口,查出容器的 ID

$ docker container ls

停止指定的容器运行

$ docker container kill [containerID]
容器停止运行之后,并不会消失,用下面的命令删除容器文件。

查出容器的 ID

$ docker container ls --all

删除指定的容器文件

$ docker container rm [containerID]
也可以使用docker container run命令的--rm参数,在容器终止运行后自动删除容器文件。

$ docker container run --rm -p 8000:3000 -it koa-demo /bin/bash
10.4 CMD 命令
上一节的例子里面,容器启动以后,需要手动输入命令node demos/01.js。我们可以把这个命令写在 Dockerfile 里面,这样容器启动以后,这个命令就已经执行了,不用再手动输入了。

FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000
CMD node demos/01.js

上面的 Dockerfile 里面,多了最后一行CMD node demos/01.js,它表示容器启动后自动执行node demos/01.js。

你可能会问,RUN命令与CMD命令的区别在哪里?简单说,RUN命令在 image 文件的构建阶段执行,执行结果都会打包进入 image 文件;CMD命令则是在容器启动后执行。另外,一个 Dockerfile 可以包含多个RUN命令,但是只能有一个CMD命令。

注意,指定了CMD命令以后,docker container run命令就不能附加命令了(比如前面的/bin/bash),否则它会覆盖CMD命令。现在,启动容器可以使用下面的命令。

$ docker container run --rm -p 8000:3000 -it koa-demo:0.0.1
10.5 发布 image 文件
容器运行成功后,就确认了 image 文件的有效性。这时,我们就可以考虑把 image 文件分享到网上,让其他人使用。

首先,去 hub.docker.com 或 cloud.docker.com 注册一个账户。然后,用下面的命令登录。

$ docker login
接着,为本地的 image 标注用户名和版本。

$ docker image tag [imageName] [username]/[repository]:

实例

$ docker image tag koa-demos:0.0.1 ruanyf/koa-demos:0.0.1
也可以不标注用户名,重新构建一下 image 文件。

$ docker image build -t [username]/[repository]: .
最后,发布 image 文件。

$ docker image push [username]/[repository]:
发布成功以后,登录 hub.docker.com,就可以看到已经发布的 image 文件。

十一、其他有用的命令
docker 的主要用法就是上面这些,此外还有几个命令,也非常有用。

(1)docker container start

前面的docker container run命令是新建容器,每运行一次,就会新建一个容器。同样的命令运行两次,就会生成两个一模一样的容器文件。如果希望重复使用容器,就要使用docker container start命令,它用来启动已经生成、已经停止运行的容器文件。

$ docker container start [containerID]
(2)docker container stop

前面的docker container kill命令终止容器运行,相当于向容器里面的主进程发出 SIGKILL 信号。而docker container stop命令也是用来终止容器运行,相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号。

$ bash container stop [containerID]
这两个信号的差别是,应用程序收到 SIGTERM 信号以后,可以自行进行收尾清理工作,但也可以不理会这个信号。如果收到 SIGKILL 信号,就会强行立即终止,那些正在进行中的操作会全部丢失。

(3)docker container logs

docker container logs命令用来查看 docker 容器的输出,即容器里面 Shell 的标准输出。如果docker run命令运行容器的时候,没有使用-it参数,就要用这个命令查看输出。

$ docker container logs [containerID]
(4)docker container exec

docker container exec命令用于进入一个正在运行的 docker 容器。如果docker run命令运行容器的时候,没有使用-it参数,就要用这个命令进入容器。一旦进入了容器,就可以在容器的 Shell 执行命令了。

$ docker container exec -it [containerID] /bin/bash

(5)docker container cp

docker container cp命令用于从正在运行的 Docker 容器里面,将文件拷贝到本机。下面是拷贝到当前目录的写法。

$ docker container cp [containID]:[/path/to/file] .
]]>
0 https://www.sxdeveloper.com/first-meet-docker.html#comments https://www.sxdeveloper.com/feed/first-meet-docker.html
PHP正则表达式 https://www.sxdeveloper.com/php-preg-expression.html https://www.sxdeveloper.com/php-preg-expression.html Fri, 11 May 2018 10:36:00 +0800 明天,你好 正则表达式的作用

匹配,分割,查找,替换字符串

正则表达式分隔符

正斜线(/),hash符号(#),取反符号(~ )

通用原子

 \d(十进制0-9)
 \D(\d的取反)
 \w(数字,字母,下划线)
 \W(\w取反)
 \s(空白符)
 \S(\s取反)

元字符

. (除了换行符以外的任意字符)
* (匹配前面的内容和,出现0次1次或者多次)
 ? (出现0次或者1次)
 ^(以它开头)
 $(以它结尾)
 +(出现一次或者多次)
 {n}恰巧出现n次
 {n,}出现大于等于n次
 {n,m} n<=次数<=m
 [](一个集合如[abc]就是匹配a或b或c)
 ()(互相引用,或者代表一个整体)
 [^](取反如[^abc]除了a,b,c)
 |(或者)
 [-](范围[0-9]代表匹配0-9)

修正模式

 i 不区分大小写
 m 将字符串的每一行分别进行匹配
 e 对匹配的结果做PHP语法处理
 s 将字符串视为单行,换行符作为普通字符.
 U 取消贪婪模式
 x 将模式中的空白忽略.
 A 强制仅从目标字符串的开头开始匹配.
 D 模式中的美元元字符仅匹配目标字符串的结尾.
 u utf-8中文匹配

后向引用

示例:匹配中的内容
1.png

贪婪模式

示例:使用.*?取消贪婪模式
2.png

正则表达式常用函数

3.png

示例

示例1:匹配以139开头的11位手机号

<?php
//匹配139开头的11位手机号码
$str = '13988888888';
$pattern = '/^139\d{8}$/';
preg_match($pattern,$str,$match);
var_dump($match);

运行结果:

array(1) {
  [0]=>
  string(11) "13988888888"
}

示例2:匹配img标签中的所有src属性

<?php
//匹配所有img标签中的src属性
$str = '<img alt = "撒旦阿萨德" id = "11" class = "image" src= "my.jpg" />';
$pattern = '/<img.*?src\s*=\s*"(.*?)".*?\/?/i';
preg_match($pattern,$str,$match);
var_dump($match);

运行结果:

array(2) {
  [0]=>
  string(68) "<img alt = "撒旦阿萨德" id = "11" class = "image" src= "my.jpg""
  [1]=>
  string(6) "my.jpg"
}
]]>
0 https://www.sxdeveloper.com/php-preg-expression.html#comments https://www.sxdeveloper.com/feed/php-preg-expression.html
BBR及BBR魔改及Lotsever(锐速)3合1一键脚本 https://www.sxdeveloper.com/bbr-lotsever-sh.html https://www.sxdeveloper.com/bbr-lotsever-sh.html Tue, 08 May 2018 10:53:00 +0800 明天,你好 需求

仅用于KVM虚拟,脚本包含BBR+BBR魔改版+Lotsever(锐速),同时支持Centos、Debian、Ubuntu系统,很方便,也很强大。

安装

运行以下命令:

wget -N --no-check-certificate "https://raw.githubusercontent.com/chiakge/Linux-NetSpeed/master/tcp.sh" && chmod +x tcp.sh && ./tcp.sh

根据自己需求操作,重启后再使用./tcp.sh命令接着操作。

]]>
0 https://www.sxdeveloper.com/bbr-lotsever-sh.html#comments https://www.sxdeveloper.com/feed/bbr-lotsever-sh.html