libcurl CURLMOPT_TIMERFUNCTION 详解:基于 multi_socket 事件驱动模型的超时回调机制
2026/9/10 12:37:13 网站建设 项目流程

libcurl CURLMOPT_TIMERFUNCTION 详解:基于 multi_socket 事件驱动模型的超时回调机制

【免费下载链接】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

CURLMOPT_TIMERFUNCTION 是 libcurl multi 接口中用于事件驱动(event-driven)编程的核心回调选项,它让应用在“没有任何 socket 事件发生”时也能被及时唤醒以处理超时与重试逻辑。本文以 docs/libcurl/opts/CURLMOPT_TIMERFUNCTION.md 为主体,结合 lib/multi.c 的底层实现、docs/examples/multi-event.c 的完整示例与 tests/unit/unit3230.c 的单元测试,系统讲解该回调的语义、触发时机、源码原理与实战接入方式,读完即可在自己的事件循环(libevent、libuv、select/poll/epoll 等)中正确集成 libcurl 的定时器驱动。

一、为什么需要 timer 回调:事件驱动接口的“静默期”问题

libcurl 的 multi 接口(libcurl-multi.md)有两种典型使用方式:

  • select/poll 轮询式:通过curl_multi_fdset()获取 fd 集合、curl_multi_timeout()获取最长等待时间,再调用curl_multi_perform()推进传输;
  • multi_socket 事件驱动式:通过CURLMOPT_SOCKETFUNCTION注册 socket 回调,配合事件循环库(libevent、libuv 等)监听每个 fd 的读写事件,有事件时调用curl_multi_socket_action()

事件驱动模式的难点在于:超时、重试、DNS 解析超时、连接建立超时等场景并不产生任何 socket 事件。如果应用只等待 socket 可读/可写,就可能无限期阻塞,导致这些需要“按时间推进”的逻辑永远得不到执行。CURLMOPT_TIMERFUNCTION 正是为此而生:libcurl 计算出内部最近的到期时间后,通过该回调通知应用“请在 N 毫秒后唤醒我一次”,从而把 libcurl 的超时管理无缝接入宿主的事件循环。

该选项自 curl 7.16.0 起加入(见文档头部 front-matter 的Added-in: 7.16.0),适用于全部协议(Protocol: All)。

二、回调原型与注册方式

CURLMOPT_TIMERFUNCTION 在 include/curl/multi.h 中以函数指针类型curl_multi_timer_callback定义:

#include <curl/curl.h> int timer_callback(CURLM *multi, /* multi handle */ long timeout_ms, /* timeout in number of ms */ void *clientp); /* private callback pointer */ CURLMcode curl_multi_setopt(CURLM *handle, CURLMOPT_TIMERFUNCTION, timer_callback);

参数语义:

参数含义
multi触发本次回调的 multi handle(CURLM *)
timeout_ms下一次到期时间距离现在的毫秒数;-1表示删除定时器
clientp应用自定义指针,由CURLMOPT_TIMERDATA传入,libcurl 不触碰、原样透传

默认值为NULL(即不注册回调,事件驱动模式下将无法获知内部超时)。注册方式:

curl_multi_setopt(multi, CURLMOPT_TIMERFUNCTION, timerfunc); curl_multi_setopt(multi, CURLMOPT_TIMERDATA, &mydata); /* 可选,配套使用 */

从源码看,setopt 在 lib/multi.c 中把回调指针存入multi->timer_cb,配套的CURLMOPT_TIMERDATA(选项编号见 include/curl/multi.h)存入multi->timer_userp,二者在 lib/multi.c 处一起被使用。

三、timeout_ms 的完整语义

回调收到的timeout_ms是 libcurl 内部“最近到期时间”与当前时刻的差值,其语义分三种情况:

1.timeout_ms >= 0:安装(或替换)一个一次性定时器

回调应安装一个单次(non-repeating)定时器,到期时间为timeout_ms毫秒。定时器到期后,应用必须主动推进 libcurl 一次:

  • 若使用 multi_socket 接口:调用curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &running)
  • 若使用旧的curl_multi_perform()接口:直接调用curl_multi_perform()

2. 定时器已存在时的新值 = 替换

文档明确强调:如果本次回调被调用时已有一个定时器在运行,这个新的到期时间会“替换”旧的。应用应当取消旧定时器,再按新值重新设置。不要在旧值和新值之间取最小值或叠加——libcurl 已经计算好最终结果,应用只需无条件采用最新一次回调给出的值。

