在面试过程中遇到一个问题,加入一个一个网站访问一次需要两秒,我们如何实现在2秒左右请求三次? 面试官想问的就是如何使用curl并发处理请求 一般来说,想到要用curl_multi_init()时,目的是要同时请求多个url,而不是一个一个依次请求,否则就要curl_init()了。 不过,在使用curl_multi的时候,你可能遇到cpu消耗过高、网页假死等现象,可以看看《PHP使用curl_multi_select解决curl_multi网页假死问题》 第一步:调用 第二步:循环调用 这一步需要注意的是, 第三步:持续调用 第四步:根据需要循环调用 第五步:调用 第六步:调用 curl_multi_init() 初始化一个curl批处理句柄资源。 curl_multi_add_handle() 向curl批处理会话中添加单独的curl句柄资源。 curl_multi_exec() 解析一个curl批处理句柄, curl_multi_remove_handle() 移除curl批处理句柄资源中的某个句柄资源, curl_multi_close() 关闭一个批处理句柄资源。 curl_multi_getcontent() 在设置了 curl_multi_info_read() 获取当前解析的curl的相关传输信息。 示例代码: ~~~
关于curl_multi_init()
使用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_add_handle()
函数有两个参数,第一个参数表示一个curl批处理句柄资源,第二个参数表示一个单独的curl句柄资源。curl_multi_exec()
函数有两个参数,第一个参数表示一个批处理句柄资源,第二个参数是一个引用值的参数,表示剩余需要处理的单个的curl句柄资源数量。curl_multi_remove_handle()
函数有两个参数,第一个参数表示一个curl批处理句柄资源,第二个参数表示一个单独的curl句柄资源。CURLOPT_RETURNTRANSFER
的情况下,返回获取的输出的文本流。// 创建一对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);
?>
$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 scurl_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
可以根据需要设置不同的参数。
版权声明:未标注转载均为本站原创,转载时请以链接形式注明文章出处。如有侵权、不妥之处,请联系站长删除。敬请谅解!
@梦幻书涯:正在实现到导航网中