marcus-ma / myBlog

写写开发笔记

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

PHP利用fsockopen发起异步请求

marcus-ma opened this issue · comments

情景导入

  PHP执行程序一般属于同步阻塞模式,有些程序可能几毫秒就完成了,也有可能几分钟都完成不了。就拿日常最常见的程序例子:用户在注册时要执行邮件发送的逻辑,若此时这段逻辑执行耗时过长,会导致浏览器直接与服务器断开连接,整个注册逻辑就走不通了。
  而就是这些时候,我们其实并不关心这些耗时脚本的返回结果,只要执行就行了。这时候就需要采用异步的方式执行。

想法

  而在PHP中是没有直接支持多线程这种东西,一般需要依赖一些扩展,如pthreads或者Swoole等。
  如果不想折腾的话,其实还有一个折衷的办法:使用自带的函数fsockopen。通过fsockopen发送请求并忽略返回结果,程序可以马上返回。
  示例代码:

$fp = fsockopen("www.marcusma.top", 80, $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
    $out = "GET /   HTTP/1.1\r\n";
    $out .= "Host: www.marcusma.top\r\n";
    $out .= "Connection: Close\r\n\r\n";
 
    fwrite($fp, $out);
    /*忽略执行结果
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }*/
    fclose($fp);
    echo 123;
}

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

探究

  实际测试的时候发现,不忽略执行结果,调试的时候每次都会成功发送sock请求;但忽略执行结果,经常看到没有成功发送sock请求。查看nginx日志,发现很多状态码499的请求。
  后来找到了原因:fwrite之后马上执行fclosenginx会直接返回499,不会把请求转发给php处理。

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


解决方案:
1)nginx.conf增加配置
# 忽略客户端中断
fastcgi_ignore_client_abort on;

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

后来测试就没有发现失败的情况了。

  示例代码:

$fp = fsockopen("www.marcusma.top", 80, $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
    $out = "GET /   HTTP/1.1\r\n";
    $out .= "Host: www.marcusma.top\r\n";
    $out .= "Connection: Close\r\n\r\n";
    //发送异步请求,不必等待回应
    fwrite($fp, $out);
    //沉睡20毫秒,防止Nginx直接返回499
    //fwrite之后马上执行fclose,nginx会直接返回499,不会把请求转发给php处理
    usleep(20000);
    fclose($fp);
    //执行其他逻辑
    echo 123;
}

封装

function async_get($url,$port = 80,$param=''){
    $host = parse_url($url, PHP_URL_HOST);
    $errno = '';
    $errstr = '';
    $timeout = 30;
    $APP_DEBUG = false;
    is_array($param)&&$param = $url.'?'.http_build_query($param);

    $fp = fsockopen($host, $port, $errno, $errstr, $timeout);
    if (!$fp) {
        echo "$errstr ($errno)<br />\n";
    } else {
        $out = "GET /{$param} HTTP/1.1\r\n";
        $out .= "Host: {$host}\r\n";
        $out .= "Connection: Close\r\n\r\n";
        fwrite($fp, $out);
        if ($APP_DEBUG){
            while (!feof($fp)) {
                echo fgets($fp, 128);
            }
        }
        usleep(20000);
        fclose($fp);
    }
}


function async_post($url,$port = 80,$data=''){
    $host = parse_url($url, PHP_URL_HOST);
    $path = parse_url($url, PHP_URL_PATH);
    $errno = '';
    $errstr = '';
    $timeout = 30;
    $APP_DEBUG = false;
    is_array($data)&&$data = http_build_query($data);
    $len = strlen($data);


    $fp = fsockopen($host,$port,$errno,$errstr, $timeout);
    if (!$fp) {
        echo "$errstr ($errno)\n";
    } else {
        $out = "POST {$path} HTTP/1.1\r\n";
        $out .= "Host: {$host}\r\n";
        $out .= "Content-type: application/x-www-form-urlencoded\r\n";
        $out .= "Connection: Close\r\n";
        $out .= "Content-Length: {$len}\r\n";
        $out .= "\r\n";
        $out .= $data."\r\n";
        fwrite($fp, $out);
        if ($APP_DEBUG){
            while (!feof($fp)) {
                echo fgets($fp, 128);
            }
        }
        usleep(20000);
        fclose($fp);
    }
}

async_get("localhost",8080,['id'=>123]);
async_post("localhost",8080,['id'=>123]);

参考

1. php中使用fsockopen实现异步请求:飞鸿影
2. PHP实现异步调用方法研究:鸟哥