1. 从 mg_http_listen 到 mg_mgr_poll一个单线程 HTTP 服务的骨架长什么样mongoose 内置的 HTTP server 是很多嵌入式项目和轻量级 C 服务的第一选择原因很直接整个网络库只有 mongoose.c 和 mongoose.h 两个文件拖进工程就能编译不依赖一堆第三方库。它用单线程加非阻塞 socket 的方式跑事件循环在资源受限的板子上也能撑起一个可用的 HTTP 服务。如果你正在找 mg_http_listen 怎么注册监听、mg_mgr_poll 到底在循环里干了什么、epoll 事件是怎么被翻译成 is_readable / is_writable 标志位的这篇就按这条链路走一遍。适合的读者是写过 C、想搞明白 mongoose HTTP server 启动流程的人被 mg_mgr_poll 里那一大坨 if-else 绕晕的人以及想自己搭一个最小可运行骨架、再往上加路由和业务逻辑的人。下面给出一份可以直接复制编译的 C 骨架然后逐段拆开讲 mg_http_listen 注册监听、mg_mgr_poll 驱动 epoll 事件循环这两步到底发生了什么。整个过程不涉及任何网络环境配置纯本地编译运行。2. 前置准备拿到 mongoose 两个文件并确认编译环境mongoose 的获取方式很朴素官方仓库里就是 mongoose.c 和 mongoose.h 两个文件把它们放到你的工程目录即可。不需要包管理器不需要 CMake 的复杂配置一个 gcc 命令就能编。我试过在 Linux 上直接用系统自带的 gcc 编译唯一要注意的是链接时带上 -lpthread因为 mongoose 内部会用到线程相关的符号即使你的 HTTP server 是单线程跑的。另外如果你用的是较新的内核epoll 是默认开启的mongoose 会走 epoll 分支而不是 select 分支这也是本文关注的重点。注意mongoose 的版本更新比较频繁不同版本里 mg_mgr 结构体的字段可能有增减。本文基于常见的 7.x 版本如果你用的是更早的 6.x部分字段名会不一样但 mg_http_listen 和 mg_mgr_poll 这两个核心 API 的用法基本稳定。如果你后续要把这套骨架接到真实的模型服务或编码 Agent 上做联调可以先把 API Key 和接入文档准备好方便本地服务起来之后直接对接。API Keys 管理入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contenthttp_server_skeletonutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contenthttp_server_skeletonutm_campaignrewrite 需要看模型对话效果的话模型对话页在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contenthttp_server_skeletonutm_campaignrewrite 。这些是后面联调阶段的事现在先把本地 HTTP 骨架跑通。3. 可复制配置最小 HTTP server 骨架与编译命令先上完整代码。这份骨架只做一件事监听 8000 端口收到任何请求都回一句 hello。它的价值在于把 mg_http_listen 和 mg_mgr_poll 的调用位置固定下来你后面加路由、加 JSON 响应、加文件服务都是在这个骨架上改。#include mongoose.h static void fn(struct mg_connection *c, int ev, void *ev_data) { if (ev MG_EV_HTTP_MSG) { struct mg_http_message *hm (struct mg_http_message *) ev_data; // 打印请求方法和 URI方便观察事件触发顺序 MG_INFO((request: %.*s %.*s, (int) hm-method.len, hm-method.ptr, (int) hm-uri.len, hm-uri.ptr)); mg_http_reply(c, 200, Content-Type: text/plain\r\n, hello from mongoose\n); } } int main(void) { struct mg_mgr mgr; mg_mgr_init(mgr); // 初始化事件管理器内部会创建 epoll_fd // 注册监听绑定 8000 端口回调统一走 fn struct mg_connection *c mg_http_listen(mgr, http://0.0.0.0:8000, fn, NULL); if (c NULL) { MG_ERROR((listen failed)); return 1; } // 事件循环1000ms 超时驱动 epoll_wait 和连接状态机 for (;;) { mg_mgr_poll(mgr, 1000); } mg_mgr_free(mgr); return 0; }编译命令gcc -o http_server http_server.c mongoose.c -lpthread -DMG_ENABLE_LOG1这里 -DMG_ENABLE_LOG1 是为了打开日志方便你看到 MG_INFO 的输出。如果你不想看日志去掉这个宏定义即可。运行./http_server然后在另一个终端里curl -v http://127.0.0.1:8000/hello你应该能看到 curl 返回 hello from mongoose同时服务端终端打印出 request: GET /hello。这一步跑通说明 mg_http_listen 注册监听和 mg_mgr_poll 驱动事件循环这条链路是通的。4. mg_http_listen 内部socket、bind、listen 到 epoll 注册mg_http_listen 的签名是struct mg_connection *mg_http_listen(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data);它做的事情可以拆成四步。第一步是解析 url从 http://0.0.0.0:8000 里提取出地址和端口。第二步是创建 socket调用 socket() 拿到一个 fd。第三步是 setsockopt 设置 SO_REUSEADDR然后 bind 到指定地址再 listenbacklog 传的是 128。第四步最关键把监听 fd 设置成非阻塞模式然后通过 MG_EPOLL_ADD 宏把这个 fd 注册到 mgr-epoll_fd 上。MG_EPOLL_ADD 宏展开后是这样的#define MG_EPOLL_ADD(c) \ do { \ struct epoll_event ev {EPOLLIN | EPOLLERR | EPOLLHUP, {c}}; \ epoll_ctl(c-mgr-epoll_fd, EPOLL_CTL_ADD, (int) (size_t) c-fd, ev); \ } while (0)注意这里注册的事件是 EPOLLIN | EPOLLERR | EPOLLHUP而且是水平触发模式没有加 EPOLLET。这意味着只要监听 fd 上有新连接进来epoll_wait 就会持续通知直到你调用 accept 把连接取走。这个设计对新手比较友好不容易因为漏读而丢事件。同时mg_http_listen 会为这个监听 fd 分配一个 struct mg_connection把 is_listening 标志位置 1然后把它挂到 mgr-conns 这个单向链表的头部。所以 mgr-conns 链表里既有监听连接也有后续 accept 出来的客户端连接mg_mgr_poll 遍历链表时靠 is_listening 这个位来区分。这里有个容易忽略的点监听 fd 对应的 mg_connection 的 pfn 字段被设置成了 http_cb这是协议处理函数。后续 accept 出来的客户端连接会继承这个 pfn所以客户端连接上的数据会被 http_cb 解析成 HTTP 报文。5. mg_mgr_poll 内部epoll_wait 与连接状态机mg_mgr_poll 是整个事件循环的心脏。它的结构可以概括为先调 mg_iotest 做一次 epoll_wait 并把结果翻译成每个连接的 is_readable / is_writable 标志位然后遍历 mgr-conns 链表根据标志位和连接状态分派到 accept_conn、read_conn、write_conn 等处理函数。mg_iotest 里先遍历一遍链表把每个连接的 is_readable 和 is_writable 清零同时统计链表长度 max。然后如果某个连接的 send.len 0说明有数据要发就调 MG_EPOLL_MOD 把 EPOLLOUT 加上去。接着用 alloca 在栈上分配一个 max 大小的 epoll_event 数组调用 epoll_wait。epoll_wait 返回后遍历返回的事件数组对每个事件做判断如果是 EPOLLERR调 mg_error 标记连接出错否则根据事件类型设置 is_readable 和 is_writable。这里有个细节can_read 和 can_write 会检查连接是否处于可读可写状态比如 is_closing 的连接就不再标记可读。回到 mg_mgr_poll 的主循环遍历链表时按优先级处理for (c mgr-conns; c ! NULL; c tmp) { tmp c-next; if (c-is_resolving || c-is_closing) { // 跳过 } else if (c-is_listening c-is_udp 0) { if (c-is_readable) accept_conn(mgr, c); } else if (c-is_connecting) { if (c-is_readable || c-is_writable) connect_conn(c); } else { if (c-is_readable) read_conn(c); if (c-is_writable) write_conn(c); } if (c-is_draining c-send.len 0) c-is_closing 1; if (c-is_closing) close_conn(c); }监听连接走 accept_conn 分支。accept_conn 调用 accept 拿到客户端 fd为它分配一个新的 mg_connection设置 is_accepted 1继承监听连接的 pfn 和 fn把新连接加到链表头部然后 MG_EPOLL_ADD 注册到 epoll。注意 accept 出来的 fd 也被设置成非阻塞。客户端连接走 read_conn 和 write_conn。read_conn 调用 recv 把数据读到 c-recv.buf然后调 iolog 处理返回值。如果 recv 返回 EWOULDBLOCK 就什么都不做返回 0 或负数就标记 is_closing返回正数就调 mg_call(c, MG_EV_READ, n)这会触发 http_cb 解析 HTTP 报文。http_cb 解析完成后调 mg_call(c, MG_EV_HTTP_MSG, hm)把解析结果交给你的 fn 回调。你的 fn 里调 mg_http_reply 把响应写进 c-send.buf。write_conn 调用 send 把 c-send.buf 发出去发完后如果 send.len 变成 0就调 MG_EPOLL_MOD(c, 0) 把 EPOLLOUT 去掉避免 epoll 一直通知可写。整个流程下来一个请求的生命周期是epoll_wait 通知监听 fd 可读 → accept_conn 建立客户端连接 → 下一轮 epoll_wait 通知客户端 fd 可读 → read_conn 读数据 → http_cb 解析 → fn 回调生成响应 → epoll_wait 通知客户端 fd 可写 → write_conn 发数据 → 响应发完连接保持或关闭。6. 验证请求与观察事件顺序跑起来之后用 curl 发几个请求观察服务端日志。你会看到类似这样的输出1234 2 accept 127.0.0.1:54321 - 0.0.0.0:8000 1235 3 request: GET /hello第一行是 accept_conn 里的 MG_DEBUG 打的第二行是你 fn 里的 MG_INFO。这说明事件顺序是 accept 先于 read符合预期。再试一个带 body 的 POSTcurl -X POST -d {name:test} http://127.0.0.1:8000/api服务端会打印 request: POST /api你的 fn 里可以通过 hm-body 拿到请求体。如果想验证并发可以用 ab 或 wrk 压一下ab -n 1000 -c 50 http://127.0.0.1:8000/观察服务端是否稳定有没有连接泄漏。mongoose 单线程处理50 并发下应该能正常跑完但如果你的 fn 里有阻塞操作吞吐会明显下降因为整个事件循环被卡住了。7. 本篇常见错排查第一个常见错是编译时报 undefined reference topthread_create这是因为没加 -lpthread。mongoose 内部有些地方会用到线程即使你的 HTTP server 是单线程跑的链接时也得带上。第二个错是运行时报 listen failed。先检查端口是不是被占用了用 ss -tlnp | grep 8000 看一下。如果端口没被占检查 url 格式必须是 http://0.0.0.0:8000 这种带协议前缀的形式写成 0.0.0.0:8000 会解析失败。第三个错是 curl 能连上但一直没响应。这通常是 fn 回调里没调 mg_http_reply或者调了但 Content-Length 和实际 body 长度不一致。mongoose 的 mg_http_reply 会自动算 Content-Length但如果你手动拼响应头就得自己保证长度正确。如果 Content-Length 写大了客户端会一直等剩余数据写小了客户端会截断。第四个错是关于 is_closing 何时被置 1 的疑问。在正常的一问一答场景里客户端发一次、服务端回一次连接不会立刻关闭is_closing 保持 0。只有当客户端主动断开触发 EPOLLHUP或者服务端在 fn 里显式设置 c-is_closing 1或者 recv 返回 0对端关闭才会进入关闭流程。如果你在 fn 里调了 mg_http_reply 之后又调 mg_printf 追加数据但没更新 Content-Length客户端可能收不全数据因为 Content-Length 决定了客户端读多少字节。要追加数据得用 mg_http_reply 一次性生成或者手动管理 Content-Length。第五个错是压测时出现 EPOLLERR。这通常和 send.len 与 Content-Length 不匹配有关也可能是对端提前关闭了连接。排查时打开 MG_ENABLE_LOG看日志里有没有 socket error 的输出结合请求内容定位。8. 把骨架接到真实服务上本地骨架跑通之后下一步通常是让它对接真实的模型服务或编码 Agent。这时候你需要一个稳定的 API 入口把 HTTP 请求转发过去。API 接入地址是 https://taotoken.net/api 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contenthttp_server_skeletonutm_campaignrewrite API Keys 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contenthttp_server_skeletonutm_campaignrewrite 。如果你要长期跑编码类 Agent可以看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contenthttp_server_skeletonutm_campaignrewrite 模型对话验证在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contenthttp_server_skeletonutm_campaignrewrite 。回到 mongoose 本身这个骨架的扩展点很清晰在 fn 回调里根据 hm-uri 做路由分发用 mg_http_reply 返回 JSON或者用 mg_http_serve_dir 直接服务静态文件。事件循环那部分不用动mg_mgr_poll 会一直帮你驱动 epoll。唯一要记住的是fn 回调里不要做阻塞操作否则整个单线程事件循环都会被卡住这是 mongoose 单线程模型的代价也是它轻量的原因。
企业数字化 ERP 产品动态
相关推荐
3步搞定!为什么我自己做的网站百度不到?保姆级建站教程 3步搞定!为什么我自己做的网站百度不到?保姆级建站教程 找建站公司怕被坑高价,自己动手又搞不定技术?别慌。很多运营朋友在湖北甚至全国都遇到过这个尴尬:花几千块做的官网,上线半年,百度搜公司名还是搜不到自家站,或者排在第8页。… · 2026/9/27 16:48:43
5类大学生网页设计作业成品下载渠道避坑指南 5类大学生网页设计作业成品下载渠道避坑指南 自己不会代码,却要在下周交出一个能跑的网页?这种焦虑我太懂了。别急着在百度搜一堆全是广告的下载站,那些所谓的“免费源码”往往藏着恶意脚本或根本打不开。这篇 避坑指南… · 2026/9/27 16:48:43
3步搞定wordpress怎么静态页面,保姆级建站教程 3步搞定wordpress怎么静态页面,保姆级建站教程 找建站公司怕被坑高价?别再花冤枉钱听那些“高端定制”的鬼话了。其实很多看似复杂的静态化需求,你自己动手半小时就能搞定,这才是真正靠谱的 保姆级建站教程 。… · 2026/9/27 16:48:30
MCP 协议版本演进与兼容性管理:用 TaoToken 统一 Key 打通多版本工具链 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 17:35:22
闲鱼卖源码一单2000+,TaoToken统一Key接入Cline配置实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 17:35:16
选对轻量级wordpress主题 官网部署完整流程不花冤枉钱 选对轻量级wordpress主题 官网部署完整流程不花冤枉钱 找建站公司报价三万五,回头自己折腾才花两千?这行水深,很多老板第一次搞官网,生怕被坑高价,又怕自己搞不定技术细节。别慌,今天不画大饼,直接上干货。咱们把… · 2026/9/27 17:35:16
无锡网站建设开发避坑:3个源码下载细节决定排名生死 无锡网站建设开发避坑:3个源码下载细节决定排名生死 很多做企业官网的老板,第一反应往往是“找个懂代码的人,或者去GitHub上把源码下载下来自己改”。这思路没错,但如果你只是把开源CMS的源码下载下来,直接丢到服务器上,那离SEO成功还差得… · 2026/9/27 17:35:04
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现 简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01
汕头网站建设制作厂家避坑指南:5大注意事项救急 汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习 简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现 简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01
汕头网站建设制作厂家避坑指南:5大注意事项救急 汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习 简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01