X  
登录

还没有账号? 立即注册

忘记密码?
登陆
X  
注册

已经有账号? 马上登陆

获取验证码
重新获取(60s)
立即注册
统计
  • 建站日期:22-09:01
  • 文章总数:80 篇
  • 评论总数:2 条
  • 分类总数:8 个
  • 最后更新:2024年07月05日
文章 技术教程

PHP中curl_multi并发详解

首页 技术教程 正文


本文实例讲述了PHP curl批处理及多请求并发实现方法。分享给大家供大家参考,具体如下:

在面试过程中遇到一个问题,加入一个一个网站访问一次需要两秒,我们如何实现在2秒左右请求三次?

面试官想问的就是如何使用curl并发处理请求

关于curl_multi_init()

一般来说,想到要用curl_multi_init()时,目的是要同时请求多个url,而不是一个一个依次请求,否则就要curl_init()了。

不过,在使用curl_multi的时候,你可能遇到cpu消耗过高、网页假死等现象,可以看看《PHP使用curl_multi_select解决curl_multi网页假死问题》

使用curl_multi的步骤总结如下:

  • 第一步:调用curl_multi_init

  • 第二步:循环调用curl_multi_add_handle

    这一步需要注意的是,curl_multi_add_handle的第二个参数是由curl_init而来的子handle。

  • 第三步:持续调用curl_multi_exec

  • 第四步:根据需要循环调用curl_multi_getcontent获取结果

  • 第五步:调用curl_multi_remove_handle,并为每个字handle调用curl_close

  • 第六步:调用curl_multi_close

各函数作用解释:

curl_multi_init() 初始化一个curl批处理句柄资源。

curl_multi_add_handle() 向curl批处理会话中添加单独的curl句柄资源。curl_multi_add_handle()函数有两个参数,第一个参数表示一个curl批处理句柄资源,第二个参数表示一个单独的curl句柄资源。

curl_multi_exec() 解析一个curl批处理句柄,curl_multi_exec()函数有两个参数,第一个参数表示一个批处理句柄资源,第二个参数是一个引用值的参数,表示剩余需要处理的单个的curl句柄资源数量。

curl_multi_remove_handle() 移除curl批处理句柄资源中的某个句柄资源,curl_multi_remove_handle()函数有两个参数,第一个参数表示一个curl批处理句柄资源,第二个参数表示一个单独的curl句柄资源。

curl_multi_close() 关闭一个批处理句柄资源。

curl_multi_getcontent() 在设置了CURLOPT_RETURNTRANSFER的情况下,返回获取的输出的文本流。

curl_multi_info_read() 获取当前解析的curl的相关传输信息。

示例代码:

~~~

// 创建一对cURL资源
$ch1 = curl_init();
$ch2 = curl_init();
// 设置URL和相应的选项
curl_setopt($ch1, CURLOPT_URL, "http://www.example.com/");
curl_setopt($ch1, CURLOPT_HEADER, 0);
curl_setopt($ch2, CURLOPT_URL, "http://www.php.net/");
curl_setopt($ch2, CURLOPT_HEADER, 0);
// 创建批处理cURL句柄
$mh = curl_multi_init();
// 增加2个句柄
curl_multi_add_handle($mh,$ch1);
curl_multi_add_handle($mh,$ch2);
$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_close($mh);
?>

获取批处理后的结果:

~~~

curl普通请求

$startTime = microtime(true);
$chArr = [];
$optArr = [
   CURLOPT_URL => 'http://www.httpbin.org/ip',
   CURLOPT_HEADER => 0,
   CURLOPT_RETURNTRANSFER => 1,
];
$result = [];

//创建多个curl资源并执行
for ($i=0; $i<10; $i++) {
   $chArr[$i] = curl_init();
   curl_setopt_array($chArr[$i], $optArr);
   $result[$i] = curl_exec($chArr[$i]);
   curl_close($chArr[$i]);
}

$endTime = microtime(true);
echo sprintf("use time: %.3f s".PHP_EOL, $endTime - $startTime);
use time: 6.080 s

curl_multi并发请求

