基于CURL_MULTI*系列函数的异步HTTP客户端

PHP Jenner 9915℃ 0评论

同步与异步

首先我们先通过一段简单的代码理解下什么是异步编程,再讲解如何使用异步HTTP客户端。
一般情况下,我们执行HTTP请求类似如下:

$result = $http->get($url, $params);

如果我们有多个HTTP请求要执行,类似如下:

$result_1 = $http->get($url_1, $params);
$result_2 = $http->get($url_2, $params);
$result_3 = $http->get($url_3, $params);
$result_4 = $http->get($url_4, $params);
...

我们看到上述代码是串行执行的。如果1、2、3、4中有任何一个请求发生了阻塞,则整个进程将发生阻塞,效率急剧下降。
异步的方式,可以让我们发出请求后不立即获取结果,等到请求到达时,再去处理。而请求可以是并行的,这种情况下,程序的执行时间,执行时间最长的一个请求。

比较常见的异步http客户端实现如下:

$http->get($url, $params, function($data){
	echo "I get the response data" . PHP_EOL;
})->run();

这里需要注意,run方法的实现一般是阻塞的,用于监听时间循环。如果你想同时请求多个,类似如下:

$http->get($url, $params, function($data){
	echo "I get the response data" . PHP_EOL;
})->get($url, $params, function($data){
	echo "I get the response data" . PHP_EOL;
})->get($url, $params, function($data){
	echo "I get the response data" . PHP_EOL;
})->run();

这种异步客户端的实现原理一般是基于epoll或select实现。php扩展中有libevent、libev支持封装了网络库,可以相对简单的实现一个异步的socket客户端。如果你的需求并不复杂,例如:你不需要SSL,不需要HTTP认证等,完全可以使用libev*实现一个socket客户端再进行一次HTTP协议的简易封装。但如果你的HTTP请求非常复杂,能够在异步的情况下重用curl*函数,则会省掉很多麻烦事。

async-http-php

项目地址:https://github.com/huyanping/async-http-php
async-http-php 是一个基于curl_mulit*系列函数实现的简易HTTP异步客户端。curl_multi*系列函数的实现原理也是select多路复用技术。简单、快速。

一个非常简单的例子如下:

$async = new JennerHttpAsync();
$task = JennerHttpTask::createGet("http://www.baidu.com");
$async->attach($task, "baidu");

$task2 = JennerHttpTask::createGet("http://www.sina.com");
$async->attach($task2, "sina");

$task3 = JennerHttpTask::createGet("http://www.qq.com");
$async->attach($task3, "qq");

/**
 * you can do something here before receive the http responses
 * eg. query data from mysql or redis.
 */

$async-start();

while (true) {
    // nonblock
    if (!$async->isDone()) {
        echo "I am running" . PHP_EOL;
        sleep(1);
        continue;
    }

    $result = $async->execute();
    print_r($result);
    break;
}

/**
 * or you just call execute. it will block the process until all tasks are done.
 * $result = $async->execute();
 * print_r($result);
 */

async对象提供了一个isDone方法,用于查询当前所有任务是否结束。在执行execute之前,你还可以做其他事情,例如查询mysql、redis等。
http响应并不是像我们封装的那样,一起返回的。也就是说,我们可以在响应到达的那一刻对他进行处理,例如只要请求到达,直接写入文件。

一个简单的示例代码如下:

$async = new JennerHttpAsync();
$task = JennerHttpTask::createGet("http://www.baidu.com");
$task->registerHandler(function ($info, $error, $content) {
    echo "get baidu response. content length:" . strlen($content) . PHP_EOL;
});
$async->attach($task, "baidu");

$task2 = JennerHttpTask::createGet("http://www.sina.com");
$task2->registerHandler(function ($info, $error, $content) {
    echo "get sina response. content length:" . strlen($content) . PHP_EOL;
});
$async->attach($task2, "sina");


$task3 = JennerHttpTask::createGet("http://www.qq.com");
$task3->registerHandler(function ($info, $error, $content) {
    echo "get qq response. content length:" . strlen($content) . PHP_EOL;
});
$async->attach($task3, "qq");


$result = $async->execute();
echo count($result);

可以结合Promise模式使用:

$async = new \Jenner\Http\Async();
$task = \Jenner\Http\Task::createGet("http://www.baidu.com");
$promise = $async->attach($task, "baidu");

$promise->then(
    function ($data) {
        echo 'success:' . var_export($data, true) . PHP_EOL;
    },
    function ($data) {
        echo 'error:' . var_export($data, true) . PHP_EOL;
    }
);

$async->execute();

实现原理

/**
 * @return array
 */
public function execute()
{
    $responses = array();

    do {
        while (($code = curl_multi_exec($this->curl, $active)) == CURLM_CALL_MULTI_PERFORM) ;

        if ($code != CURLM_OK) {
            break;
        }

        // a request was just completed -- find out which one
        while ($done = curl_multi_info_read($this->curl)) {

            // get the info and content returned on the request
            $info = curl_getinfo($done['handle']);
            $error = curl_error($done['handle']);
            $content = curl_multi_getcontent($done['handle']);

            $responses[] = compact('info', 'error', 'content');

            // remove the curl handle that just completed
            curl_multi_remove_handle($this->curl, $done['handle']);
            curl_close($done['handle']);
        }

        // Block for data in / output; error handling is done by curl_multi_exec
        if ($active > 0) {
            curl_multi_select($this->curl, 0.05);
        }

    } while ($active);

    curl_multi_close($this->curl);

    return $responses;
}

curl_multi_exec用于执行多个curl请求,active参数是一个引用参数,它会返回当前仍然在运行的请求数量。当curl_multi_exec返回CURLM_CALL_MULTI_PERFORM时,表示程序当前仍在接收或发送数据,如果返回值为CURLM_OK,则表示当前有请求已经执行结束。调用curl_multi_info_read方法可以读出已经完成的请求信息。结合返回值和active参数,我们可以判断当前是否所有请求已经结束,这也是isDone方法的实现原理。

性能测试

测试环境:

CPU:单核

内存:1G

网络环境:从美国请求qq、baidu、sina,分别请求20次,加动态参数避免各种缓存。

结论:完胜同步执行。执行时间一般取决于执行时间最长的一次请求。

# 同步
[root@huyanping async-http-php]# php tests/performance_sync.php  
------------------------------------------
mark:[total diff]
time:55.121547937393s
memory_real:1536KB
memory_emalloc:1300.5859375KB
memory_peak_real:2304KB
memory_peak_emalloc:1898.640625KB
# 异步
[root@huyanping async-http-php]# php tests/performance_async.php 
------------------------------------------
mark:[total diff]
time:4.6412570476532s
memory_real:256KB
memory_emalloc:187.7109375KB
memory_peak_real:13312KB
memory_peak_emalloc:10387.8671875KB

原创文章,转载请注明: 转载自始终不够

本文链接地址: 基于CURL_MULTI*系列函数的异步HTTP客户端

转载请注明:始终不够 » 基于CURL_MULTI*系列函数的异步HTTP客户端

喜欢 (0)
    • 是哦,之前用过guzzle,感觉有点过度封装,之后没再了解。当学习了,嘿嘿