單執行緒循環呼叫 curl_exec 會依序等待每個請求完成,總耗時 = Σ(單請求耗時)。curl_multi 允許並發發送多個請求,總耗時 ≈ max(單請求耗時),大幅提升爬蟲/聚合 API 效率。
curl_multi_init() 建立批處理 handle($mh)curl_init() → 設定選項 → curl_multi_add_handle($mh, $ch)curl_multi_exec($mh, $active) 驅動傳輸curl_multi_select($mh) 等待網路事件(避免 CPU 100%)$active === 0 表示全部完成,逐個 curl_multi_getcontent($ch) 取得結果curl_multi_remove_handle() → curl_close() → curl_multi_close() 清理資源官方文件範例在 do-while 迴圈中未呼叫 curl_multi_select,導致忙等待佔滿 CPU。正確寫法:
do {
$mrc = curl_multi_exec($mh, $active);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
while ($active && $mrc === CURLM_OK) {
if (curl_multi_select($mh) !== -1) { // 有網路事件才繼續
do {
$mrc = curl_multi_exec($mh, $active);
} while ($mrc === CURLM_CALL_MULTI_PERFORM);
}
}
class MultiCurl {
private $mh;
private $handles = [];
private $maxConcurrency = 10;
private $callbacks = [];
public function __construct($maxConcurrency = 10) {
$this->maxConcurrency = $maxConcurrency;
$this->mh = curl_multi_init();
}
public function addTask($url, callable $callback, array $options = []) {
$this->callbacks[] = ['url' => $url, 'cb' => $callback, 'opt' => $options];
}
public function run() {
$pending = $this->callbacks;
$running = [];
while ($pending || $running) {
// 補滿併發槽
while (count($running) < $this->maxConcurrency && $pending) {
$task = array_shift($pending);
$ch = curl_init($task['url']);
$defaults = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => false,
];
curl_setopt_array($ch, $defaults + $task['opt']);
curl_multi_add_handle($this->mh, $ch);
$running[(int)$ch] = ['ch' => $ch, 'cb' => $task['cb']];
}
// 驅動傳輸
do { $mrc = curl_multi_exec($this->mh, $active); } while ($mrc === CURLM_CALL_MULTI_PERFORM);
// 等待事件
if ($active) curl_multi_select($this->mh);
// 檢查完成的 handle
while ($info = curl_multi_info_read($this->mh)) {
$ch = $info['handle'];
$task = $running[(int)$ch];
unset($running[(int)$ch]);
$content = curl_multi_getcontent($ch);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// 執行回調
try {
($task['cb'])($content, $httpCode, $error, $task['ch']);
} catch (Throwable $e) {
error_log("Callback error: " . $e->getMessage());
}
curl_multi_remove_handle($this->mh, $ch);
curl_close($ch);
}
}
curl_multi_close($this->mh);
}
}
// 使用範例
$crawler = new MultiCurl(20);
$urls = ['https://api.example.com/1', 'https://api.example.com/2', /* ... */];
foreach ($urls as $url) {
$crawler->addTask($url, function($content, $code, $err, $ch) {
if ($code === 200) {
$data = json_decode($content, true);
// 處理資料、存資料庫、發佈隊列...
}
});
}
$crawler->run();
| 參數 | 建議值 | 說明 |
|---|---|---|
| CURLOPT_TIMEOUT | 10-30 | 單請求最長等待秒數 |
| CURLOPT_CONNECTTIMEOUT | 5 | 連線建立逾時 |
| CURLOPT_TCP_KEEPALIVE | 1 | 保持長連線 |
| CURLOPT_FORBID_REUSE | 0 | 允許連線重用(HTTP/1.1 Keep-Alive) |
| CURLOPT_MAXCONNECTS | 等於併發數 | 連線快取池大小 |
CURLOPT_DNS_CACHE_TIMEOUT 或使用本地 DNS 快取。curl_multi_remove_handle 並 curl_close,避免 handle 累積。CURLOPT_SSL_SESSIONID_CACHE。$httpCode >= 500 || $error,重新 addTask(需限制重試次數)。curl_multi 配合 curl_multi_select 事件驅動模型,是 PHP 高併發 HTTP 客戶端的核心。封裝為可控制併發、支援回調、自動重試的類別,可構建穩健的爬蟲、API 聚合、微服務閘道等系統。