PHP实现类似javascript的setTimeout异步回调

在php中,我们偶尔会希望这样的场景:当执行某段程序的时候,需要执行某个任务,但是该任务并不需要立即执行,而是需要延迟几秒再执行。这个功能正如javascript的setTimeout,但是php中并没有现成的异步功能,php的单线程的,所以要实现这个效果,我们还需要想想其他的办法。

不过幸运的是,php中提供了一种非阻塞的通信方式。因此,我们创建下面这个类:

<?php

class AsyncCallback {    private $auth;    public function __construct()    {        $this->auth = substr(md5('iosnae23a9a~40^33fsdf.adsfie*'),8,16); // 修改内容,作为密钥        $headers = array();        foreach ($_SERVER as $key => $value) {            if('HTTP_' == substr($key,0,5)) {                $key = substr($key,5);                $key = strtolower($key);                $headers[$key] = $value;            }        }        $this->headers = $headers;        if(isset($this->headers['auth']) && $this->headers['auth'] == $this->auth)        {            ignore_user_abort();            set_time_limit(0);            $event = $this->headers['event'];            $data = $this->headers['data'];            parse_str($data,$data);            call_user_func(array($this,$event.'Callback'),$data);            exit;        }    }    public function setTimeout($function,$timeout)    {        $this->sock('setTimeout',array('function' => $function,'timeout' => $timeout));    }    private function setTimeoutCallback($data) {        $function = $data['function'];        $timeout = $data['timeout'];        sleep($timeout);        call_user_func($function);    }    private function sock($event,$data) {        $url = $this->current_url();        $host = parse_url($url,PHP_URL_HOST);        $path = parse_url($url,PHP_URL_PATH);        $query = parse_url($url,PHP_URL_QUERY);        if($query)            $path .= '?'.$query;        $port = parse_url($url,PHP_URL_PORT);        $port = $port ? $port : 80;        $scheme = parse_url($url,PHP_URL_SCHEME);        if($scheme == 'https')            $host = 'ssl://'.$host;        $data = http_build_query($data);        $fp = fsockopen($host,$port,$errno,$errstr,1);        if(!$fp) {            return false;        }        stream_set_blocking($fp,0);        stream_set_timeout($fp,1);        $header = "GET $path  / HTTP/1.1\r";        $header .= "Host: $host\r";        $header .= "Event: $event\r";        $header .= "Data: $data\r";        $header .= "Auth: {$this->auth}\r";        $header .= "Connection: Close\r\r";        fwrite($fp,$header);        fclose($fp);        return true;    }    private function current_url() {        $url = $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];        if(!isset($_SERVER['HTTPS']))            $url = 'http://'.$url;        elseif($_SERVER['HTTPS'] === 1 || $_SERVER['HTTPS'] === 'on' || $_SERVER['SERVER_PORT'] == 443)            $url = 'https://'.$url;        else            $url = 'http://'.$url;        return $url;    }}

我们来看下具体的用法:

<?php

require 'AsyncCallback.class.php';$AsyncCallback = new AsyncCallback();

file_put_contents('./runtime/setTimeout.txt','['.date('Y-m-d H:i:s').']['.microtime().'] 我先处理一点事情',LOCK_EX);

$AsyncCallback->setTimeout('setTimeout',10);function setTimeout() {    file_put_contents('./runtime/setTimeout.txt',"".'['.date('Y-m-d H:i:s').']['.microtime().'] 我是延时执行的结果',FILE_APPEND);}

file_put_contents('./runtime/setTimeout.txt',"".'['.date('Y-m-d H:i:s').']['.microtime().'] 我再处理一点事情,看看是否有时间差距',FILE_APPEND);

接下来我们来解释一下这里面都做了什么。

首先,sock()方法

我们来具体分析一下sock方法中fsockopen的用法。fsockopen开启了一个fsock通信,stream_set_blocking()函数的第二个参数设置为0,声明这个fsock为非阻塞模式,也就是说,下次在调用$this->sock()的时候,程序是流畅执行的,fsock发出请求时,不会阻塞进程,fsock发出请求后就继续往下执行,而无需得到结果。

fsock使用fwrite或fput发出请求,其中第二个参数需要我们自己构造http header信息,你需要了解http协议的东西,构造header可以帮我们实现get、post、head、put、delete等多种请求方式。

总之,sock()方法,帮我们实现非阻塞的发出请求。

其次,异步执行

我们来看下__construct()方法中的 if(isset($this->headers['auth']) && $this->headers['auth'] == $this->auth) 的内容。

ignore_user_abort();set_time_limit(0);

这两句可以帮助程序在后台继续执行,即使访问被客户端关闭,也就是说fsock发出的非阻塞请求虽然并没有等结果返回,但是我们如此声明以后,程序仍然会继续执行,不会被中断。

call_user_func(array($this,$event.'Callback'),$data);

这句则是调用当前事件的回调函数(方法),这里就是$this->setTimeoutCallback(),而在setTimeoutCallback()中,使用了

call_user_func($function);

也就是调用我们第二段代码中规定的那个setTimeout()函数。

最后,回调函数

而这个setTimeout()函数就是回调函数,它会在fsock请求的终端进行执行,而它的执行将会开启一个新的php进程,不会影响当前的这个页面的程序的执行(虽然它是被写在当前页面内的)。

关于php的进程,请阅读《PHP进程分支设计》,了解php单线程的局限。

总之,你只需要在你的程序中引入AsyncCallback类,并且实例化后如下操作即可:

$AsyncCallback = new AsyncCallback();$AsyncCallback->setTimeout('setTimeout',10);function setTimeout() {    // 延时执行的代码}

不过,本类的使用,也有一些缺陷:

  • 和javascript不同,该类不能在界面展示上实现异步展示,而只能实现任务上的异步
  • 必须在页面开头的地方实例化类,因为回调是在类的构造方法中实现的
  • 不能和主进程通信,这一点是最大的缺陷

2016-02-18

为价值买单

向我捐赠1-5RMB