从实现看,lib/multi.c 的Curl_update_timer()会记录上一次的绝对到期时刻multi->last_expire_offset_us与“是否已设置”标志multi->last_timeout_set,仅当到期绝对时刻发生变化(timeouts_offset_us不同)时才重新调用回调,避免对同一时刻反复通知应用重置定时器。

3.timeout_ms == -1:删除定时器

值为-1表示 libcurl 当前没有任何需要等待的超时(例如所有传输完成、全部挂起被清除),应用应删除/取消当前定时器,进入纯事件等待状态。

4.timeout_ms == 0:立即处理

0是合法值,表示“立刻就需要被唤醒”——比如刚加入传输、内部状态变更后 libcurl 需要马上推进。应用应当尽快(比如通过事件循环的下一个 tick 或立即调用)触发一次curl_multi_socket_action(..., CURL_SOCKET_TIMEOUT, ...)

四、回调返回值与错误处理

return 0; /* 成功 */ return -1; /* 错误:multi handle 中所有进行中的传输将被中止并失败 */

关键约束(文档原文强调):

  • 成功返回0
  • 返回-1表示错误,此时multi handle 中所有正在进行的传输都会被中止并标记为失败

源码中的对应处理在 lib/multi.c:回调返回-1后,libcurl 将multi->dead置为TRUE并返回CURLM_ABORTED_BY_CALLBACK。也就是说,定时器回调不仅是“通知”,还是一个可用的中止开关——当应用判断自身状态已无法继续(例如底层事件循环已销毁)时,可以借此让所有传输统一失败退出。

五、零毫秒超时的递归风险(重要警告)

文档在末尾特别给出 WARNING:

timeout_ms为 0 时,不要在回调内部直接调用 libcurl 的函数,因为这可能触发危险的递归行为——立即产生另一次值为 0 的回调……

也就是说,回调中不能因为收到0就同步调用curl_multi_socket_action()curl_multi_perform()——后者在推进传输时又可能再次调用 timer 回调(还是 0),形成“回调 → 推进 → 回调 → 推进……”的无限递归。正确做法是把“立即处理”的需求交给事件循环去调度:例如在 docs/examples/multi-event.c 中,示例把0归一化为1毫秒的定时器,“0 means call socket_action asap”,既保证尽快触发,又避免同步递归。

六、回调的触发时机与底层原理

1. 触发链路

timer 回调并非每次传输推进都会触发,而是仅在到期时刻发生变化时被调用(文档原文:The timer_callback is called when the timeout expire time is changed)。核心实现Curl_update_timer()位于 lib/multi.c,逻辑如下:

  1. 若未注册回调(!multi->timer_cb)或 multi 已“死亡”,直接返回;
  2. 通过内部multi_timeout()计算当前最近到期时间对应的timeout_ms
  3. 与上一次记录的状态比较:
    • 原来无超时、现在有超时 → 通知“设置定时器”([TIMER] set %dms, none before);
    • 原来有超时、现在无超时 → 通知“清除定时器”(timeout_ms = -1);
    • 两次绝对到期时刻不同 → 通知“替换定时器”([TIMER] set %dms, replace previous);
    • 绝对到期时刻相同 →不调用回调(应用已有定时器在跑,无需重启);
  4. 需要通知时,以multi->timer_cb(multi, timeout_ms, multi->timer_userp)调用应用回调,并按返回值决定是否置dead

注意第 3 点的一个细节:即便两次回调给出的相对timeout_ms相同,只要绝对到期时刻(last_expire_offset_us)不同,libcurl 也会要求应用重启定时器,因为“起点”已经变了,旧的定时器计时基准不再准确。

2. 单元测试的验证

tests/unit/unit3230.c 用 6 次回调完整覆盖了上述行为:

  • 初始无超时时Curl_update_timer()调用回调(ctx.count == 0);
  • 设置 600000ms 超时 → 回调 1 次,timeout_ms[0] >= 0
  • 改为 1200000ms(替换)→ 再回调 1 次,且timeout_ms[1] > timeout_ms[0]
  • 相同到期时间再次 update →不再回调count不变);
  • curl_multi_socket_action(handle, CURL_SOCKET_TIMEOUT, 0, ...)强制刷新 → 回调,timeout_ms[2] >= 0
  • Curl_expire_clear_all()清除全部超时 → 回调,timeout_ms[3] == -1
  • 标记 dirty(零超时场景)→ 回调,timeout_ms[4] == 0;清除 dirty → 回调,timeout_ms[5] == -1

这组断言与文档语义一一对应,是理解回调行为的“活文档”。

七、与 curl_multi_timeout 的关系

