PHP实现并发请求

PHP实现并发请求

后端服务开发中经常会有并发请求的需求,比如你需要获取10家供应商的带宽数据(每个都提供不同的 url ),然后返回一个整合后的数据,你会怎么做呢?

PHP 中,最直观的做法 foreach 遍历 urls ,并保存每个请求的结果即可,那么如果供应商提供的接口平均耗时 5s ,你的这个接口请求耗时就达到了 50s ,这对于追求速度和性能的网站来说是不可接受的。

这个时候你就需要并发请求了。

PHP 请求

PHP 是单进程同步模型,一个请求对应一个进程, I/O 是同步阻塞的。通过 nginx/apache/php-fpm 等服务的扩展,才使得PHP提供高并发的服务,原理就是维护一个进程池,每个请求服务时单独起一个新的进程,每个进程独立存在。

PHP 不支持多线程模式和回调处理,因此 PHP 内部脚本都是同步阻塞式的,如果你发起一个 5s 的请求,那么程序就会 I/O 阻塞 5s ,直到请求返回结果,才会继续执行代码。因此做爬虫之类的高并发请求需求很吃力。

那怎么来解决并发请求的问题呢?除了内置的 file_get_contents fsockopen 请求方式, PHP 也支持 cURL 扩展来发起请求,它支持常规的单个请求: PHP cURL请求详解 ,也支持并发请求,其并发原理是 cURL 扩展使用多线程来管理多请求。

PHP 并发请求

我们直接来看代码 demo :

// 简单demo,默认支持为GET请求
public function multiRequest($urls) {
    $mh = curl_multi_init();
    $urlHandlers = [];
    $urlData = [];
    // 初始化多个请求句柄为一个
    foreach($urls as $value) {
        $ch = curl_init();
        $url = $value['url'];
        $url .= strpos($url, '?') ? '&' : '?';
        $params = $value['params'];
        $url .= is_array($params) ? http_build_query($params) : $params;
        curl_setopt($ch, CURLOPT_URL, $url);
        // 设置数据通过字符串返回,而不是直接输出
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $urlHandlers[] = $ch;
        curl_multi_add_handle($mh, $ch);
    $active = null;
    // 检测操作的初始状态是否OK,CURLM_CALL_MULTI_PERFORM为常量值-1
    do {
        // 返回的$active是活跃连接的数量,$mrc是返回值,正常为0,异常为-1
        $mrc = curl_multi_exec($mh, $active);
    } while ($mrc == CURLM_CALL_MULTI_PERFORM);
    // 如果还有活动的请求,同时操作状态OK,CURLM_OK为常量值0
    while ($active && $mrc == CURLM_OK) {
        // 持续查询状态并不利于处理任务,每50ms检查一次,此时释放CPU,降低机器负载
        usleep(50000);
        // 如果批处理句柄OK,重复检查操作状态直至OK。select返回值异常时为-1,正常为1(因为只有1个批处理句柄)
        if (curl_multi_select($mh) != -1) {
            do {
                $mrc = curl_multi_exec($mh, $active);
            } while ($mrc == CURLM_CALL_MULTI_PERFORM);
    // 获取返回结果
    foreach($urlHandlers as $index => $ch) {
        $urlData[$index] = curl_multi_getcontent($ch);
        // 移除单个curl句柄
        curl_multi_remove_handle($mh, $ch);
    curl_multi_close($mh);
    return $urlData;

在该并发请求中,先创建一个批处理句柄,然后将 url cURL 句柄添加到批处理句柄中,并不断查询批处理句柄的执行状态,当执行完成后,获取返回的结果。

curl_multi 相关函数

/** 函数作用:返回一个新cURL批处理句柄
    @return resource 成功返回cURL批处理句柄,失败返回false
resource curl_multi_init ( void )
/** 函数作用:向curl批处理会话中添加单独的curl句柄
    @param $mh 由curl_multi_init返回的批处理句柄
    @param $ch 由curl_init返回的cURL句柄
    @return resource 成功返回cURL批处理句柄,失败返回false
int curl_multi_add_handle ( resource $mh , resource $ch )
/** 函数作用:运行当前 cURL 句柄的子连接
    @param $mh 由curl_multi_init返回的批处理句柄
    @param $still_running 一个用来判断操作是否仍在执行的标识的引用
    @return 一个定义于 cURL 预定义常量中的 cURL 代码
int curl_multi_exec ( resource $mh , int &$still_running )
/** 函数作用:等待所有cURL批处理中的活动连接
    @param $mh 由curl_multi_init返回的批处理句柄
    @param $timeout 以秒为单位,等待响应的时间
    @return 成功时返回描述符集合中描述符的数量。失败时,select失败时返回-1,否则返回超时(从底层的select系统调用).
int curl_multi_select ( resource $mh [, float $timeout = 1.0 ] )
/** 函数作用:移除cURL批处理句柄资源中的某个句柄资源
    说明:从给定的批处理句柄mh中移除ch句柄。当ch句柄被移除以后,仍然可以合法地用curl_exec()执行这个句柄。如果要移除的句柄正在被使用,则这个句柄涉及的所有传输任务会被中止。
    @param $mh 由curl_multi_init返回的批处理句柄
    @param $ch 由curl_init返回的cURL句柄
    @return 成功时返回0,失败时返回CURLM_XXX中的一个
int curl_multi_remove_handle ( resource $mh , resource $ch )
/** 函数作用:关闭一组cURL句柄
    @param $mh 由curl_multi_init返回的批处理句柄
    @return void
void curl_multi_close ( resource $mh )