解決 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 實例;以前預期是一個 資源。 |
範例 #1 curl_multi_exec() 範例
這個範例將為 URL 列表建立 curl 控制代碼,將它們添加到多重控制代碼,並非同步處理它們。
<?php
$urls = [
"https://php.dev.org.tw/",
"https://www.example.com/",
];
$mh = curl_multi_init();
$map = new WeakMap();
foreach ($urls as $url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_multi_add_handle($mh, $ch);
$map[$ch] = $url;
}
do {
$status = curl_multi_exec($mh, $unfinishedHandles);
if ($status !== CURLM_OK) {
throw new \Exception(curl_multi_strerror(curl_multi_errno($mh)));
}
while (($info = curl_multi_info_read($mh)) !== false) {
if ($info['msg'] === CURLMSG_DONE) {
$handle = $info['handle'];
curl_multi_remove_handle($mh, $handle);
$url = $map[$handle];
if ($info['result'] === CURLE_OK) {
$statusCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
echo "Request to {$url} finished with HTTP status {$statusCode}:", PHP_EOL;
echo curl_multi_getcontent($handle);
echo PHP_EOL, PHP_EOL;
} else {
echo "Request to {$url} failed with error: ", PHP_EOL;
echo curl_strerror($info['result']);
echo PHP_EOL, PHP_EOL;
}
}
}
if ($unfinishedHandles) {
if (($updatedHandles = curl_multi_select($mh)) === -1) {
throw new \Exception(curl_multi_strerror(curl_multi_errno($mh)));
}
}
} while ($unfinishedHandles);
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.dev.org.tw/manual/en/function.curl-multi-getcontent.php
函數 curl_multi_getcontent 絕對應該在 curl_multi_exec 的「另請參閱」部分中提及。可能大多數找到 curl_multi_exec 文件頁面的人,實際上是想將多個 HTML 頁面(或來自多個平行 Curl 連線的其他內容)下載到緩衝區中,每個頁面一個緩衝區。
// 所有網址記錄在陣列中
$url[] = 'http://www.link1.com.br';
$url[] = 'https://www.link2.com.br';
$url[] = 'https://www.link3.com.br';
// 設定所有網址的預設選項並加入佇列以進行處理
$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 年以來就已經失效,至少七年前就已經失效了。
引用 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.dev.org.tw",
CURLOPT_HEADER=>false,
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>false,
// https://stackoverflow.com/a/41135574
CURLOPT_HEADERFUNCTION=>function($ch, $header)
{
print "header from https://php.dev.org.tw: ".$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;
// half of urls will be run in background in 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);
});
// run curl multi exec in background while fiber is in suspend status
$fiber->start($mh_fiber);
$still_running = null;
do {
$status = curl_multi_exec($mh, $still_running);
} while ($still_running);
do {
/**
* at this moment curl in fiber already finished (maybe)
* so we must refresh $still_running variable with one more cycle "do while" in fiber
**/
$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;
//執行 handles
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>選擇錯誤</h1>";
break;
}
} while (true);
?>
這在 (PHP 5.2.5 @ FBSD 6.2) 上運作良好,無須執行非阻塞迴圈且不會浪費 CPU 時間。
然而,這似乎是 curl_multi_select 的唯一用途,因為沒有簡單的方法將它與其他用於 select 系統呼叫的 PHP 包裝器綁定。
我嘗試了 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 選項加入每個 handle
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();
// 加入 handles
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] ); // 取得結果
// 關閉目前的 handle
curl_multi_remove_handle($mh, $handles[$k] );
}
curl_multi_close($mh);
return $res; // 回傳回應
}
這個範例在 PHP5.6 上無法完全正常運作。
使用 curl_multi 和 CURLOPT_VERBOSE 以及 CURLOPT_STDERR,我發現如果在任何「傳統」cURL 請求之前發送 curl_multi 請求,將會導致針對某些主機名稱發生逾時錯誤。錯誤訊息顯示:
> * 在 DNS 快取中找不到主機名稱
將這行程式碼加入第一個迴圈似乎可以解決這個問題。
do {
$mrc = curl_multi_exec($mh, $active);
// 檢查錯誤
if ($mrc > 0) {
// 顯示錯誤
error_log(curl_multi_strerror($mrc));
}
} while ($mrc === CURLM_CALL_MULTI_PERFORM || $active);
來源:https://stackoverflow.com/questions/30935541/php-curl-error-hostname-was-not-found-in-dns-cache