libcurl CURLMOPT_TIMERFUNCTION 详解:基于 multi_socket 事件驱动模型的超时回调机制
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/curlCURLMOPT_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 handleCURLM *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逻辑如下若未注册回调!multi-timer_cb或 multi 已“死亡”直接返回通过内部multi_timeout()计算当前最近到期时间对应的timeout_ms与上一次记录的状态比较原来无超时、现在有超时 → 通知“设置定时器”[TIMER] set %dms, none before原来有超时、现在无超时 → 通知“清除定时器”timeout_ms -1两次绝对到期时刻不同 → 通知“替换定时器”[TIMER] set %dms, replace previous绝对到期时刻相同 →不调用回调应用已有定时器在跑无需重启需要通知时以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] 0Curl_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.mdcurl_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 的 evtimerstatic 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 表示成功 */ }接入要点对照start_timeout作为CURLMOPT_TIMERFUNCTION的回调任何一次调用都直接采用最新的timeout_ms先evtimer_del再evtimer_add实现“替换”timeout_ms -1时只删除定时器timeout_ms 0时归一化为 1ms既尽快触发又不造成同步递归对应文档的 WARNING定时器到期后调用curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, running_handles)推进 libcurlCURL_SOCKET_TIMEOUT定义于 include/curl/multi.h主流程按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.cFIFO select、docs/examples/ephiperfifo.cepoll FIFO、docs/examples/evhiperfifo.clibevent FIFO、docs/examples/ghiper.cglib 主循环、docs/examples/multi-uv.clibuv以及测试目录中的 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()返回CURLMcodeCURLM_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),仅供参考