17 November 2017

异步查询 dns

class AsyncDns implements Async {
   public function begin($continuation) {
       swoole_async_dns_lookup("www.baidu.com", function ($host, $ip) use ($continuation) {
           // deal with $host $ip
           echo $ip . PHP_EOL;
           $continuation(null);
       });
   }
}

function newGen() {
   echo date('Y-m-d H:i:s', time()) . PHP_EOL;
   yield new AsyncDns();
   echo date('Y-m-d H:i:s', time()) . PHP_EOL;
}

$task = new AsyncTask(newGen());
$task->begin();

我们先要看看 AsyncTask 这个 class

if ($value instanceof Generator) {
    $value = new self($value);
}

if ($value instanceof Async) {
    $async = $value;
    $continuation = [$this, "next"];
    $async->begin($continuation);
} else {
    $this->next($value);
}

其实上面是一种简化写法

if ($value instanceof Generator) {
    $new_task = new AsyncTask($value);
    $new_continuation = [$this, 'next'];
    $new_task->begin($new_continuation);
    return;
}

if ($value instanceof Async) {
    $new_continuation = [$this, 'next'];
    $value->begin($new_continuation);
    return;
}

else {
    $this->next($value);
    return;
}

异步查询 dns 所做的事情
首先是调用 swoole_async_dns_lookup,再让这个函数的 callback 来调用我们在 AsynTask 里面保存的 continuation 就可以让 AsyncTask 这个处理程序继续执行。

AsyncDns 的 begin 函数里面的 continuation 参数就是从 AsyncTask 传进来的 continuation

once 函数

once 使得一个函数在多次调用只会执行一次,实际上,他不是真正的只执行一次,而是在内部保存了一个值,第一次执行就改了这个值,下次执行的时候就直接忽略了

function once(callable $f) {
    $has = false;

    return function (...$args) use ($f, &$has) {  // 注意这里的 $has 是引用的
        if ($has === false) {
            $has = true;
            $f(...$args);
        } else {
            // ...
        }
    };
}

$myFunc = function () {
    echo 'ok' . PHP_EOL;
};

$myFunc = once($myFunc);

$myFunc();
$myFunc();
$myFunc();

timeoutWrapper

timeoutWrapper 实际上是 once 函数的一个应用

function timeoutWrapper(callable $fun, $timeout) {
    return function($k) use($fun, $timeout) {
        $k = once($k);
        $fun($k);
        swoole_timer_after($timeout, function() use($k) { // 这⾥异常可以从外部传⼊
            $k(null, new Exception("timeout"));
        });
    };
}

首先来看看下面的两个函数

function callcc(callable $fun, $timeout = 0) {
    if ($timeout > 0) {
        $fun = timeoutWrapper($fun, $timeout);
    }

    return new CallCC($fun);
}

function async_dns_lookup($host, $timeout = 100) {
    return callcc(function ($k) use ($host) {
        swoole_async_dns_lookup($host, function ($host, $ip) use ($k) {
            $k($ip);
        });
    }, $timeout);
}

function f() {
    try {
        yield async_dns_lookup("www.baidu.com", 1);
    } catch (Exception $ex) {
        echo $ex->getMessage() . PHP_EOL;
    }
}

$gen = f();
(new AsyncTask($gen))->begin();

f() 来看,函数首先执行了 async_dns_lookup,如果有异常就捕获。

async_dns_lookup 这个函数调用了 callcc,callcc 返回 CallCC 的对象是一个 Async,丢到 AsyncTask 里面就会自动执行 begin,begin 会带着 AsyncTask 传来的 continuation 作为自己的参数。即 CallCC 对象的 fun 参数都是一个带有一个参数的,这个参数在 fun 运行时,会由 AsyncTask 传入为 AsyncTask 的 continuation。

经过 timeoutWrapper 封装过之后,我们的这个 fun 就会变得有点奇怪。

$k = once($k);
$fun($k);
swoole_timer_after($timeout, function () use ($k) {
    $k(null, new Exception("timeout here"));
});

上面的意思是,要么由 $fun 来调用 continuation,要么由 swoole_time_after 来调用 continuation
实际上,和下面的代码是类似的

swoole_timer_after(10000, function () {
    echo 'timeout';
});

echo 'end' . PHP_EOL;

上面的代码会打印 end,然后等待 10 秒再打印 timeout

spawn