«

PHP 异步解决大量接口访问

时间:2023-3-1 22:36     作者:wen     分类: PHP


一、使用fsockopen

通过fsockopen发送请求并忽略返回结果,程序可以马上返回。

示例代码:

$fp = fsockopen("www.example.com", 80, $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
    $out = "GET /backend.php   HTTP/1.1\r\n";
    $out .= "Host: www.example.com\r\n";
    $out .= "Connection: Close\r\n\r\n";

    fwrite($fp, $out);
    /*忽略执行结果
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }*/
    fclose($fp);
}

需要注意的是我们需要手动拼出header头信息。通过打开注释部分,可以查看请求返回结果,但这时候又变成同步的了,因为程序会等待返回结果才结束。

实际测试的时候发现,不忽略执行结果,调试的时候每次都会成功发送sock请求;但忽略执行结果,经常看到没有成功发送sock请求。查看nginx日志,发现很多状态码为499的请求。

后来找到了原因:fwrite之后马上执行fclose,nginx会直接返回499,不会把请求转发给php处理。

客户端主动端口请求连接时,NGINX 不会将该请求代理给上游服务(FastCGI PHP 进程),这个时候 access log 中会以 499 记录这个请求。

解决方案:
1)nginx.conf增加配置

//忽略客户端中断
fastcgi_ignore_client_abort on;

2)fwrite之后使用usleep函数休眠20毫秒:

usleep(20000);
后来测试就没有发现失败的情况了。

附上完整代码:

<?php
/**
 * 工具类
 * */
class FsockService {

    public static function post($url, $param){

        $host = parse_url($url, PHP_URL_HOST);
        $port = 80;
        $errno = '';
        $errstr = '';
        $timeout = 30;

        $data = http_build_query($param);

        // create connect
        $fp = fsockopen($host, $port, $errno, $errstr, $timeout);

        if(!$fp){
            return false;
        }

        // send request
        $out = "POST ${url} HTTP/1.1\r\n";
        $out .= "Host:${host}\r\n";
        $out .= "Content-type:application/x-www-form-urlencoded\r\n";
        $out .= "Content-length:".strlen($data)."\r\n";
        $out .= "Connection:close\r\n\r\n";
        $out .= "${data}";

        fwrite($fp, $out);

        //忽略执行结果;否则等待返回结果
//        if(APP_DEBUG === true){
        if(false){
            $ret = '';
            while (!feof($fp)) {
                $ret .= fgets($fp, 128);
            }
        }

        usleep(20000); //fwrite之后马上执行fclose,nginx会直接返回499

        fclose($fp);
    }

    public static function get($url, $param){
        $host = parse_url($url, PHP_URL_HOST);
        $port = 80;
        $errno = '';
        $errstr = '';
        $timeout = 30;

        $url = $url.'?'.http_build_query($param);

        // create connect
        $fp = fsockopen($host, $port, $errno, $errstr, $timeout);

        if(!$fp){
            return false;
        }

        // send request
        $out = "GET ${url} HTTP/1.1\r\n";
        $out .= "Host:${host}\r\n";
        $out .= "Connection:close\r\n\r\n";

        fwrite($fp, $out);

        //忽略执行结果;否则等待返回结果
//        if(APP_DEBUG === true){
        if(false){
            $ret = '';
            while (!feof($fp)) {
                $ret .= fgets($fp, 128);
            }
        }

        usleep(20000); //fwrite之后马上执行fclose,nginx会直接返回499

        fclose($fp);
    }

}

?>

二、curl_multi实现并发

PHP中的curl_multi系列函数可以实现同时请求多个URL来实现并发,而不是像普通curl函数那样请求后会阻塞,直到结果返回才进行下一个请求。因此在批量请求URL时可通过curl_multi系列函数提升程序的运行效率。

function curl_multi($urls)
    {
        $result = [];
        //1 创建批处理cURL句柄
        $mh = curl_multi_init();
        $chArr = [];
        //创建多个cURL资源
        for ($i = 0; $i < count($urls); $i++) {
            $chArr[$i] = curl_init();
            curl_setopt($chArr[$i], CURLOPT_URL, $urls[$i]);
            curl_setopt($chArr[$i], CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($chArr[$i], CURLOPT_TIMEOUT, 1);
            //2 增加句柄
            curl_multi_add_handle($mh, $chArr[$i]);
        }

        $active = null;

        /**
         * 本次循环第一次处理 $mh 批处理中的 $ch 句柄,并将 $mh 批处理的执行状态写入 $active,
         * 当状态值等于 CURLM_CALL_MULTI_PERFORM 时,表明数据还在写入或读取中,执行循环,
         * 当第一次 $ch 句柄的数据写入或读取成功后,状态值变为 CURLM_OK ,跳出本次循环,进入下面的大循环中。
         */
        do {
            //处理在批处理栈中的每一个句柄
            $mrc = curl_multi_exec($mh, $active);
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        /**
         * 上面这段代码中,是可以直接使用 $active > 0 来作为 while 的条件,如下:
         * do {
         *     $mrc = curl_multi_exec($mh, $active);
         * } while ($active > 0);
         * 此时如果整个批处理句柄没有全部执行完毕时,系统会不停的执行 curl_multi_exec 函数,从而导致系统CPU占用会很高,
         * 因此一般不采用这种方案,可以通过 curl_multi_select 函数来达到没有需要读取的程序就阻塞住的目的。
         */

        /**
         * $active 为 true 时,即 $mh 批处理之中还有 $ch 句柄等待处理,
         * $mrc == CURLM_OK,即上一次 $ch 句柄的读取或写入已经执行完毕。
         */
        while ($active && $mrc == CURLM_OK) {
            /**
             * 程序进入阻塞状态,直到批处理中有活动连接(即 $mh 批处理中还有可执行的 $ch 句柄),
             * 这样执行的好处是 $mh 批处理中的 $ch 句柄会在读取或写入数据结束后($mrc == CURLM_OK)进入阻塞阶段,
             * 而不会在整个 $mh 批处理执行时不停地执行 curl_multi_exec 函数,白白浪费CPU资源。
             */
            if (curl_multi_select($mh) != -1) {
                //程序退出阻塞状态继续执行需要处理的 $ch 句柄
                do {
                    $mrc = curl_multi_exec($mh, $active);
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
            }
        }

        foreach ($chArr as $k => $ch) {
            //5 获取句柄的返回值
            $result[$k] = curl_multi_getcontent($ch);
            //6 将$mh中的句柄移除
            curl_multi_remove_handle($mh, $ch);
        }
        //7 关闭全部句柄
        curl_multi_close($mh);
        return $result;
    }