PHP的非阻塞或并行请求实现方式

发布于 20 天前  24 次阅读


我们都知道,php是串行请求的,我们在碰到以下几个场景的时候,php的效率会变的比较低下:

1:一个请求,在输出结果前,有比较长时间的耗时操作(但这个操作不影响输出结果),这时候,客户端会等待比较长的时间。
2:如果我要同时获得多个远程接口的结果,耗时是所有接口响应耗时之和

那么,有没有办法来提升上述场景的效率呢?

答案是肯定的:

1:针对第一个场景,若你使用的是FastCGI模式 ,使用fastcgi_finish_request()能把结果输出到客户端,但PHP进程继续在跑

<?php
fastcgi_finish_request();

echo "xtgxiso"//这儿结束之后的执行

备注:此方式不能算是非阻塞,只是把结果尽快输出到客户端,但本身这个php进程还是被阻塞的占用着。

2:使用curl_multi_init的方法,进行并行url请求

<?php
$time = time();

// 创建3个cURL资源
$ch1 = curl_init();
$ch2 = curl_init();
$ch3 = curl_init();

// 设置URL和相应的选项
curl_setopt($ch1, CURLOPT_URL, 
"http://test.xtgxiso.cn/sleep1.php");
curl_setopt($ch1, CURLOPT_HEADER, 0);
curl_setopt($ch2, CURLOPT_URL, 
"http://test.xtgxiso.cn/sleep2.php");
curl_setopt($ch2, CURLOPT_HEADER, 0);
curl_setopt($ch3, CURLOPT_URL, 
"http://test.xtgxiso.cn/sleep3.php");
curl_setopt($ch3, CURLOPT_HEADER, 0);

// 创建批处理cURL句柄
$mh = curl_multi_init();

// 增加3个句柄
curl_multi_add_handle($mh,$ch1);
curl_multi_add_handle($mh,$ch2);
curl_multi_add_handle($mh,$ch3);
$running=null;

// 执行批处理句柄
do {
    usleep(10000);
    curl_multi_exec($mh,$running);
} 
while ($running > 0);

// 关闭全部句柄
curl_multi_remove_handle($mh, $ch1);
curl_multi_remove_handle($mh, $ch2);
curl_multi_remove_handle($mh, $ch3);
curl_multi_close($mh);
echo "\n total time : ".(time()-$time)."\n";

3:使用stream_set_blocking + select方法,进行并行url请求

// url数组,每个url发送一个请求

$urls = array(
    "sleep1.php",
    "sleep2.php",
    "sleep3.php"
);

// 保存socket的数组
$sockets = array();

$time = time();

// 批量创建链接并发送数据

foreach($urls as $url){
    $socket = stream_socket_client("tcp://test.xtgxiso.cn:80", $errno, $errstr, 3);
    // 设置成非阻塞
    stream_set_blocking($socket, 0);
    fwrite($socket, "GET /{$url} HTTP/1.0\r\nHost: test.xtgxiso.cn\r\nAccept: */*\r\n\r\n");
    // 记录数组
    $sockets[(int)$socket] = $socket;

}

// 批量等待数据返回
while(count($sockets)>0){
    $read = $sockets;
    $write = $e = array();
    // 等待数据可读
    if(stream_select($read, $write, $e, 10))
    {
        // 循环读数据
        foreach($read as $socket)
        {
            // 这里是服务端返回的数据,需要的话可以循环读
            echo fread($socket, 8192)."\n";
            // 数据读取完毕关闭链接,并删除链接
            fclose($socket);
            unset($sockets[(int)$socket]);
        }
    }
}

echo time()-$time;echo "\n";

上面两种方式请求多个url的时候,时间由原来的所有请求响应时间之和变为只是最长的那个请求的响应时间(如请求1:10ms,请求2:15ms,请求3:20ms,串行处理的时间将是:45ms,而并行处理的时间只有:25ms),从而提高效率和并发,我们可以封装个get请求的方法,post方法和get类似!

function http_get_url($arr=''){
    if ( is_array($arr) && $arr ){
        $sockets = array();
        foreach($arr as $url)
        {
            $url_info = parse_url($url);
            if ( !@$url_info["port"] ){
                $url_info["port"] = 80;
            }
            $socket = stream_socket_client("tcp://".$url_info["host"].":".$url_info["port"], $errno, $errstr,3);
            if ( $socket ) {
                stream_set_blocking($socket, 0);
                $str = "GET ".$url_info["path"]."?".$url_info["query"]." HTTP/1.0\r\nHost: ".$url_info["host"]."\r\nAccept: */*\r\n\r\n";
                echo $str;
                fwrite($socket,$str);
                $sockets[(int)$socket] = $socket;
            }
        }

        while(count($sockets)>0)
        {
            $read = $sockets;
            $write = $e = array();
            if(stream_select($read, $write, $e, 3))
            {
                foreach($read as $socket)
                {
                    $result[(int)$socket] .= fread($socket, 8192);
                    unset($sockets[(int)$socket]);
                }
            }
        }
        foreach ($result as  $k=>$v) {
            $result[$k] = trim(strstr($v,"\r\n\r\n"),"\r\n");
        }

        return $result;
    }else if ( $arr ){
        $curl = curl_init (); 
        curl_setopt($curl, CURLOPT_URL,$arr);
        curl_setopt($curl, CURLOPT_HEADER, 0);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_TIMEOUT_MS,3000);
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($curl, CURLOPT_MAXREDIRS, 3);
        $result = curl_exec($curl);
        curl_close($curl);
        return $result;
    }else{
        return false;
    }}

这个方法可以get一个或多个url,尤其在多个的时候,可以提高效率!

备注:
1: curl_multi和stream_select 都是调用 系统的select进行多路i/o复用

2: 并行请求的场景,用swoole也更合适,fpm里,通过swoole_client,把url发送到swoole的server, swoole_server天然支持并行请求,把汇总的结果返回到fpm。

3:开启PHP子进程也是一个并行的方法,不过回收有些麻烦。

 

下次说说多路i/o复用


公交车司机终于在众人的指责中将座位让给了老太太