需同時請求多個 URL 時,curl_multi_* 族函數是 PHP 並發 HTTP 的原生利器。但官方文檔極簡、示例有**致命死循環缺陷**(do { curl_multi_exec() } while ($active) 導致 CPU 100%)。本文提供**正確事件驅動寫法**、超時控制、錯誤處理完整範本。
curl_multi_init() 建立多執行緒句柄。curl_init($url) 建立子句柄,設定選項,curl_multi_add_handle() 加入。curl_multi_exec() + curl_multi_select() 等待活動。curl_multi_getcontent() 取得結果。curl_multi_remove_handle() + curl_close() 清理子句柄。curl_multi_close() 關閉主句柄。do { $n = curl_multi_exec($mh, $active); } while ($active); // 死循環佔滿 CPU!
<?php
$urls = [
'https://www.cnn.com/',
'https://www.canada.com/',
'https://www.yahoo.com/',
];
$mh = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // 單請求總超時
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 連線超時
curl_multi_add_handle($mh, $ch);
$handles[$i] = $ch;
}
// 事件驅動主循環
$active = null;
do {
// 1. 執行所有可立即完成的動作(連線、發送、接收)
$mrc = curl_multi_exec($mh, $active);
// 2. 若仍有進行中請求,等待活動(關鍵:避免 CPU 空轉)
if ($active) {
// curl_multi_select 返回 -1 表示無文件描述符就緒(需短暫 sleep)
// 返回 >=0 表示有描述符就緒,可繼續執行
if (curl_multi_select($mh) === -1) {
usleep(100); // 短暫讓出 CPU,避免 select 過快返回 -1 導致忙等
}
}
} while ($active && $mrc === CURLM_OK);
// 檢查執行錯誤
if ($mrc !== CURLM_OK) {
trigger_error("curl_multi_exec 失誤: " . curl_multi_strerror($mrc), E_USER_WARNING);
}
// 3. 取得結果
$results = [];
foreach ($handles as $i => $ch) {
$errno = curl_errno($ch);
if ($errno) {
$results[$i] = ['error' => curl_error($ch), 'errno' => $errno];
} else {
$results[$i] = [
'content' => curl_multi_getcontent($ch),
'info' => curl_getinfo($ch), // HTTP 碼、耗時等
];
}
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
print_r($results);
?>
| 細節 | 說明 |
|---|---|
curl_multi_select() |
封裝 select() 系統呼叫,等待任一 Socket 可讀/可寫/異常。**返回 -1 非錯誤**,表示無 fd 就緒(可能正在連線中),需 usleep 避免忙等。 |
CURLM_CALL_MULTI_PERFORM |
curl_multi_exec 返回此碼表示「還有立即可做的事」,需**內層循環**持續呼叫直到非此碼。 |
| 單請求超時 | CURLOPT_TIMEOUT(總超時)、CURLOPT_CONNECTTIMEOUT(連線超時)必須設定,防止殭屍連線拖死整批。 |
| 錯誤檢查 | 取內容前必檢 curl_errno($ch),區分網路錯誤、HTTP 4xx/5xx、SSL 驗證失敗等。 |
| 版本相容性坑 | 某些 PHP/cURL 版本組合存在 Bug(如 CURLOPT_USERAGENT 某值導致 Header 變二進位),**上線前必在目標環境實測**。 |
原生 curl_multi 仍屬阻塞式事件循環,單進程併發上限受限於 select() fd 數量與 CPU。高併發(>500 並發)場景建議:
Swoole\Coroutine\Http\Client 協程化,寫同步代碼跑異步 I/O,萬級併發輕鬆支撐。react/http-client,無擴展依賴。amphp/http-client 實現協程風格異步。掌握正確的 curl_multi_select 事件循環,是 PHP 並發 HTTP 從「能跑」到「穩跑」的關鍵一步。