十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

libcurl 多接口事件驱动机制内幕:解析 Multi Event Based(MULTI-EV)架构

libcurl 多接口事件驱动机制内幕:解析 Multi Event Based(MULTI-EV)架构 libcurl 多接口事件驱动机制内幕解析 Multi Event BasedMULTI-EV架构【免费下载链接】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/curllibcurl 的 multi 接口支持事件驱动event based模式应用程序借助libuv、libevent等事件库监听 libcurl 使用的 socket 与文件描述符从而以极低开销驱动大量并发传输。本文基于 docs/internals/MULTI-EV.md 展开逐层剖析 multi 事件驱动的内部实现——从lib/multi_ev.c与lib/multi_ev.h的职责划分、socket 变更追踪的数据结构到事件回流的处理路径与资源清理时序帮助读者真正理解curl_multi_socket_action()背后发生了什么以及为何 socket 复用会成为事件追踪的关键难点。事件驱动模式的两层视角从应用视角看事件驱动模式并不复杂应用把 libcurl 关心的 socket 注册到事件库如 libuv当 socket 上有可读/可写事件时事件库回调应用应用再调用curl_multi_socket_action()通知 libcurl 处理。这套 API 的使用方式由 libcurl-multi(3) 手册描述对应仓库中的 docs/libcurl/curl_multi_socket_action.md 等文档。本文关注的是另一层——libcurl 内部如何处理这些事件。所有与事件驱动相关的代码集中在两个文件中lib/multi_ev.c事件追踪与差分计算的具体实现lib/multi_ev.h对外暴露一组内部函数并定义内嵌于每个 multi 句柄的struct curl_multi_ev结构体。struct curl_multi_ev的完整定义见 lib/multi_ev.h它只包含一个哈希表sh_entries以 socket 为键保存每个 socket 的追踪状态struct curl_multi_ev { struct Curl_hash sh_entries; };这个结构体作为字段ev内嵌在struct Curl_multi中见 lib/multihandle.h随 multi 句柄的创建与销毁而初始化与释放。生命周期由两个函数管理Curl_multi_ev_init()在 multi 句柄创建时初始化事件簿记哈希调用点位于 lib/multi.cCurl_multi_ev_cleanup()在 multi 句柄销毁时释放哈希调用点位于 lib/multi.c 与 lib/multi.c。事件追踪的前提socket 回调必须已注册lib/multi_ev.h中声明的众多函数只有在应用通过curl_multi_setopt(CURLMOPT_SOCKETFUNCTION, ...)注册了multi-socket_cb回调后才会真正工作。这是理解整个模块的关键前提。原因在于multi-socket_cb是 libcurl 与应用之间唯一的事件通告通道。每当发生以下变化时libcurl 都必须调用它新增了一个 socket移除了一个现存 socket某个 socket 上的POLLIN/POLLOUT关注标志发生了变化。因此multi_ev必须记录上一次已经向应用报告过什么才能检测出现在有什么变化。在 lib/multi_ev.c 的mev_assess()中可以看到这个前提被显式检查static CURLMcode mev_assess(struct Curl_multi *multi, struct Curl_easy *data, struct connectdata *conn) { struct easy_pollset ps, *last_ps; CURLMcode mresult CURLM_OK; if(!multi || !multi-socket_cb) return CURLM_OK; ... }值得注意的是虽然大多数应用从一开始就采用事件驱动模式但 libcurl 的 API 并不禁止应用先以其他方式如curl_multi_perform()启动中途再切换到事件驱动——甚至可以在一次传输进行到一半时切换。multi_ev的差分机制必须能优雅应对这种状态迁移。传输事件pollset 的获取与差分传输transfer相关的事件占据了绝大多数场景一次传输要打开连接、连接打开 socket例如使用 TCP 时等待 socket 变为可写POLLOUT。这时 multi 会调用Curl_multi_ev_assess_xfer(multi, data)声明见 lib/multi_ev.h让事件代码检测该传输对哪些 socket 感兴趣。mev_assess()的处理流程lib/multi_ev.c如下通过Curl_multi_pollset()获取传输当前的 pollset即当前关心的 socket 及各自需要的 IN/OUT 标志通过Curl_meta_get(data, CURL_META_MEV_POLLSET)取出上一次保存的 pollset调用mev_pollset_diff()对前后两个 pollset 做差分lib/multi_ev.c检测出三类变化socket 在当前集合中但不在上一次集合中新增关注socket 两次都在但 IN/OUT 标志发生了变化关注类型变更socket 在上一次集合中但已不在当前集合中停止关注。差分结束后Curl_pollset_move(prev_ps, ps)把本次 pollset 保存为新的上一次 pollset供下次比对。mev_sh_entry每个 socket 的追踪状态multi_ev.c为每个 socket 在哈希sh_entries中维护一个struct mev_sh_entry定义见 lib/multi_ev.cstruct mev_sh_entry { struct uint32_spbset xfers; /* bitset of transfers mids on this socket */ struct connectdata *conn; /* connection using this socket or NULL */ void *user_data; /* libcurl app data via curl_multi_assign() */ unsigned int action; /* CURL_POLL_IN/CURL_POLL_OUT we last told the * libcurl application to watch out for */ unsigned int readers; /* this many transfers want to read */ unsigned int writers; /* this many transfers want to write */ ... BIT(announced); /* this socket has been passed to the socket callback at least once */ };这个条目记录的关键信息包括哪些传输以data-mid标识保存在uint32_spbset位集中对这个 socket 感兴趣有多少个传输想读readers、多少个想写writers上一次通告给应用的汇总动作actionCURL_POLL_IN/CURL_POLL_OUT组合是否已经通告过announced。为什么需要按传输计数而不是简单布尔值因为同一个 socket 可能同时被多个传输使用——典型例子是 HTTP/2 在同一连接上的多路复用。当一个传输结束并从 socket 条目中移除时它会使readers/writers计数减一而这可能导致条目的汇总动作发生变化也可能不变还有其他传输仍需要该方向。因此只有基于计数才能真正决定是否值得再次调用multi-socket_cb。mev_sh_entry_update()lib/multi_ev.c实现这个聚合逻辑先更新单个传输在读写计数上的增减再根据writers/readers是否非零合成comboaction只有当汇总动作与上次通告给应用的动作不同时才触发回调。代码中的注释还揭示了一个工程细节由于curl_easy_pause()被允许在任意回调中调用它可能重入mev_assess()并释放当前entry因此回调返回后需要重新从哈希中取回 entry再更新其状态。回调失败与清理差分过程中如果发现某个 socket 已无任何传输或连接使用mev_sh_entry_user_count() 0会调用mev_forget_socket()lib/multi_ev.c做清理从各 pollset 中移除该 socket若该 socket 曾通告给应用announced为真则调用multi-socket_cb并传入CURL_POLL_REMOVE最后从哈希中删除条目。若回调返回 -1则标记multi-dead TRUE并返回CURLM_ABORTED_BY_CALLBACK即回调有权中止整个 multi 的操作。连接事件内部句柄与连接级 pollset并非所有事件都与某个传输绑定。multi 的连接缓存关注连接关闭shutdown期间的 socket 事件以保证连接能够干净地关闭。关键设计是连接缓存借助一个内部 easy 句柄internal easy handle来复用 libcurl 的基础设施。这个内部句柄本身并不是一次传输它仅在很短的时间内与某个特定连接绑定用于完成该连接的所有关闭操作。因此为内部句柄记录上一次 pollset是没有意义的——句柄与连接的绑定关系转瞬即逝。解决办法是让连接缓存调用Curl_multi_ev_assess_conn()声明见 lib/multi_ev.h实现见 lib/multi_ev.c由事件处理代码针对连接自身进行检查并单独记录一份连接级的上一次 pollset。这份 pollset 通过连接元数据键CURL_META_MEV_POLLSET定义于 lib/multi_ev.h保存在连接上与传输级 pollset 互不干扰。从调用链上看连接缓存中的关闭流程位于 lib/cshutdn.c在 lib/cshutdn.c 中内部句柄multi-admin先Curl_attach_connection()挂接连接再调用Curl_multi_ev_assess_conn()完成连接级事件评估随后立即Curl_detach_connection()解除绑定而在连接真正被释放前lib/cshutdn.c会调用Curl_multi_ev_conn_done()清理连接的事件追踪。事件处理从 socket 事件到传输执行当事件库告知应用某个 socket 上有事件时应用调用curl_multi_socket_action()。该 API 的入口实现位于 lib/multi.c它内部转发到静态函数multi_socket()lib/multi.c。multi_socket()的核心处理路径对应文档中Event Processing一节如下若s ! CURL_SOCKET_TIMEOUT调用Curl_multi_ev_dirty_xfers(multi, s)把所有对该 socket 感兴趣的传输标记为 dirtylib/multi.c通过multi_mark_expired_as_dirty()把已到期的传输也标记为 dirty调用multi_run_dirty()运行所有 dirty 传输必要时再次运行处理运行期间新到期者最后更新定时器。注文档中提到的内部函数名为Curl_multi_ev_expire_xfers()expire所有对该 socket 感兴趣的传输并返回bool告知连接池是否也需处理在当前仓库版本中该职责已由Curl_multi_ev_dirty_xfers()承担——它通过 lib/multi_ev.c 的实现把 socket 条目上所有相关传输标记为 dirty且如果条目上还有连接entry-conn则连同内部句柄multi-admin一起标记等价于通知连接池。阅读源码时以Curl_multi_ev_dirty_xfers()为准。Curl_multi_ev_dirty_xfers()还有一处值得注意的容错逻辑注释指出在真实世界的测试中libevent 等事件库可能给出已经被要求移除的 socket 上的事件lib/multi_ev.c因此对哈希中不存在的 socket它选择忽略而不是报错——这正是对迟到事件的防御。万物皆会消逝三类清理与无序性文档All Things Pass一节描述了三个清理时机当前实现与文档一一对应传输结束传输从 multi 句柄移除时调用Curl_multi_ev_xfer_done()lib/multi_ev.h。实现见 lib/multi_ev.c先做一次最终评估确保把传输最后的兴趣变化通知出去再删除传输上的CURL_META_MEV_POLLSET元数据。调用点位于 lib/multi.c且调用前传输已被Curl_detach_connection()分离。连接销毁连接被销毁前调用Curl_multi_ev_conn_done()lib/multi_ev.h清理连接级 pollset 追踪调用点在 lib/cshutdn.c。socket 关闭socket 即将关闭时调用Curl_multi_ev_socket_done()lib/multi_ev.h通过mev_forget_socket()清理 socket 条目及其中保存的全部信息。调用点位于 lib/multi.c 的Curl_multi_will_close()中。这三个调用不要求按特定顺序发生。传输进行期间它的 socket 可能一直存在也可能在传输中途就消失同时一个传输可能在同一时刻关心多个 socket——域名解析、Happy Eyeballs 双栈连接探测eye balling、FTP 的多连接场景都是典型例子。multi_ev的数据结构必须能容忍这些交错的生命周期。Come Againsocket 标识符复用难题文档最后一节Come Again点出了事件追踪中最微妙的问题传输和连接的标识符在 libcurl 应用中几乎唯一但 socket 标识符文件描述符数值不是。操作系统热衷于复用资源——刚关闭的 socket其标识符很快就会被分配给下一个新建 socket。这意味着 multi 事件处理必须在 socket 关闭之前被通知即Curl_multi_ev_socket_done()的调用时机完成所有追踪信息的清理并准备好迎接同一个 socket 标识符立刻再次出现的情况。如果清理不及时新 socket 可能会被误认为旧 socket从而向应用报告错误的事件。这一设计也解释了mev_pollset_diff()中的一处细节当传输首次出现在某个 socket 上first_time时会忽略last_poll中记录的旧动作并重置为 0lib/multi_ev.c因为该 socket 可能已被销毁并重新打开——尽管sh_entry已被清除但哈希化的 pollset 中可能仍残留旧引用必须以全新状态对待。小结一条贯穿始终的差分主线把整个机制串起来可以看到一条清晰的主线应用注册CURLMOPT_SOCKETFUNCTION回调后multi 进入事件驱动模式每次传输或连接的兴趣集合变化时multi_ev通过 pollset 差分识别出新增/变更/移除三类变化以按传输计数的读写聚合决定是否通告应用socket 事件到达时应用调用curl_multi_socket_action()内部把相关传输标记为 dirty 并运行传输、连接、socket 各自结束时都有对应的*_done()函数清理追踪状态且不要求顺序由于操作系统会复用 socket 标识符所有清理必须在 socket 关闭前完成并准备好接受同一标识符的轮回。这套机制使得 libcurl 的事件驱动模式既能支撑 HTTP/2 多路复用这种一 socket 多传输的高密度场景又能容忍应用在传输中途切换事件模式、事件库投递迟到事件等现实世界的各种边角情况。想要深入阅读实现细节的读者可以重点查看 lib/multi_ev.c、lib/multi_ev.h 这两个核心文件以及 lib/multi.c 中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创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表