timer 回调可以替代或补充curl_multi_timeout()(见 curl_multi_timeout.md):

  • curl_multi_timeout(multi, &timeo)轮询式查询:调用时返回当前应等待的毫秒数,0表示立即推进,-1表示无超时。它适合 select/poll 模型,在每次循环中查询并设置select()的等待上限;
  • timer 回调是推送式通知:到期时间一变就主动告知应用,适合事件驱动模型,避免了每次循环重复查询的麻烦。

curl_multi_timeout.md 明确建议:使用 multi_socket API 的应用不应使用curl_multi_timeout(),而应使用 CURLMOPT_TIMERFUNCTION。若坚持用轮询方式,注意-1表示“当前没有已存超时”,也不应等待过久(文档建议不超过几秒)再调用curl_multi_perform()

八、完整实战:基于 libevent 的 timer 回调接入

下面直接取自仓库示例 docs/examples/multi-event.c,展示如何把 timer 回调翻译成 libevent 的 evtimer:

static void on_timeout(evutil_socket_t fd, short events, void *arg) { int running_handles; (void)fd; (void)events; (void)arg; curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &running_handles); check_multi_info(); } static int start_timeout(CURLM *multi, long timeout_ms, void *userp) { (void)multi; (void)userp; if(timeout_ms < 0) { evtimer_del(timeout); /* -1:删除定时器 */ } else { struct timeval tv; if(timeout_ms == 0) timeout_ms = 1; /* 0:立即处理,但避免同步递归 */ tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; evtimer_del(timeout); /* 替换旧定时器 */ evtimer_add(timeout, &tv); } return 0; /* 0 表示成功 */ }

接入要点对照:

  1. start_timeout作为CURLMOPT_TIMERFUNCTION的回调,任何一次调用都直接采用最新的timeout_ms(先evtimer_delevtimer_add,实现“替换”);
  2. timeout_ms == -1时只删除定时器;
  3. timeout_ms == 0时归一化为 1ms,既尽快触发又不造成同步递归(对应文档的 WARNING);
  4. 定时器到期后调用curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &running_handles)推进 libcurl(CURL_SOCKET_TIMEOUT定义于 include/curl/multi.h);
  5. 主流程按curl_multi_socket_action文档(curl_multi_socket_action.md)的典型步骤组合:初始化 multi → 设置 SOCKETFUNCTION → 设置 TIMERFUNCTION →curl_multi_add_handle加入 easy handle → 用curl_multi_socket_action(..., CURL_SOCKET_TIMEOUT, 0, ...)启动 → 在事件循环中等待 socket 事件与定时器到期。

仓库还提供了多个同主题的完整参考实现:docs/examples/hiperfifo.c(FIFO + select)、docs/examples/ephiperfifo.c(epoll + FIFO)、docs/examples/evhiperfifo.c(libevent + FIFO)、docs/examples/ghiper.c(glib 主循环)、docs/examples/multi-uv.c(libuv),以及测试目录中的 tests/libtest/lib530.c、tests/libtest/lib758.c、tests/libtest/lib582.c 等 libtest 用例,可作为多场景移植模板。

九、配套选项:CURLMOPT_TIMERDATA

回调的clientp参数由 CURLMOPT_TIMERDATA 提供:

CURLMcode curl_multi_setopt(CURLM *handle, CURLMOPT_TIMERDATA, void *pointer);
  • libcurl不触碰该指针,仅在每次调用 timer 回调时把它原样传给clientp(默认值为NULL);
  • 典型用法是传入一个包含事件循环上下文的结构体,例如示例中的struct priv { void *custom; },让回调无需全局变量即可访问应用状态。

配套文档 CURLMOPT_SOCKETFUNCTION 描述了与本选项协同的 socket 回调,两者共同构成 multi_socket 事件驱动模型的两个“通知出口”。

十、返回值与错误码

curl_multi_setopt()返回CURLMcode

  • CURLM_OK (0):设置成功;
  • 非 0:出错,具体错误码参见 libcurl 错误码说明(libcurl-multi.md 与 multi 接口文档)。

小结

CURLMOPT_TIMERFUNCTION 是 libcurl 事件驱动编程中不可或缺的一环:它以“到期时间变更即通知”的方式,把 libcurl 内部的超时/重试调度精确映射到宿主事件循环。使用时牢记四点:-1删除定时器、0立即处理但禁止在回调内同步调用 libcurl、新值总是替换旧值、返回-1会中止全部传输。结合 docs/examples/multi-event.c 的 libevent 模板与 tests/unit/unit3230.c 的行为断言,即可在自己的应用中实现正确、高效的事件驱动传输框架。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询