尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

libcurl 多接口事件循环基石:curl_multi_fdset 提取文件描述符并驱动 select() 的完整实践指南

libcurl 多接口事件循环基石:curl_multi_fdset 提取文件描述符并驱动 select() 的完整实践指南 libcurl 多接口事件循环基石curl_multi_fdset 提取文件描述符并驱动 select() 的完整实践指南【免费下载链接】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_fdset是 libcurl multi 接口select 风格的核心 API负责从 multi handle 中提取 libcurl 当前正在使用的文件描述符填充到应用自定义的fd_set中使应用能够在自己的事件循环里通过select()统一等待网络活动。本文基于当前仓库 docs/libcurl/curl_multi_fdset.md 展开结合 lib/multi.c 的源码实现与仓库测试用例完整讲解函数签名、三个fd_set参数与max_fd的语义、与curl_multi_timeout/curl_multi_perform配合的完整驱动循环、FD_SETSIZE限制的底层原因并给出可直接复制运行的生产级示例代码。读完本文你将能独立搭建一个基于 select() 的 libcurl 多路并发传输事件循环。一、函数总览从 multi handle 提取文件描述符curl_multi_fdset是 libcurl multi 接口中面向 select() 风格编程的核心函数。它的作用非常纯粹扫描 multi handle 中所有 easy handle 当前的状态把 libcurl 需要监听的 socket 文件描述符填充进应用提供的fd_set集合中并返回其中最大的描述符编号供应用构造select()调用。#include curl/curl.h CURLMcode curl_multi_fdset(CURLM *multi_handle, fd_set *read_fd_set, fd_set *write_fd_set, fd_set *exc_fd_set, int *max_fd);该函数适用于所有协议文档Protocol: All自 libcurl 7.9.6 版本加入Added-in: 7.9.6是 multi 接口最早期的 API 之一。函数的输入输出由三个fd_set指针和一个int *max_fd指针承载其含义与select()的对应参数一一对应。参数语义速查表参数方向类型说明multi_handle输入CURLM *由curl_multi_init()创建的 multi handleread_fd_set输出fd_set *返回后包含需要检查可读的文件描述符集合write_fd_set输出fd_set *返回后包含需要检查可写的文件描述符集合exc_fd_set输出fd_set *返回后包含需要检查异常/错误条件的文件描述符集合max_fd输出int *返回 libcurl 设置的最大描述符编号无任何描述符时返回 -1二、三个 fd_set 参数的正确打开方式read_fd_set、write_fd_set、exc_fd_set三个参数分别指向应用自己分配的fd_set对象curl_multi_fdset返回时read_fd_set指定需要被检查是否已准备好读取的文件描述符。例如 TCP 连接上到达了新的数据、TLS 握手完成等可读事件都会反映在这个集合中write_fd_set指定需要被检查是否已准备好写入的文件描述符。例如 socket 发送缓冲区清空、connect() 完成等可写事件exc_fd_set指定需要被检查异常条件的文件描述符。需要说明的是从源码实现看libcurl 当前并不会向exc_fd_set中设置任何描述符见下文源码剖析中的(void)exc_fd_set;但保留该参数是为了与select()的接口签名保持对齐应用仍应像往常一样将其传入。最关键的调用约定调用前必须 FD_ZERO文档明确强调了一个极易被忽视的约定be sure toFD_ZEROthem before calling this function as curl_multi_fdset(3) only adds its own descriptors, it does not zero or otherwise remove any others.也就是说curl_multi_fdset只做加法——它把 libcurl 自己的描述符通过FD_SET追加到你的集合中而不会帮你清零或移除集合中已有的其他描述符。如果你在每次循环迭代中复用了同一组fd_set这是事件循环的标准做法那么必须在调用curl_multi_fdset之前手动FD_ZERO这三个集合否则上一次迭代留下的陈旧描述符会残留其中导致select()等待到早已不需要等待的 socket甚至引发虚假唤醒或忙等。典型的循环开头长这样FD_ZERO(fdread); FD_ZERO(fdwrite); FD_ZERO(fdexcep); mresult curl_multi_fdset(multi, fdread, fdwrite, fdexcep, maxfd);关于 exc_fd_set 的实现细节从 lib/multi.c 中curl_multi_fdset的实现可以看到源码在函数入口处即执行了(void)exc_fd_set;表明当前实现并不向异常集合写入任何描述符。这意味着在基于本仓库代码的实际使用中exc_fd_set更多是接口兼容性的保留参数——你仍需要传入一个合法的fd_set *或安全的占位对象但不能依赖它获得有意义的内容。三、max_fd 的返回值语义-1 与 FD_SETSIZE 陷阱max_fd是curl_multi_fdset最容易被误读的输出参数它的取值有两种情况处理方式截然不同。情况一返回 -1 —— libcurl 当前没有可监视的 socket当 libcurl 没有设置任何文件描述符时max_fd会被置为 -1。文档明确指出这通常意味着libcurl 正在做一件无法通过 socket 监视的事情例如某些解析操作或内部阻塞任务因此你无法用select()精确得知当前动作何时完成。此时的正确做法是先等上一段时间然后无条件调用curl_multi_perform()。至于等多久文档给出了一条务实建议Unless curl_multi_timeout(3) gives you a lower number, we suggest 100 milliseconds or so, but you may want to test it out in your own particular conditions to find a suitable value.即优先采用curl_multi_timeout()给出的值如果该函数没有给出更小的值建议等待约100 毫秒并鼓励在自身运行环境下实测调优。情况二返回非负值 —— select() 的第一个参数当 libcurl 设置了描述符时max_fd返回其中最大的描述符编号。select()的第一个参数要求传入最大描述符编号 1因此这里直接就是rc select(maxfd 1, fdread, fdwrite, fdexcep, timeout);FD_SETSIZE 限制不设置比越界写更安全文档用一个专门段落警告了fd_set的容量问题If one of the sockets used by libcurl happens to be larger than what can be set in an fd_set, which on POSIX systems means that the file descriptor is larger than FD_SETSIZE, then libcurl tries to not set it. Setting a too large file descriptor in an fd_set implies an out of bounds write which can cause crashes, or worse.在 POSIX 系统上fd_set是固定大小的位图其容量由FD_SETSIZE决定典型值为 1024。向其中写入超过该容量的描述符编号会构成越界写可能导致内存破坏乃至崩溃。因此 libcurl 在遇到超大描述符时策略是干脆不把它放进集合。这一点在 lib/select.h 中有精确的源码佐证/* With Winsock the valid range is [0..INVALID_SOCKET-1] according to https://learn.microsoft.com/windows/win32/winsock/socket-data-type-2 */ #ifdef USE_WINSOCK #define VALID_SOCK(s) ((s) INVALID_SOCKET) #define FDSET_SOCK(x) 1 #else #define VALID_SOCK(s) ((s) 0) /* If the socket is small enough to get set or read from an fdset */ #define FDSET_SOCK(s) ((s) FD_SETSIZE) #endifFDSET_SOCK(s)宏在非 Winsock 平台上被定义为(s) FD_SETSIZE而在curl_multi_fdset的实现中for(i 0; i ps.n; i) { if(!FDSET_SOCK(ps.sockets[i])) /* pretend it does not exist */ continue; if(ps.actions[i] CURL_POLL_IN) FD_SET(ps.sockets[i], read_fd_set); if(ps.actions[i] CURL_POLL_OUT) FD_SET(ps.sockets[i], write_fd_set); if((int)ps.sockets[i] this_max_fd) this_max_fd (int)ps.sockets[i]; }超出FD_SETSIZE的 socket 会被注释为 pretend it does not exist 而跳过。后果是双面的不设置它可能让你免于崩溃但你的程序将不会等待本应等待的 socket——事件循环可能因此错过该连接上的活动。文档明确提示The effect of NOT storing it might possibly save you from the crash, but makes your program NOT wait for sockets it should wait for... 这是 select() 接口固有的伸缩性缺陷也是后续curl_multi_wait/curl_multi_pollAPI 被引入的根本动机之一详见第七节。四、超时策略与 curl_multi_timeout 的黄金搭档仅靠curl_multi_fdset提供 socket 集合还不够事件循环还需要知道select()应该等待多久。这个问题由curl_multi_timeout解决CURLMcode curl_multi_timeout(CURLM *multi_handle, long *timeout);它返回的timeout是毫秒数语义为0应立即继续处理无需等待任何活动-1libcurl 当前没有设置任何超时值。文档警告此时不要等待太久最多几秒否则内部的重试和超时机制可能无法按预期工作其他正值select()等待的上限。在select()之前把毫秒数转换成struct timevallong timeo; curl_multi_timeout(multi, timeo); if(timeo 0) timeo 980; /* 无超时时的合理默认值 */ timeout.tv_sec timeo / 1000; timeout.tv_usec (timeo % 1000) * 1000;文档特别强调了一个容易踩的坑即使select()在超时时间内没有观察到任何 socket 活动超时一到也必须调用curl_multi_perform()否则 internal retries and timeouts may not work as you would think and want——libcurl 内部的超时、重试机制完全依赖应用在合适的时间点回来驱动它任何一次偷懒都可能让连接卡死或行为异常。标准驱动循环TYPICAL USAGE综合文档对curl_multi_fdset与curl_multi_timeout的说明select 风格 multi 接口的典型使用模式是调用curl_multi_perform()推进传输调用curl_multi_fdset()获取要监视的 fd_set 与max_fd调用curl_multi_timeout()获取超时上限用select()等待 socket 活动或超时无论有无活动回到第 1 步直到所有传输完成。五、完整可运行示例一个健壮的 select() 事件循环下面是文档示例的完整化版本——补上了初始化、curl_multi_timeout整合、超时兜底、错误处理与退出条件形成一个真正可复制运行的 select 风格 multi 事件循环#include stdio.h #include sys/select.h #include curl/curl.h int main(void) { CURL *easy; CURLM *multi; CURLMcode mresult; fd_set fdread, fdwrite, fdexcep; int maxfd; int still_running; int rc; easy curl_easy_init(); multi curl_multi_init(); /* 配置 easy handle 并加入 multi stack */ curl_easy_setopt(easy, CURLOPT_URL, https://example.com/); curl_multi_add_handle(multi, easy); do { struct timeval timeout; long timeo; /* 推进所有传输still_running 表示仍在进行的传输数 */ mresult curl_multi_perform(multi, still_running); if(mresult ! CURLM_OK) { fprintf(stderr, curl_multi_perform() failed, code %d.\n, mresult); break; } if(!still_running) break; /* 所有传输结束 */ /* 关键约定调用 fdset 前必须清零三个集合 */ FD_ZERO(fdread); FD_ZERO(fdwrite); FD_ZERO(fdexcep); /* 从 multi handle 提取文件描述符 */ mresult curl_multi_fdset(multi, fdread, fdwrite, fdexcep, maxfd); if(mresult ! CURLM_OK) { fprintf(stderr, curl_multi_fdset() failed, code %d.\n, mresult); break; } /* 用 curl_multi_timeout 计算 select 等待时长 */ mresult curl_multi_timeout(multi, timeo); if(mresult ! CURLM_OK) break; if(timeo 0) timeo 100; /* 无内部超时按文档建议默认等待 100ms */ else if(timeo 1000) timeo 1000; /* 上限 1 秒保持循环响应性 */ timeout.tv_sec timeo / 1000; timeout.tv_usec (timeo % 1000) * 1000; /* maxfd 为 -1 表示没有可监视的 socket也要短暂等待后继续 */ rc select(maxfd 1, fdread, fdwrite, fdexcep, maxfd -1 ? timeout : timeout); if(rc 0) { perror(select); break; } /* select 返回后无论是否有活动循环回到 curl_multi_perform() */ } while(still_running); curl_multi_remove_handle(multi, easy); curl_easy_cleanup(easy); curl_multi_cleanup(multi); return 0; }该示例的关键细节still_running是循环的心跳curl_multi_perform通过这个输出参数告知仍有多少传输在进行当它为 0 时表示全部传输完成注意完成不等于成功具体成败需用curl_multi_info_read查询见下文第六节无论select()是否有活动都必须回到curl_multi_perform()这正是上一节强调的驱动义务——curl_multi_fdset文档原话是 The curl_multi_perform(3) function should be called as soon as one of them is ready to be read from or written tomaxfd -1分支此时没有可监视的 socket但仍应按超时等待一小段再继续避免忙等。六、源码剖析curl_multi_fdset 内部到底做了什么理解底层实现能帮助你更准确地预判行为。curl_multi_fdset的完整实现位于 lib/multi.c约第 1257–1310 行核心逻辑分三步1. 遍历 multi handle 中的所有 easy handle聚合 pollsetCurl_pollset_init(ps); if(Curl_uint32_bset_first(multi-process, mid)) { do { struct Curl_easy *data Curl_multi_get_easy(multi, mid); ... Curl_multi_pollset(data, ps); ... } while(Curl_uint32_bset_next(multi-process, mid, mid)); }libcurl 用位集合uint32_bset见 lib/uint-bset.h管理当前需要处理的 easy handle 集合逐个调用Curl_multi_pollset()lib/multi.c 中Curl_multi_pollset约第 1142–1255 行收集每个 easy handle 当前希望监视的 socket 与动作。2. 依据 easy handle 状态机决定监视什么Curl_multi_pollset内部根据传输所处的mstatemulti state 状态机决定是否提供 socket 以及提供什么动作相关分支包括状态pollset 行为MSTATE_INIT/MSTATE_PENDING/MSTATE_SETUP/MSTATE_CONNECT尚无 socket 可监视MSTATE_CONNECTING监视连接过程可写事件代表 connect 完成MSTATE_PROTOCONNECT/MSTATE_PROTOCONNECTING监视协议连接阶段如 TLS 握手MSTATE_DO/MSTATE_DOING/MSTATE_DOING_MORE监视请求发送阶段MSTATE_DID/MSTATE_PERFORMING监视数据传输阶段MSTATE_RATELIMITING需要让时间流逝忽略 socketMSTATE_DONE/MSTATE_COMPLETED/MSTATE_MSGSENT无需再监视这解释了为什么curl_multi_fdset返回的集合会随传输阶段动态变化——同一连接在连接期、发送期、接收期会被放入不同的集合应用层无需感知这些细节只需忠实转发给select()即可。3. 把 pollset 映射进 fd_setfor(i 0; i ps.n; i) { if(!FDSET_SOCK(ps.sockets[i])) continue; /* 超出 FD_SETSIZE跳过 */ if(ps.actions[i] CURL_POLL_IN) FD_SET(ps.sockets[i], read_fd_set); if(ps.actions[i] CURL_POLL_OUT) FD_SET(ps.sockets[i], write_fd_set); if((int)ps.sockets[i] this_max_fd) this_max_fd (int)ps.sockets[i]; }ps.actions是CURL_POLL_IN/CURL_POLL_OUT的位图定义参见 lib/select.hFDSET_SOCK宏在此处完成对FD_SETSIZE的容量守卫。最后函数还会把关闭流程所需的描述符Curl_cshutdn_setfds见 lib/cshutdn.c并入集合并将this_max_fd写入max_fd输出参数。此外整个函数被CURL_MAPI_ENTER/CURL_MAPI_LEAVE包裹这是本仓库 multi 接口 API 的并发访问守卫机制确保函数在多线程环境下安全调用。测试用例佐证仓库测试中tests/libtest/lib504.c 即是一个使用 multi 接口驱动传输并通过代理端口异常场景验证不挂死行为的经典用例源自 bug 651464 报告展示了 multi 事件循环 超时保护在真实故障条件下的正确形态tests/libtest/lib1905.c、tests/libtest/lib1507.c 等也直接调用了curl_multi_fdset。需要说明的是正如curl_multi_wait文档所述新代码通常优先使用curl_multi_wait/curl_multi_poll规避 fd_set 容量问题仓库测试亦多采用这些新 API。七、定位与取舍fdset、wait、poll 与 multi_socket在 docs/libcurl/libcurl-multi.md 中multi 接口被明确分为两种风格select() 风格旧curl_multi_fdsetcurl_multi_timeoutcurl_multi_perform。文档原文描述它为 the select() oriented one其优势是接口朴素、与任何基于 select/poll 的既有事件循环都能直接对接multi_socket 风格新curl_multi_socket_actionCURLMOPT_SOCKETFUNCTIONCURLMOPT_TIMERFUNCTION面向 libevent、libev、kqueue、epoll 等事件驱动框架可扩展到数千并发连接。针对 fd_set 的固有缺陷FD_SETSIZE上限、需要maxfd等libcurl 后来引入了更现代的替代curl_multi_wait7.28.0 加入内部使用 poll() 语义一次性等待 multi handle 中所有 easy handle 的 socket其文档明确说明 This function is encouraged to be used instead of select(3) when using the multi interface to allow applications to easier circumvent the common problem with 1024 maximum file descriptors——即官方鼓励用curl_multi_wait替代 select 风格的curl_multi_fdset以规避 1024 描述符上限问题curl_multi_pollcurl_multi_wait的增强版即使没有任何可等待的 socket 也会按超时阻塞避免curl_multi_wait在无描述符时立即返回导致的忙等问题。何时仍然需要 curl_multi_fdset尽管有更现代的替代curl_multi_fdset在以下场景中依然不可替代需要把 libcurl 的 socket 与你自己应用的文件描述符在同一个select()中统一等待这是 multi 接口的核心目标之一Enable the application to wait for action on its own file descriptors and curls file descriptors simultaneously见 docs/libcurl/libcurl-multi.md你的应用本身基于 select() 架构不希望引入 poll 语义或事件驱动框架描述符数量可控远低于FD_SETSIZE的轻量并发场景。八、返回值与错误处理curl_multi_fdset返回CURLMcodeCURLM_OK(0)一切正常非零发生错误具体错误码参见 docs/libcurl/libcurl-errors.md。从源码看本函数可能返回的错误包括CURLM_OUT_OF_MEMORYpollset 扩容失败时对应底层CURLE_OUT_OF_MEMORY与CURLM_INTERNAL_ERROR确定 pollset 时出错。任何非CURLM_OK的返回值都应被视为致命错误中断循环、清理资源后退出而不是继续调用select()。九、实践注意事项汇总综合文档与源码使用curl_multi_fdset时有几条必须牢记的纪律每次调用前FD_ZERO三个集合——curl_multi_fdset只追加不清零max_fd -1时不能精确等待——按curl_multi_timeout或默认 100ms 短等后继续调用curl_multi_perform超时后必须调用curl_multi_perform——即使select()没有观察到活动否则内部重试与超时机制失效警惕FD_SETSIZE上限——描述符超限时 libcurl 会跳过该 socket导致程序不等待本该等待的连接并发规模较大时优先改用curl_multi_wait/curl_multi_poll参见 docs/libcurl/curl_multi_wait.md、docs/libcurl/curl_multi_poll.md传输完成不等于成功——通过curl_multi_info_read读取完成消息判断每个传输的成败见 docs/libcurl/curl_multi_info_read.md注意阻塞点——根据 docs/libcurl/libcurl-multi.md 的 BLOCKING 章节即使使用 multi 接口域名解析除非使用 c-ares 或线程解析后端、file://传输和 TELNET 传输仍可能阻塞设计事件循环时需要规避或接受这一限制使用完毕记得清理——每个 easy handle 需单独curl_easy_cleanup最后curl_multi_cleanup见 docs/libcurl/curl_multi_cleanup.md。十、结语curl_multi_fdset是理解 libcurl multi 接口工作原理的最佳切入点它表面上只是提取 fd_set背后却牵动着 easy handle 状态机、pollset 聚合、FD_SETSIZE容量守卫与超时协作等一整套机制。当你需要在 select() 风格的事件循环中接入 libcurl、与自己的文件描述符统一等待时它仍然是标准答案而当并发规模跨越 1024 描述符门槛时沿 docs/libcurl/curl_multi_wait.md 和 multi_socket 路线升级则是官方推荐的演进路径。把本文的示例循环跑通你就掌握了 libcurl 多路并发传输的底层驱动模型。【免费下载链接】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),仅供参考
返回列表