傳統 curl_exec 是同步阻塞的:逐個請求、等待回應、再發下一個。若需抓取 100 個 API、每個 200ms,串行需 20 秒。curl_multi_* 系列函數實現單進程非阻塞並發,可在數秒內完成同等工作量,大幅提升爬蟲、聚合 API、批次推播等場景效率。
curl_multi_init():建立多執行緒控制代碼curl_init() → curl_setopt() → curl_multi_add_handle():加入子請求curl_multi_exec() 迴圈執行:非阻塞驅動所有請求curl_multi_getcontent() 取得各請求回應curl_multi_remove_handle() + curl_close() 清理子控制代碼curl_multi_close() 釋放主控制代碼<?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, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // 單請求超時
$handles[$i] = $ch;
curl_multi_add_handle($mh, $ch);
}
// ❌ 糟糕寫法:忙等待導致 CPU 100%
do {
$mrc = curl_multi_exec($mh, $active);
} while ($active);
$results = [];
foreach ($handles as $i => $ch) {
$results[$i] = curl_multi_getcontent($ch);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
print_r($results);
<?php
$mh = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; MyBot/1.0)');
$handles[$i] = $ch;
curl_multi_add_handle($mh, $ch);
}
$active = null;
// 執行主迴圈
do {
// 1. 盡可能執行所有可立即處理的動作
$mrc = curl_multi_exec($mh, $active);
while ($mrc === CURLM_CALL_MULTI_PERFORM) {
$mrc = curl_multi_exec($mh, $active);
}
// 2. 若仍有進行中請求,等待活動發生(阻塞不佔 CPU)
if ($active && $mrc === CURLM_OK) {
if (curl_multi_select($mh) === -1) {
// select 失敗或無檔案描述符,短暫睡眠避免忙等
usleep(10000); // 10ms
}
}
} while ($active && $mrc === CURLM_OK);
// 收集結果
$results = [];
foreach ($handles as $i => $ch) {
$errno = curl_errno($ch);
$error = curl_error($ch);
$results[$i] = [
'content' => $errno ? null : curl_multi_getcontent($ch),
'error' => $errno ? "cURL error ($errno): $error" : null,
'info' => curl_getinfo($ch),
];
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
print_r($results);
| 優化點 | 說明 | 設定範例 |
|---|---|---|
| 連線超時 | 避免卡在 DNS/TCP 握手 | CURLOPT_CONNECTTIMEOUT => 5 |
| 總超時 | 單請求最長執行時間 | CURLOPT_TIMEOUT => 30 |
| User-Agent | 避免被反爬蟲封禁 | CURLOPT_USERAGENT => '...' |
| Follow Location | 自動處理 3xx 重導向 | CURLOPT_FOLLOWLOCATION => true |
| SSL 驗證 | 生產環境務必驗證 | CURLOPT_SSL_VERIFYPEER => true |
| 限制併發數 | 避免耗盡檔案描述符/記憶體 | 分批處理,每批 50-100 個 |
| 錯誤處理 | 區分網路錯誤、HTTP 錯誤、超時 | curl_errno()、curl_getinfo()['http_code'] |
作者實測對 Memcached 讀寫介面進行併發模擬:
結論:PHP cURL Multi 適合中等併發(< 500),超大規模建議使用 Swoole、Workerman、Go/Rust 等原生非阻塞框架,或引入訊息佇列(RabbitMQ/Kafka)削峰填谷。
cURL Multi 是 PHP 單進程併發網路請求的基石。掌握 curl_multi_select 事件驅動寫法、合理設定超時與併發上限、完善錯誤處理,即可構建高效穩定的批次請求系統。隨著業務增長,適時遷移至協程框架或異步架構,才是長久之計。