解决 CPU 100% 使用率,更简单更正确的方法
<?php
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
?>
(PHP 5, PHP 7, PHP 8)
curl_multi_exec — 运行当前 cURL 句柄的子连接
处理堆栈中的每个句柄。无论句柄是否需要读取或写入数据,都可以调用此方法。
版本 | 描述 |
---|---|
8.0.0 |
multi_handle 现在期望一个 CurlMultiHandle 实例;以前,期望一个 resource。 |
范例 #1 curl_multi_exec() 范例
此示例将创建两个 cURL 句柄,将它们添加到多句柄中,并异步处理它们。
<?php
// 创建两个 cURL 资源
$ch1 = curl_init();
$ch2 = curl_init();
// 设置 URL 和其他适当的选项
curl_setopt($ch1, CURLOPT_URL, "http://example.com/");
curl_setopt($ch1, CURLOPT_HEADER, 0);
curl_setopt($ch2, CURLOPT_URL, "https://php.net/");
curl_setopt($ch2, CURLOPT_HEADER, 0);
// 创建多个 cURL 句柄
$mh = curl_multi_init();
// 添加两个句柄
curl_multi_add_handle($mh,$ch1);
curl_multi_add_handle($mh,$ch2);
// 执行多句柄
do {
$status = curl_multi_exec($mh, $active);
if ($active) {
// 等待一段时间以获得更多活动
curl_multi_select($mh);
}
} while ($active && $status == CURLM_OK);
// 关闭句柄
curl_multi_remove_handle($mh, $ch1);
curl_multi_remove_handle($mh, $ch2);
curl_multi_close($mh);
?>
解决 CPU 100% 使用率,更简单更正确的方法
<?php
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
?>
您可能还想能够将 HTML 内容下载到缓冲区/变量中,以便解析 HTML 或在您的程序中进行其他处理。
此页面上的示例代码只输出所有内容到屏幕上,而没有给你将下载的页面保存到字符串变量中的可能性。因为我想做的是下载多个页面(并不奇怪,对吧?这就是使用多页并行 Curl 的原因),所以我最初很困惑,因为这个页面没有提供关于如何做到这一点的指南。
幸运的是,有一种方法可以并行 Curl 请求下载内容(就像您对单个下载使用常规 curl_exec 一样)。您需要使用:https://php.net/manual/en/function.curl-multi-getcontent.php
curl_multi_getcontent 函数绝对应该在 curl_multi_exec 的“参见”部分中提及。可能大多数来到 curl_multi_exec 文档页面的人,实际上想要将多个 HTML 页面(或来自多个并行 Curl 连接的其他内容)下载到缓冲区中,每个页面一个缓冲区。
// 所有 url 都记录在数组中
$url[] = 'http://www.link1.com.br';
$url[] = 'https://www.link2.com.br';
$url[] = 'https://www.link3.com.br';
// 为所有 url 设置默认选项并将它们添加到队列中以进行处理
$mh = curl_multi_init();
foreach($url as $key => $value){
$ch[$key] = curl_init($value);
curl_setopt($ch[$key], CURLOPT_NOBODY, true);
curl_setopt($ch[$key], CURLOPT_HEADER, true);
curl_setopt($ch[$key], CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch[$key], CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch[$key], CURLOPT_SSL_VERIFYHOST, false);
curl_multi_add_handle($mh,$ch[$key]);
}
// 执行查询
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
// 获取所有查询的数据并将其从队列中移除
foreach(array_keys($ch) as $key){
echo curl_getinfo($ch[$key], CURLINFO_HTTP_CODE);
echo curl_getinfo($ch[$key], CURLINFO_EFFECTIVE_URL);
echo "\n";
curl_multi_remove_handle($mh, $ch[$key]);
}
// 完成
curl_multi_close($mh);
/!\ 注意
/!\ 此页面上许多未被否定投票的笔记都使用了过时的信息。
CURLM_CALL_MULTI_PERFORM 返回代码自 2012 年左右(至少 7 年前)就已失效。
引用 curl 作者的话,来自 https://curl.haxx.se/mail/lib-2012-08/0042.html:
> CURLM_CALL_MULTI_PERFORM 已弃用,将永远不会返回,如文档所述。
> 在 libcurl 的多接口的前十年左右,我从未见过这个功能的任何正确使用。然而,我看到了许多错误和误解。这让我决定这个功能并不重要或不够好,所以从 7.20.0 开始,CURLM_CALL_MULTI_PERFORM 不复存在了。
通过 https://stackoverflow.com/q/19490837/3229684, 发现了这一切,它建议使用以下替换 while 循环
<?php
do {
$mrc = curl_multi_exec($mc, $active);
} while ($active > 0);
?>
https://www.google.com/search?q=CURLM_CALL_MULTI_PERFORM <-- 可能是我在这里能提供的最具前瞻性的有用链接
仅仅是为了帮助那些难以让它工作的人,这里是我的方法。
没有无限循环,没有 CPU 100%,速度可以调整。
<?php
function curl_multi_exec_full($mh, &$still_running) {
do {
$state = curl_multi_exec($mh, $still_running);
} while ($still_running > 0 && $state === CURLM_CALL_MULTI_PERFORM && curl_multi_select($mh, 0.1));
return $state;
}
function curl_multi_wait($mh, $minTime = 0.001, $maxTime = 1){
$umin = $minTime*1000000;
$start_time = microtime(true);
$num_descriptors = curl_multi_select($mh, $maxTime);
if($num_descriptors === -1){
usleep($umin);
}
$timespan = (microtime(true) - $start_time);
if($timespan < $umin){
usleep($umin - $timespan);
}
}
$handles = [
[
CURLOPT_URL=>"http://example.com/",
CURLOPT_HEADER=>false,
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>false,
],
[
CURLOPT_URL=>"https://php.net",
CURLOPT_HEADER=>false,
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>false,
// https://stackoverflow.com/a/41135574
CURLOPT_HEADERFUNCTION=>function($ch, $header)
{
print "header from https://php.net: ".$header;
return strlen($header);
}
]
];
$mh = curl_multi_init();
$chandles = [];
foreach($handles as $opts) {
$ch = curl_init();
curl_setopt_array($ch, $opts);
curl_multi_add_handle($mh, $ch);
$chandles[] = $ch;
}
$prevRunning = null;
do {
$status = curl_multi_exec_full($mh, $running);
if($running < $prevRunning){
while ($read = curl_multi_info_read($mh, $msgs_in_queue)) {
$info = curl_getinfo($read['handle']);
if($read['result'] !== CURLE_OK){
print "Error: ".$info['url'].PHP_EOL;
}
if($read['result'] === CURLE_OK){
/*
if(isset($info['redirect_url']) && trim($info['redirect_url'])!==''){
print "running redirect: ".$info['redirect_url'].PHP_EOL;
$ch3 = curl_init();
curl_setopt($ch3, CURLOPT_URL, $info['redirect_url']);
curl_setopt($ch3, CURLOPT_HEADER, 0);
curl_setopt($ch3, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch3, CURLOPT_FOLLOWLOCATION, 0);
curl_multi_add_handle($mh,$ch3);
}
*/
print_r($info);
//echo curl_multi_getcontent($read['handle']));
}
}
}
if ($running > 0) {
curl_multi_wait($mh);
}
$prevRunning = $running;
} while ($running > 0 && $status == CURLM_OK);
foreach($chandles as $ch){
curl_multi_remove_handle($mh, $ch);
}
curl_multi_close($mh);
?>
一个例子,说明如何使用 Fibers 使 multi_curl 的速度提高一倍(伪代码)
<?php
$curlHandles = [];
$urls = [
'https://example.com/1',
'https://example.com/2',
...
'https://example.com/1000',
];
$mh = curl_multi_init();
$mh_fiber = curl_multi_init();
$halfOfList = floor(count($urls) / 2);
foreach ($urls as $index => $url) {
$ch = curl_init($url);
$curlHandles[] = $ch;
// 一半的 URL 将在 Fiber 中的后台运行
$index > $halfOfList ? curl_multi_add_handle($mh_fiber, $ch) : curl_multi_add_handle($mh, $ch);
}
$fiber = new Fiber(function (CurlMultiHandle $mh) {
$still_running = null;
do {
curl_multi_exec($mh, $still_running);
Fiber::suspend();
} while ($still_running);
});
// 在 Fiber 处于挂起状态时,在后台运行 curl multi exec
$fiber->start($mh_fiber);
$still_running = null;
do {
$status = curl_multi_exec($mh, $still_running);
} while ($still_running);
do {
/**
* 此时 Fiber 中的 curl 可能已经完成了
* 因此我们需要在 Fiber 中使用另一个 "do while" 循环来刷新 $still_running 变量
**/
$status_fiber = $fiber->resume();
} while (!$fiber->isTerminated());
foreach ($curlHandles as $index => $ch) {
$index > $halfOfList ? curl_multi_remove_handle($mh_fiber, $ch) : curl_multi_remove_handle($mh, $ch);
}
curl_multi_close($mh);
curl_multi_close($mh_fiber);
?>
http://curl.haxx.se/libcurl/c/libcurl-multi.html
"当你添加了当前需要的句柄(你可以在任何时候添加新的句柄)之后,你可以通过调用 curl_multi_perform(3) 来开始传输。
curl_multi_perform(3) 是异步的。它只执行尽可能少的操作,然后将控制权返回给你的程序。它被设计为永不阻塞。如果它返回 CURLM_CALL_MULTI_PERFORM,你最好尽快再次调用它,因为这表示它仍然有本地数据要发送或远程数据要接收."
所以,示例脚本中的循环看起来应该是这样的
<?php
$running=null;
// 执行句柄
do {
while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $running));
if (!$running) break;
while (($res = curl_multi_select($mh)) === 0) {};
if ($res === false) {
echo "<h1>select error</h1>";
break;
}
} while (true);
?>
这个在没有运行非阻塞循环,浪费CPU时间的情况下运行良好 (PHP 5.2.5 @ FBSD 6.2)。
但是这似乎是 curl_multi_select 的唯一用途,因为没有简单的方法将其与其他 PHP select 系统调用的包装器绑定。
我尝试了 Daniel G Zylberberg 的函数,
但它没有按发布的方式工作。
我做了一些修改让它工作,这是我使用的代码:
function multiCurl($res, $options=""){
if(count($res)<=0) return False;
$handles = array();
if(!$options) // 添加默认选项
$options = array(
CURLOPT_HEADER=>0,
CURLOPT_RETURNTRANSFER=>1,
);
// 将curl选项添加到每个句柄
foreach($res as $k=>$row){
$ch{$k} = curl_init();
$options[CURLOPT_URL] = $row['url'];
$opt = curl_setopt_array($ch{$k}, $options);
var_dump($opt);
$handles[$k] = $ch{$k};
}
$mh = curl_multi_init();
// 添加句柄
foreach($handles as $k => $handle){
$err = curl_multi_add_handle($mh, $handle);
}
$running_handles = null;
do {
curl_multi_exec($mh, $running_handles);
curl_multi_select($mh);
} while ($running_handles > 0);
foreach($res as $k=>$row){
$res[$k]['error'] = curl_error($handles[$k]);
if(!empty($res[$k]['error']))
$res[$k]['data'] = '';
else
$res[$k]['data'] = curl_multi_getcontent( $handles[$k] ); // 获取结果
// 关闭当前句柄
curl_multi_remove_handle($mh, $handles[$k] );
}
curl_multi_close($mh);
return $res; // 返回响应
}
在 OS X Mavericks 系统上运行 curl_multi_exec() 时,您可能无法获得正确的结果,
脚本可能一直运行但没有返回值,
这是因为在 OS X 系统上,PHP 版本 5.3.8+,在调用 curl_multi_exec() 方法之前调用 curl_multi_select(),
可能会一直返回 -1。
<?php
$ch1 = curl_init();
$ch2 = curl_init();
// 设置URL和相应的选项
curl_setopt($ch1, CURLOPT_URL, "http://lxr.php.net");
curl_setopt($ch1, CURLOPT_HEADER, 0);
curl_setopt($ch2, CURLOPT_URL, "https://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);
$active = null;
do {
$mrc = curl_multi_exec($mh, $active);
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
while ($active && $mrc == CURLM_OK)
{
// 添加这一行
while (curl_multi_exec($mh, $active) === CURLM_CALL_MULTI_PERFORM);
if (curl_multi_select($mh) != -1)
{
do {
$mrc = curl_multi_exec($mh, $active);
if ($mrc == CURLM_OK)
{
while($info = curl_multi_info_read($mh))
{
var_dump($info);
}
}
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
}
}
// 关闭句柄
curl_multi_remove_handle($mh, $ch1);
curl_multi_remove_handle($mh, $ch2);
curl_multi_close($mh);
?>
qdujunjie at 126 dot com ¶
当在 OS X Mavericks 中运行 curl_multi_exec() 时,
可能会得不到想要的结果,
因为脚本会一直执行但是却没有返回值,
这是因为在 OS X 中当 PHP 版本在 5.3.8 以上版本时,
在调用 curl_multi_exec() 方法之前调用 curl_multi_select() 会一直返回 -1,
可能会一直返回 -1。
只是 PHP 不会显示它。
我(也许错误地)认为这是一个错误,所以我创建了一个错误报告,请参阅它以了解我用来重现这种奇怪行为的代码示例:
https://bugs.php.net/bug.php?id=62409
Jimmy Ruska ¶
15 年前
> while (($res = curl_multi_select($mh)) === 0) {};
这在我的 Windows 计算机 (php 5.2.5) 上运行良好,但当我将 curl 程序运行在我的新的 CentOS 服务器 (php 5.1.6) 上时,
该函数除非添加 curl_multi_exec() 到循环中才更新,而这消除了使用它来节省周期的目的。curl_multi_select() 也可以设置超时时间,
但这似乎没有帮助,再说一遍,我不明白为什么人们不使用它。
do {
即使 curl_multi_select($mh) 工作,也没有办法知道哪些套接字更新了状态,或者它们是否收到了部分数据,
完全完成,还是仅仅超时。如果你想在事情完成时立即删除/添加数据,它不可靠。尝试只调用 example.com 或其他数据量很少的网站。
curl_multi_select($mh) 可能在完成之前发送数千次非 0 值。惰性地添加 usleep(25000) 或一些最小值也可以帮助不浪费周期。
evaisse at gmail dot com ¶
6 年前
}
此示例在 PHP5.6 上并不完全有效。
使用 curl_multi 和 CURLOPT_VERBOSE & CURLOPT_STDERR,我发现如果在任何“经典”cURL 请求之前发送 curl_multi 请求,
// 检查错误
if ($mrc > 0) {
<?php
// 作者:Daniel G Zylberberg
// 日期:2012 年 7 月 11 日
// $res:数组结构 0=>array("url"=>"blah"),1=>array("url"=>"some url")
// $options(可选):包含 curl 选项(超时、postfields 等)的数组
// 返回与获取的数组相同,并在当前行(html 内容)中添加 "data"
// 以及 "error",如果失败,则包含字符串描述。
function multiCurl($res,$options=""){
if(count($res)<=0) return False;
$handles = array();
if(!$options) // 添加默认选项
$options = array(
CURLOPT_HEADER=>0,
CURLOPT_RETURNTRANSFER=>1,
);
// 将 curl 选项添加到每个句柄
foreach($res as $k=>$row){
$ch{$k} = curl_init();
$options[CURLOPT_URL] = $row['url'];
curl_setopt_array($ch{$k}, $options);
$handles[$k] = $ch{$k};
}
$mh = curl_multi_init();
foreach($handles as $k => $handle){
curl_multi_add_handle($mh,$handle);
//echo "<br>adding handle {$k}";
}
$running_handles = null;
// 执行句柄
do {
$status_cme = curl_multi_exec($mh, $running_handles);
} while ($cme == CURLM_CALL_MULTI_PERFORM);
while ($running_handles && $status_cme == CURLM_OK) {
if (curl_multi_select($mh) != -1) {
do {
$status_cme = curl_multi_exec($mh, $running_handles);
// echo "<br>''threads'' running = {$running_handles}";
} while ($status == CURLM_CALL_MULTI_PERFORM);
}
}
foreach($res as $k=>$row){
$res[$k]['error'] = curl_error($handles[$k]);
if(!empty($res[$k]['error']))
$res[$k]['data'] = '';
else
$res[$k]['data'] = curl_multi_getcontent( $handles[$k] ); // 获取结果
// 关闭当前句柄
curl_multi_remove_handle($mh, $handles[$k] );
}
curl_multi_close($mh);
return $res; // 返回响应
}
$res = array(
"11"=>array("url"=>"http://localhost/public_html/test/sleep.php?t=1"),
"13"=>array("url"=>"http://localhost/public_html/test/sleep.php?t=3"),
"25"=>array("url"=>"this doesn't exist"),
);
print_r( multiCurl($res));
?>
---------- sleep.php -------------------------------------
<?php
sleep($_GET['t']);
echo "sleep for {$_GET['t']} seconds and show this.";
?>
我在测试 PHP 手册中 curl_multi_exec 部分提供的由 [email protected] 提供的 PHP 代码。
代码中的 “$err = curl_error($conn[$i])” 部分应该返回每个 cURL 会话的错误消息,但它没有。
curl_error() 函数与 curl_exec() 一起使用时效果很好。有没有其他解决方案可以获取使用 curl_multi_exec() 的会话错误消息,或者 cURL 库中存在错误?
该脚本已在 Windows XP 和 PHP-5.2.4 上测试。