使用 curl_multi_perform 並發請求多個**長連接/訂閱型** URL (如 WebSocket、Server-Sent Events、長輪詢) 時,常規「跑完再解析」模式會陷入死循環,因為 easy handle 永遠不會完成 (CURLMSG_DONE 不會產生)。本文分析問題根因,提供三種解決思路並指出最佳實踐方向。
while (running_handles) {
curl_multi_select(multi, &timeout);
while (CURLM_CALL_MULTI_PERFORM == curl_multi_perform(multi, &running_handles));
// 此處永遠執行不到,因為 running_handles 永遠 > 0
}
curl_multi_info_read(...); // 無法走到這裡解析數據
訂閱型連接**永不主動關閉**,服務器持續推送數據 (甚至心跳幀),導致 easy handle 始終處於「執行中」狀態。
while (running_handles) {
curl_multi_select(...);
curl_multi_perform(...);
// 嘗試讀取完成信息
while ((msg = curl_multi_info_read(multi, &left))) {
if (msg->msg == CURLMSG_DONE) { /* 處理 */ }
}
}
結果:無效。訂閱型 handle 永不產生 CURLMSG_DONE,循環體永不執行。
CURLOPT_TIMEOUT_MS:整體超時,**連接會被強制斷開**,不適合長連接;CURLOPT_RANGE:斷點續傳,收滿指定字節後斷開,**後續數據丟失**;while (running_handles) {
curl_multi_select(...);
curl_multi_perform(...);
// 主動掃描所有 handle 的接收緩衝區
for (int i = 0; i < MAX_HANDLES; i++) {
if (recv_buf[i][0] != '\0') { // 有新數據
process_data(recv_buf[i]);
memset(recv_buf[i], 0, sizeof(recv_buf[i]));
}
}
}
缺點:
libcurl 設計初衷是**回調驅動**而非輪詢。正確模式:
CURLOPT_WRITEFUNCTION 寫入回調;curl_multi_select + curl_multi_perform 驅動事件輪詢;curl_multi_remove_handle,再 curl_easy_cleanup。size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
size_t total = size * nmemb;
// userdata 可傳入上下文結構體 (協議解析器、業務隊列等)
parse_and_dispatch(ptr, total, userdata);
return total; // 必須返回 total,否則 libcurl 認為出錯並中斷
}
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(easy, CURLOPT_WRITEDATA, &context); // 傳遞上下文
| 方案 | 可行性 | 優點 | 缺點 |
|---|---|---|---|
| 移入 curl_multi_info_read | ❌ | - | 訂閱型無 DONE 信息 |
| 超時/範圍選項 | ❌ | - | 破壞長連接語義 |
| 輪詢緩衝區 | ⚠️ 可行 | 簡單直觀 | 效率低、狀態感知弱 |
| 寫入回調 (推薦) | ✅ 最佳 | 即時處理、零拷貝、狀態可控 | 需重構為回調思維 |
**核心心法**:libcurl 多介面是**事件驅動**框架,**不要試圖同步輪詢**,擁抱回調,在 WRITEFUNCTION 中完成流式數據的幀解析與業務分發,才是處理持續數據流的正確姿勢。