Mongoose 网络库之事件驱动核心设计

Mongoose 网络库之事件驱动核心设计
Mongoose 的整个运行时构建在单线程、非阻塞的事件循环基础之上——这种设计使其在裸机微控制器和完整的 POSIX 系统上均具备极高的适用性。本页将深入剖析该循环的三大支柱事件分发机制、定时器子系统以及将二者紧密相连的基于轮询的连接管理器。事件分发架构Mongoose 的核心在于一个单一的函数——mg_call()它作为库产生的所有事件的通用分发器。它在 src/event.c 中的实现异常简洁却编码了一个关键的设计决策在每次事件触发时协议处理函数优先于用户处理函数执行。第 19 行的注释明确引用了 issue #2559证实了这种顺序是刻意为之且经过实战检验的。其签名void mg_call(struct mg_connection *c, int ev, void *ev_data)接收一个连接指针、一个整型事件代码以及一个不透明数据指针其类型取决于具体事件。事件类型本身在 src/event.h 中被定义为一个普通的 C 枚举以MG_EV_ERROR值为 0开始以MG_EV_USER结束后者作为应用层自定义事件的起始 ID。该枚举刻意保持紧凑——仅用 23 个值便涵盖了从连接创建MG_EV_OPEN到数据传输MG_EV_READ、MG_EV_WRITE再到终止MG_EV_CLOSE的完整生命周期此外还包含了针对 HTTP、WebSocket、MQTT、SNTP、mDNS 的协议特定事件以及一种通用的唤醒机制。每个事件都携带明确记录的有效载荷类型例如MG_EV_READ传递long *bytes_readMG_EV_HTTP_MSG携带struct mg_http_message *等。配套的错误处理函数mg_error()位于 src/event.c展示了事件系统在连接生命周期管理中的作用。它将错误信息格式化到栈缓冲区中记录日志在分发MG_EV_ERROR之前设置c-is_closing 1然后调用mg_call()。这种顺序至关重要用户处理函数在接收到错误事件时连接已被标记为即将关闭但处理函数仍有机会检查状态并执行清理工作。第 31 行的注释明确指出了这一点——is_closing是在事件触发前刻意设置的。双处理函数模式协议与用户src/net.h 中的mg_connection结构体包含两个独立的处理函数指针fn用户处理函数和pfn协议处理函数。它们各自拥有对应的fn_data/pfn_data空指针用于传递上下文。之所以进行这种拆分是因为 Mongoose 需要同时支持原始连接和协议封装连接HTTP、MQTT、WebSocket、TLS。当 src/net.c 中的mg_connect_svc()创建一个感知协议的连接时它会同时设置c-fn用户回调和c-pfn协议的内部状态机。协议处理函数处理线缆级别的帧格式并发出更高级别的事件交由用户处理函数消费。这种架构意味着单个 TCP 连接在其生命周期内可以穿越多个协议层。一个以MG_EV_CONNECT和MG_EV_READ开头的原始 TCP 连接可以在传输中途升级为 WebSocket此时pfn会切换为 WebSocket 状态机用户处理函数开始接收MG_EV_WS_MSG而不再是原始字节。对于原始 TCP/UDP 连接pfn指针也可以为NULL此时mg_call()会完全跳过协议处理步骤。协议处理函数pfn始终在用户处理函数fn之前执行。这意味着当你的用户回调运行时协议级状态如 HTTP 解析或 MQTT 会话跟踪必定已经是最新的。绝不要做其他假设——如果你需要在处理函数中检查协议状态它此时必然是一致的。事件目录src/event.h 中完整的事件枚举揭示了一种分层设计。核心传输事件负责处理套接字的生命周期事件有效载荷触发条件MG_EV_ERRORchar *任何致命错误连接将关闭MG_EV_OPENNULL(客户端) / URL 字符串连接对象已创建MG_EV_POLLuint64_t *uptime_millis每次mg_mgr_poll()迭代MG_EV_RESOLVENULLDNS 解析完成MG_EV_CONNECTNULLTCP 连接已建立MG_EV_ACCEPTNULL监听器接受了新连接MG_EV_READlong *bytes_readc-recv中有可用数据MG_EV_WRITElong *bytes_written数据已从c-send刷新MG_EV_CLOSENULL连接正在被销毁MG_EV_WAKEUPstruct mg_str *通过mg_wakeup()发送了数据协议事件构建在这些传输原语之上。MG_EV_HTTP_MSG和MG_EV_HTTP_HDRS携带解析后的 HTTP 消息MG_EV_WS_OPEN、MG_EV_WS_MSG和MG_EV_WS_CTL代表 WebSocket 生命周期MG_EV_MQTT_CMD、MG_EV_MQTT_MSG和MG_EV_MQTT_OPEN覆盖了 MQTT 客户端状态机。MG_EV_USER哨兵值值为 23为应用程序定义自定义事件提供了一个稳定的基准偏移量。定时器子系统Mongoose 在 src/timer.c 和 src/timer.h 中的定时器实现刻意追求轻量级——这是一个通过指针的指针操作来管理的单链表。每个mg_timer结构体存储了以毫秒为单位的周期、过期时间戳、回调函数、用户参数、标志字段以及用于链表链接的next指针。src/timer.c 中的mg_timer_init()函数只需将新定时器 prepend 到链表头部使得注册操作的时间复杂度为 O(1)。src/timer.c 中mg_timer_expired()的过期逻辑以极其优雅的方式处理了三种边缘情况。首先如果now prd *t说明系统时钟发生了回绕或向后跳跃因此将过期时间重置为零。其次如果*t 0说明这是初始化后的首次轮询因此将过期时间设置为now prd。第三下一次过期时间的计算公式*t (now - *t) prd ? now prd : *t prd防止了定时器漂移如果回调执行时间超过了一个周期定时器会重新同步到当前时间而不是试图通过多次快速触发来“赶上进度”。src/timer.c 中的mg_timer_poll()函数遍历链表为每个定时器调用mg_timer_expired()并对已过期或需要立即调度的定时器执行回调。它还处理了MG_TIMER_AUTODELETE标志对于至少被调用过一次且携带自动删除标志的非重复定时器系统会自动将其从链表中释放。这种模式常用于一次性延迟操作调用者无需保留对定时器结构体的引用。标志值行为MG_TIMER_ONCE0触发一次默认MG_TIMER_REPEAT1周期性触发MG_TIMER_RUN_NOW2在首次轮询时立即触发MG_TIMER_CALLED4内部使用回调至少触发过一次MG_TIMER_AUTODELETE8一次性完成后释放定时器结构体连接管理器mg_mgrsrc/net.h 中的mg_mgr结构体是整个事件循环唯一的协调中心。它持有一个活跃连接链表conns、一个定时器链表timers、用于 IPv4 和 IPv6 的 DNS 客户端状态、用于 Linux 的 epoll 文件描述符、指向内置 TCP/IP 接口的指针在无操作系统环境下使用、用于跨线程唤醒的 socketpair 管道以及各种配置字段。src/net.c 中的mg_mgr_init()函数首先将结构体置零然后执行平台特定的设置在 Linux 上创建 epoll 实例在 FreeRTOS 上初始化套接字集在 POSIX 上忽略SIGPIPE信号或在裸机目标上调用驱动初始化宏。DNS 服务器默认设置为 Google 的8.8.8.8和2001:4860:4860::8888。src/net.c 中的mg_mgr_free()函数展示了有序的关闭序列它首先遍历定时器链表并释放所有定时器然后将每个连接标记为正在关闭接着以零超时调用mg_mgr_poll(mgr, 0)以刷新关闭事件。这确保了每个连接在内存被回收前都会接收到其MG_EV_CLOSE事件——mg_close_conn()中第 155 行的注释明确引用了 issue #1331指出MG_EV_CLOSE必须在接收到的数据被释放之前触发。连接生命周期事件每个连接从创建到销毁都会经历一个确定的事件序列。对于客户端连接src/net.c 中的mg_connect_svc()会分配一个mg_connection将其添加到管理器的链表中设置is_client true以 URL 作为ev_data触发MG_EV_OPEN然后启动异步 DNS 解析。对于服务端连接src/net.c 中的mg_listen()会分配连接打开一个监听套接字设置is_listening true将其添加到链表并以 NULL 有效载荷触发MG_EV_OPEN。src/net.h 中的mg_connection结构体对其状态标志采用了密集的位域布局——17 个独立的布尔标志被打包到无符号位域中。这些标志并非随意设置的状态位它们构成了一个状态机。is_resolving→is_connecting→is_tls_hs跟踪了客户端连接的握手进度。is_draining和is_closing提供了两种截然不同的关闭模式draining 会等待直到c-send被刷新而 closing 则是立即执行。is_full通过在接收缓冲区达到容量上限时暂停读取实现了 TCP 反压机制。src/net.c 中的mg_alloc_conn()函数使用mg_calloc(1, sizeof(*c) mgr-extraconnsize)来分配连接其中extraconnsize被内置 TCP/IP 栈用于追加特定于驱动的每连接数据。对recv、send和rtls这三个 iobuf 应用的MG_IO_SIZE对齐确保了为嵌入式网络硬件提供最佳的 DMA 对齐。mg_mgr_poll()事件循环声明于 src/net.h 并在平台特定网络层实现的mg_mgr_poll()函数是驱动一切的引擎。其签名void mg_mgr_poll(struct mg_mgr *mgr, int ms)接收一个以毫秒为单位的超时时间并在每次迭代中执行四个独立的阶段定时器轮询调用mg_timer_poll(mgr-timers, now_ms)触发所有已过期的定时器回调确保周期性任务和一次性延迟操作在 I/O 处理之前执行。I/O 多路复用使用平台原生机制Linux 上的 epoll其他 POSIX 系统上的 poll 或 selectRTOS 上的套接字事件在给定的超时时间内等待任意连接套接字描述符的可读或可写状态。连接处理对于每个就绪的连接执行相应的操作——在监听器上接受新连接、将数据读入c-recv并触发MG_EV_READ、从c-send写出数据并触发MG_EV_WRITE、完成非阻塞连接并触发MG_EV_CONNECT或处理接受错误并触发MG_EV_ERROR。清理阶段遍历所有连接关闭任何标记了is_closing或is_draining且发送缓冲区为空的 draining 连接的连接。关闭过程会触发MG_EV_CLOSE释放 TLS 状态释放 iobuf将结构体置零并释放内存。正是由于这种单函数循环Mongoose 不需要线程、不需要单独的读写任务也不需要外部事件库。整个系统——DNS 解析、TLS 握手、HTTP 解析、MQTT 保活、WebSocket 帧处理——全部馈入同一个轮询与分发周期。传递给mg_mgr_poll()的ms参数既是睡眠超时时间也是响应性控制参数。传入0会使循环以最大速度空转适用于关闭期间的排空处理而传入较大的值则能降低 CPU 占用。定时器子系统会自动调整实际超时时间使其不超过下一个定时器的过期时间因此你无需手动进行计算。无锁跨线程唤醒Mongoose 被设计为单线程运行但实际系统通常需要从其他线程注入数据GUI 回调、传感器中断、RTOS 任务。声明于 src/net.h 的mg_wakeup()机制通过向 socketpair 管道写入数据解决了这一问题。mg_mgr中的pipe字段保存了该管道对的一端另一端则注册到了 I/O 多路复用器中。当mg_wakeup()向管道写入数据时下一次mg_mgr_poll()迭代会检测到可读状态读取数据并向目标连接分发带有struct mg_str *有效载荷的MG_EV_WAKEUP事件。src/queue.c 中的底层队列实现使用编译器内存屏障MG_MEMORY_BARRIER()提供了一个无锁环形缓冲区。mg_queue_book()函数预留写入空间mg_queue_add()提交写入mg_queue_next()推进读取指针mg_queue_del()释放已读数据。这种模式允许不同线程中的生产者和消费者在无需互斥锁的情况下操作共享内存完全依赖于内存排序保证。数据流IO 缓冲区契约每个连接都携带三个在 src/net.h 中定义的mg_iobuf结构体recv传入的应用数据、send传出的应用数据和rtls等待 TLS 解密的传入加密数据。事件契约是明确无歧义的当MG_EV_READ触发时新接收的数据会被追加到c-recvlong *bytes_read有效载荷会告诉你添加了多少字节。你的处理函数从c-recv中处理数据通常使用协议解析函数来消耗缓冲区头部的字节。当MG_EV_WRITE触发时long *bytes_written会告诉你从c-send中刷新了多少字节。输出操作通过调用mg_printf()或mg_send()进行排队这些函数会向c-send追加数据。实际的套接字写入发生在mg_mgr_poll()内部——你永远不需要直接调用 write。这种排队与传输的解耦正是非阻塞模型得以实现的原因你的处理函数可以在单次MG_EV_HTTP_MSG回调中排队数兆字节的响应数据而轮询循环会在多次迭代中逐步排空缓冲区并且仅在c-send达到容量极限时才会施加反压。协议组合双处理函数模式意味着协议是通过链式调用来组合的。TLS 连接的pfn被设置为 TLS 状态机它将数据从c-rtls解密到c-recv并在传输前将c-send中的数据加密。HTTP 服务端连接的pfn被设置为 HTTP 解析器它从c-recv中积累数据解析出完整的消息并触发MG_EV_HTTP_MSG。建立在 HTTP 之上的 WebSocket 连接将 WebSocket 处理函数设置为pfn由它管理帧格式并触发MG_EV_WS_MSG。每一层只能看到来自其下层的事件和数据。src/net.c 中的mg_connect_svc()函数是协议组合的入口点。虽然mg_connect()会为pfn和pfn_data传递NULL产生原始连接但感知协议的函数如mg_ws_connect()或mg_mqtt_connect()会在调用mg_connect_svc()时预装其协议处理函数。这意味着你的用户处理函数看到的第一个事件已经是经过协议过滤的——无需手动编写升级逻辑。