$startTime = microtime(true);
$chArr = [];
$optArr = [
   CURLOPT_URL => 'http://www.httpbin.org/ip',
   CURLOPT_HEADER => 0,
   CURLOPT_RETURNTRANSFER => 1,
];
$result = [];

//创建多个curl资源
for ($i=0; $i<10; $i++) {
   $chArr[$i] = curl_init();
   curl_setopt_array($chArr[$i], $optArr);
}
//创建批处理curl句柄
$mh = curl_multi_init();
//将单个curl句柄添加到批处理curl句柄中
foreach ($chArr as $ch) {
   curl_multi_add_handle($mh, $ch);
}
//判断操作是否仍在执行的标识的引用
$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 $i=>$ch) {
   //获取某个curl句柄的返回值
   $result[$i] = curl_multi_getcontent($ch);
   //移除批处理句柄中的某个句柄资源
   curl_multi_remove_handle($mh, $ch);
}
//关闭一组curl句柄
curl_multi_close($mh);
$endTime = microtime(true);
echo sprintf("use time: %.3f s".PHP_EOL, $endTime - $startTime);



后端服务开发中经常会有并发请求的需求,比如你需要获取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请求详解](https://segmentfault.com/a/1190000014922772#articleHeader3),也支持并发请求,其并发原理是`cURL`扩展使用多线程来管理多请求。

## `PHP`并发请求

我们直接来看代码`demo`:

```php
// 简单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` 相关函数

```php
/** 函数作用:返回一个新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 )

/** 函数作用:如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
    @param $ch 由curl_init返回的cURL句柄
    @return string 如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流。
*/
string curl_multi_getcontent ( resource $ch )
```

> 本例中使用到的[预定义常量](http://php.net/manual/zh/curl.constants.php):
> `CURLM_CALL_MULTI_PERFORM: (int) -1`
> `CURLM_OK: (int) 0`

## `PHP`并发请求耗时对比

1. 第一次请求使用上面的`curl_multi_init`方法,并发请求`105`次。
2. 第二次请求使用传统的`foreach`方法,遍历`105`次使用`curl_init`方法请求。

实际的请求耗时结果为:

![img](https://img2018.cnblogs.com/blog/1504257/201811/1504257-20181116203953439-1082205242.png)

刨除`download`的约`765ms`耗时,单纯的请求耗时优化达到了`39.83/1.58`达到了`25`倍,如果继续刨除建连相关的耗时,应该会更高。这其中的耗时:

- 方案1:最慢的一个接口达到了`1.58s`
- 方案2:`105`个接口的平均耗时是`384ms`

> 这个测试的请求是我的环境的内部接口,所以耗时很短,实际爬虫请求环境优化会更明显。

## 注意项

### 并发数限制

`curl_multi`会消耗很多的系统资源,在并发请求时并发数有一定阈值,一般为`512`,是由于`CURL`内部限制,超过最大并发会导致失败。

> 具体的测试结果我没有做,可以参考别人的文章:[每次使用curl multi同时并发多少请求合适](https://blog.csdn.net/loophome/article/details/53266814)

### 超时时间

为了防止慢请求影响整个服务,可以设置`CURLOPT_TIMEOUT`来控制超时时间,防止部分假死的请求无限阻塞进程处理,最后打死机器服务。

### `CPU`负载打满

在代码示例中,如果持续查询并发的执行状态,会导致`cpu`的负载过高,所以,需要在代码里加上`usleep(50000);`的语句。
同时,`curl_multi_select`也可以控制`cpu`占用,在数据有回应前会一直处于等待状态,新数据一来就会被唤醒并继续执行,减少了`CPU`的无谓消耗。

## 参考资料

1. `PHP手册 curl_multi_init`:[http://php.net/manual/zh/func...](http://php.net/manual/zh/function.curl-multi-init.php)
2. `PHP手册 curl预定义常量`:[http://php.net/manual/zh/curl...](http://php.net/manual/zh/curl.constants.php)
3. `PHP中foreach curl实现多线程`:[http://www.111cn.net/phper/ph...](http://www.111cn.net/phper/php/79106.htm)
4. `Doing curl_multi_exec the right way`:[http://www.adrianworlddesign....](http://www.adrianworlddesign.com/Knowledge-Base/php/Download-content-with-cURL/Doing-curlmultiexec-the-right-way)
5. `Segmentfault PHP cURL请求详解`:[https://segmentfault.com/a/11...](https://segmentfault.com/a/1190000014922772#articleHeader7)
6. `CSDN 每次使用curl multi同时并发多少请求合适`:[https://blog.csdn.net/loophom...](https://blog.csdn.net/loophome/article/details/53266814)
7. `简书 Curl多线程及原理`:[https://www.jianshu.com/p/f50...](https://www.jianshu.com/p/f50a3f6f9217)
 
这就是我碰到的第一个问题,百度的网页都获取不了了

(1)利用php的curl抓取网站信息,出现中文乱码的情况:
    $rs = curl_exec($ch);
//关闭cURL资源,并且释放系统资源
        curl_close($ch);
        $rs = mb_convert_encoding($rs, 'utf-8', 'GBK,UTF-8,ASCII'); //加上这行

(2)如果抓取的网页进行了gzip压缩,那么获取的内容很有可能是乱码
解决方案:curl_setopt($ch,CURLOPT_ENCODING,'gzip')//加入gzip解析

(3)如果curl请求的网页发生了重定向,那么抓取的结果很可能为空
解决方案:curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1);//加入重定向处理

二、关于curl_getinfo

curl_getinfo的使用。返回来一个数组类型的值,里面有一个url,有一个http_code,http_code可以是302,200,404,500等,如果是302的话,
就是页面跳转,直接可以得到跳转的页面的url。
如果是想取到具体的值,可以采用:curl_getinfo($ch,CURLINFO_HTTP_CODE),则会返回一个http_code字符串。
参数(20个):
CURLINFO_EFFECTIVE_URL – 最后一个有效的URL地址
CURLINFO_HTTP_CODE – 最后一个收到的HTTP代码
CURLINFO_FILETIME – 远程获取文档的时间,如果无法获取,则返回值为“-1”
CURLINFO_TOTAL_TIME – 最后一次传输所消耗的时间
CURLINFO_NAMELOOKUP_TIME – 名称解析所消耗的时间
CURLINFO_CONNECT_TIME – 建立连接所消耗的时间
CURLINFO_PRETRANSFER_TIME – 从建立连接到准备传输所使用的时间
CURLINFO_STARTTRANSFER_TIME – 从建立连接到传输开始所使用的时间
CURLINFO_REDIRECT_TIME – 在事务传输开始前重定向所使用的时间
CURLINFO_SIZE_UPLOAD – 上传数据量的总值
CURLINFO_SIZE_DOWNLOAD – 下载数据量的总值
CURLINFO_SPEED_DOWNLOAD – 平均下载速度
CURLINFO_SPEED_UPLOAD – 平均上传速度
CURLINFO_HEADER_SIZE – header部分的大小
CURLINFO_HEADER_OUT – 发送请求的字符串
CURLINFO_REQUEST_SIZE – 在HTTP请求中有问题的请求的大小
CURLINFO_SSL_VERIFYRESULT – 通过设置CURLOPT_SSL_VERIFYPEER返回的SSL证书验证请求的结果
CURLINFO_CONTENT_LENGTH_DOWNLOAD – 从Content-Length: field中读取的下载内容长度
CURLINFO_CONTENT_LENGTH_UPLOAD – 上传内容大小的说明
CURLINFO_CONTENT_TYPE – 下载内容的Content-Type:值,NULL表示服务器没有发送有效的Content-Type: header
可以根据需要设置不同的参数。
 


版权说明
文章采用: 《 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权。
版权声明:未标注转载均为本站原创,转载时请以链接形式注明文章出处。如有侵权、不妥之处,请联系站长删除。敬请谅解!

-- 展开阅读全文 --
这篇文章最后更新于 2023年04月15日,已超过 XXX 年没有更新,如果文章内容或图片资源失效,请留言反馈,我们会及时处理,谢谢!
打赏&Support - 请我们来一杯咖啡
« 上一篇

发表评论

个人信息

梦幻书涯
星际导航

热门文章

1
2
3
4