使用curl_multi_*实现C高并发异步API调用及报错排查
问题根因分析
你找到的参考代码存在5个核心逻辑缺陷,直接导致内存崩溃、空指针访问和响应内容异常:
- 所有并发请求共享同一个全局
data_t结构体,write_cb回调会在curl_multi_perform调度时被不同请求交错触发,多个请求同时修改response_counter、写入sub_match_json数组,直接造成内存踩踏、计数器计数错乱,这是curl_easy_cleanup处触发堆内存校验错误的核心原因。 - 响应写入逻辑完全错误:
write_cb是分块多次触发的(单次只返回当前接收到的部分数据,不是完整响应),原代码每次回调就新申请一块内存存储当前块、计数器+1,一个请求的响应会被拆成多块占用多个数组位置,最终计数器值远大于实际请求数,访问数组直接越界触发野指针/空指针异常。 - 开启了
CURLOPT_HEADER=1配置,会把HTTP响应头和响应体拼接后传入写回调,如果你预期存储JSON格式的响应体,自然会混入非业务字节数据,看起来像乱码。 - 末尾遍历响应的循环存在越界:
response_counter-- >=0会多访问一次下标为-1的数组位置,直接触发内存访问异常。 - 函数调用不匹配:
init_data定义只接收1个参数,调用时传入了2个参数(额外传了NULL),编译阶段就会产生栈内存破坏隐患;同时新请求添加逻辑放在curl_multi_info_read循环内,没有请求完成时待发请求不会被加入调度,并发数控制完全失效。
可直接复用的100并发curl_multi入门实现
不需要一开始就啃curl_multi_socket+epoll的复杂整合逻辑,curl_multi_perform+curl_multi_wait的组合足够支撑100级别的并发请求,代码量小、逻辑透明,不需要提前声明固定数量的easy handle变量,支持任意数量的请求接入,每个请求独立存储响应,无共享内存竞争问题:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <curl/curl.h> // 最大并发连接数,按需求设为100 #define MAX_PARALLEL 100 // 单个请求的独立上下文,每个请求单独分配,无多请求共享 typedef struct { char *url; char *response_buf; // 动态拼接完整响应 size_t response_len; } RequestCtx; // 全局调度上下文 typedef struct { CURLM *multi_handle; int total_requests; int added_requests; int finished_requests; RequestCtx **request_list; // 存储所有请求上下文,方便后续统一处理 } GlobalCtx; // 正确的写回调:支持分块接收自动扩容拼接,一个请求对应一块连续的完整响应内存 static size_t write_cb(void *chunk, size_t size, size_t nmemb, void *userp) { size_t chunk_size = size * nmemb; RequestCtx *ctx = (RequestCtx *)userp; char *new_buf = realloc(ctx->response_buf, ctx->response_len + chunk_size + 1); if (!new_buf) { fprintf(stderr, "memory alloc failed: %s\n", strerror(errno)); return 0; } ctx->response_buf = new_buf; memcpy(ctx->response_buf + ctx->response_len, chunk, chunk_size); ctx->response_len += chunk_size; ctx->response_buf[ctx->response_len] = '\0'; // 保证是合法C字符串 return chunk_size; } // 初始化单个请求并加入多句柄调度栈 static int add_request(GlobalCtx *g, const char *url) { CURL *easy = curl_easy_init(); if (!easy) return -1; RequestCtx *ctx = calloc(1, sizeof(RequestCtx)); if (!ctx) { curl_easy_cleanup(easy); return -1; } ctx->url = strdup(url); ctx->response_buf = NULL; ctx->response_len = 0; g->request_list[g->added_requests] = ctx; // 基础请求配置 curl_easy_setopt(easy, CURLOPT_URL, url); curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(easy, CURLOPT_WRITEDATA, ctx); // 绑定当前请求的独立上下文 curl_easy_setopt(easy, CURLOPT_TCP_NODELAY, 1L); curl_easy_setopt(easy, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT, 5L); // 需要存储纯响应体就注释掉下面这行,开启会把响应头混入内容导致乱码 // curl_easy_setopt(easy, CURLOPT_HEADER, 1L); curl_easy_setopt(easy, CURLOPT_PRIVATE, ctx); // 上下文绑定到easy handle,完成时直接读取 curl_multi_add_handle(g->multi_handle, easy); g->added_requests++; return 0; } int main() { // 替换为你自己的URL列表,支持任意长度,不需要提前固定数量 const char *urls[] = { "https://example.com/api/1", "https://example.com/api/2", // 追加你的业务请求地址即可 }; int url_count = sizeof(urls)/sizeof(urls[0]); curl_global_init(CURL_GLOBAL_ALL); GlobalCtx g = {0}; g.total_requests = url_count; g.request_list = calloc(url_count, sizeof(RequestCtx *)); g.multi_handle = curl_multi_init(); // 配置最大并发连接数 curl_multi_setopt(g.multi_handle, CURLMOPT_MAXCONNECTS, (long)MAX_PARALLEL); curl_multi_setopt(g.multi_handle, CURLMOPT_MAX_TOTAL_CONNECTIONS, (long)MAX_PARALLEL); int still_running = 0; // 先填充第一批请求,数量不超过最大并发值 while (g.added_requests < MAX_PARALLEL && g.added_requests < g.total_requests) { add_request(&g, urls[g.added_requests]); } // 事件调度循环 do { CURLMcode mc = curl_multi_perform(g.multi_handle, &still_running); if (mc != CURLM_OK) { fprintf(stderr, "multi perform error: %s\n", curl_multi_strerror(mc)); break; } // 处理所有已完成的请求 int msg_left = 0; CURLMsg *msg; while ((msg = curl_multi_info_read(g.multi_handle, &msg_left))) { if (msg->msg == CURLMSG_DONE) { CURL *easy = msg->easy_handle; RequestCtx *ctx = NULL; curl_easy_getinfo(easy, CURLINFO_PRIVATE, &ctx); fprintf(stderr, "request %s done, ret: %s, response len: %zu\n", ctx->url, curl_easy_strerror(msg->data.result), ctx->response_len); // 清理已完成的句柄 curl_multi_remove_handle(g.multi_handle, easy); curl_easy_cleanup(easy); g.finished_requests++; // 完成一个补一个,维持并发窗口大小 if (g.added_requests < g.total_requests) { add_request(&g, urls[g.added_requests]); still_running = 1; } } } if (!still_running) break; // 等待IO事件,最长阻塞1秒,不需要自行整合epoll mc = curl_multi_wait(g.multi_handle, NULL, 0, 1000, NULL); if (mc != CURLM_OK) { fprintf(stderr, "multi wait error: %s\n", curl_multi_strerror(mc)); break; } } while (still_running); // 所有请求完成后统一处理响应结果 for (int i = 0; i < g.finished_requests; i++) { RequestCtx *ctx = g.request_list[i]; if (ctx->response_buf) { printf("=== Response from %s ===\n%s\n", ctx->url, ctx->response_buf); // 在此处将响应存入你的业务存储结构即可 } } // 统一释放资源 for (int i = 0; i < g.total_requests; i++) { RequestCtx *ctx = g.request_list[i]; if (ctx) { free(ctx->url); free(ctx->response_buf); free(ctx); } } free(g.request_list); curl_multi_cleanup(g.multi_handle); curl_global_cleanup(); return 0; }
补充说明
- 编译命令:
gcc curl_client.c -o curl_client -lcurl,依赖系统安装libcurl开发包(Debian/Ubuntu系为libcurl4-openssl-dev,CentOS/RHEL系为libcurl-devel)。 - 公共请求头可以统一创建一个
curl_slist全局复用,curl不会修改该结构内容,所有请求完成后统一释放即可,不需要每个请求单独创建。 - 等你把这套逻辑跑通、完全理解curl multi的调度模型后,再去看官方
curl_multi_socket_action+epoll的示例门槛会低很多,后者性能更高但代码复杂度翻倍,100并发场景下前者性能完全够用。
内容的提问来源于stack exchange,提问作者federikowsky
相关产品推荐
相关产品推荐

