curl_multi_add_handle 详解:将 easy handle 加入 multi 会话的正确姿势
【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl
导读
curl_multi_add_handle()是 libcurl 多接口(multi interface)的核心入门口:它把单个 easy handle 挂载到由curl_multi_init()创建的 multi 会话上,使该句柄进入 libcurl 内部的异步调度体系。在 curl 的 multi 编程模型中,几乎所有并发传输(多个 HTTP 请求并行、事件驱动的 socket 回调、DoH 内部解析等)都以它为起点。读完本文,你将掌握该函数的调用签名、加入后的行为约束、与curl_easy_perform()的互斥关系、连接池与 DNS 缓存的共享机制、事件驱动下的定时器联动,以及完整的错误码与清理顺序。
函数签名与原型
curl_multi_add_handle()在头文件 include/curl/curl.h 中声明,自libcurl 7.9.6起提供,适用于所有协议。
#include <curl/curl.h> CURLMcode curl_multi_add_handle(CURLM *multi_handle, CURL *easy_handle);| 参数 | 说明 |
|---|---|
multi_handle | 由curl_multi_init()返回的 multi 会话句柄(CURLM *),传输完成后应交给curl_multi_cleanup()释放 |
easy_handle | 由curl_easy_init()创建、并已通过curl_easy_setopt()配置好选项的 easy 句柄(CURL *) |
返回值是CURLMcode:CURLM_OK (0)表示成功,非零表示出错,完整错误码见 libcurl-errors(3)。
DESCRIPTION:加入 multi 栈后发生了什么
行为一:easy 句柄进入 multi 独占模式
一旦 easy handle 被加入 multi 栈,在它仍处于 multi 栈中的期间,绝不能对同一个句柄调用curl_easy_perform()。从源码看,curl_easy_perform()在 lib/easy.c 中本身就会为单个 easy 句柄内部创建私有 multi 句柄并调用内部Curl_multi_add_handle();若一个 easy 句柄同时被外部 multi 与内部私有 multi 引用,会产生状态冲突。只有在通过curl_multi_remove_handle()把该 easy 句柄从 multi 栈移除之后,才能再次放心地以 easy 接口(curl_easy_perform())使用它。
行为二:共享 DNS 缓存
如果该 easy handle 没有通过CURLOPT_SHARE指定共享缓存,那么当curl_multi_add_handle()被调用时,它会被设置为使用一个在该 multi 句柄内所有 easy handle 之间共享的 DNS 缓存。这意味着同一 multi 会话中并发的多个 easy 句柄解析同一主机名时,可以复用解析结果,减少重复 DNS 查询。
行为三:共享连接缓存与连接复用
easy 接口被加入 multi 句柄后,它会被设置为使用由 multi 句柄拥有的共享连接缓存(connection cache / cpool)。反复地移除再加入新的 easy 句柄不会影响连接池中的连接,也不影响连接复用能力——已经建立的连接会留在 multi 的连接池里,供后续加入的 easy 句柄复用。这一点在 lib/multi.c 的内部实现中通过Curl_cpool_xfer_init(data)以及multi_conn_should_close()(lib/multi.c)对连接关闭与保留的判定得到印证:当传输结束时,只要连接仍可复用(未被标记关闭、协议支持复用、没有 premature 结束),连接会被保留在 cpool 中(infof(data, "Connection #... left intact", ...)),而不是随 easy 句柄一起销毁。
行为四:事件驱动模式下的定时器联动
如果你在 multi 句柄上设置了CURLMOPT_TIMERFUNCTION(使用curl_multi_socket_action()及配套函数做事件驱动编程时理应如此),那么该回调会在这个函数内部被调用,用于请求一个更新后的定时器,以便你的主事件循环能及时获得该句柄上的活动并开始处理。源码层面的对应关系是:内部实现Curl_multi_add_handle()在完成注册后会调用Curl_update_timer(multi)(lib/multi.c),并在注释中明确说明“在基于事件的处理中,dirty handle 会触发一次 timeout callback 调用”。
行为五:句柄会一直留在 multi 栈中
easy handle 在被curl_multi_remove_handle()移除之前,始终保留在 multi 句柄中——即使该句柄上的传输已经完成。也就是说,一次传输结束不代表句柄被自动摘除,你需要显式地调用移除函数。
正确的清理顺序(Teardown 顺序)
文档明确给出了在终止前应当遵循的移除/清理顺序:
curl_multi_remove_handle()—— 先从 multi 栈中移除 easy 句柄;curl_easy_cleanup()—— 再释放 easy 句柄本身;curl_multi_cleanup()—— 最后销毁 multi 句柄。
先移除、后清理的顺序保证 easy 句柄不再被 multi 会话引用,避免在释放句柄时出现悬空引用或双重释放。参考实现中curl_easy_perform()的收尾(lib/easy.c)也是“先Curl_multi_remove_handle()、再让私有 multi 在curl_multi_cleanup()中释放”的模式,可以作为正确用法的内部印证。
完整示例:添加两个 easy 句柄到 multi 会话
int main(void) { /* init a multi stack */ CURLM *multi = curl_multi_init(); /* create two easy handles */ CURL *http_handle = curl_easy_init(); CURL *http_handle2 = curl_easy_init(); /* add individual transfers */ curl_multi_add_handle(multi, http_handle); curl_multi_add_handle(multi, http_handle2); }更完整的实战流程是:创建 easy 句柄 →curl_easy_setopt()设置 URL 等选项 →curl_multi_add_handle()加入会话 → 用curl_multi_poll()/curl_multi_perform()驱动传输 → 用curl_multi_info_read()读取每个句柄的完成状态 → 对完成的句柄调用curl_multi_remove_handle()并curl_easy_cleanup()→ 最后curl_multi_cleanup()结束会话。注意:同一 easy 句柄不能重复添加(见下文错误码CURLM_ADDED_ALREADY),且不能同时添加到多个 multi 栈。
RETURN VALUE 与错误码
该函数返回CURLMcode,成功为CURLM_OK (0),非零表示错误。常见错误码及其含义(摘自 libcurl-errors.md):
| 错误码 | 数值 | 含义 |
|---|---|---|
CURLM_OK | 0 | 一切正常 |
CURLM_BAD_HANDLE | 1 | 传入的不是有效的CURLM句柄 |
CURLM_BAD_EASY_HANDLE | 2 | easy 句柄无效/损坏,可能根本不是 easy 句柄,或该句柄已被本 multi 或其他 multi 使用 |
CURLM_OUT_OF_MEMORY | 3 | 内存不足(文档原话:You are doomed) |
CURLM_ADDED_ALREADY | 7 | 尝试把一个已经加入过 multi 句柄的 easy 句柄再次加入 |
从源码可以印证这些错误的产生路径:公开入口curl_multi_add_handle()(lib/multi.c)会先校验句柄合法性(不合法返回CURLM_BAD_EASY_HANDLE),再调用内部Curl_multi_add_handle();内部实现(lib/multi.c)中,如果data->multi已被设置(句柄已属于某个 multi),直接返回CURLM_ADDED_ALREADY,注释明确写着“Prevent users from adding same easy handle more than once and prevent adding to more than one multi stack”。此外,如果 multi 处于“dead”状态且仍存在存活的传输,返回CURLM_ABORTED_BY_CALLBACK;句柄表扩容失败时返回CURLM_OUT_OF_MEMORY。
内部实现要点(源码级)
- 入口与校验:公开函数
curl_multi_add_handle()(lib/multi.c)通过CURL_MAPI_ENTER/LEAVE保护并发访问,并检查GOOD_EASY_HANDLE(data),不通过则返回CURLM_BAD_EASY_HANDLE。 - 重复添加防护:内部
Curl_multi_add_handle()(lib/multi.c)先检查data->multi,已存在则返回CURLM_ADDED_ALREADY。 - 私有 multi 的回收:如果该 easy 句柄此前通过
curl_easy_perform()使用过(data->multi_easy非空),内部会调用curl_multi_cleanup(data->multi_easy)清掉这个私有 multi(lib/multi.c),保证句柄干净地进入外部 multi。 - 注册与状态初始化:将 easy 插入 multi 的传输表并分配
mid,初始化超时(multi_timeouts_init),把data->multi指回 multi 句柄,设置状态机为MSTATE_INIT,标记为 dirty 以触发运行,最后调用Curl_update_timer()更新事件驱动定时器(lib/multi.c)。 - 连接池准备:
Curl_cpool_xfer_init(data)(lib/multi.c)为这个 easy 句柄准备与 multi 共享的连接池关系,这正是文档中“共享连接缓存、连接可复用”的实现基础。 - DoH 的内部复用:libcurl 自身在 DoH(DNS over HTTPS)解析时也会用内部 multi 添加 DoH 请求句柄(lib/vdns/doh.c 调用
Curl_multi_add_handle(multi, doh)),说明该函数不仅是用户接口,也是 libcurl 内部异步架构的基础设施。
相关接口
- curl_multi_init(3) —— 创建 multi 会话
- curl_multi_remove_handle(3) —— 从 multi 会话移除 easy 句柄
- curl_multi_cleanup(3) —— 销毁 multi 会话
- curl_multi_get_handles(3) —— 获取 multi 会话中的句柄列表
- curl_multi_setopt(3) —— 设置 multi 会话选项(如
CURLMOPT_TIMERFUNCTION) - curl_multi_socket_action(3) —— 事件驱动模式下驱动传输
- libcurl-errors(3) —— 完整错误码说明
【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考