You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 11:03:25