
1. 为什么要在 mongoose 里手搓一个 SSE 服务Server-Sent EventsSSE是一种基于 HTTP 的单向推送机制服务端声明Content-Type: text/event-stream后连接就变成一条持续的数据流浏览器或 curl 会一直挂着等新消息。它比 WebSocket 轻不用握手升级协议天然带断线重连语义特别适合日志推送、任务进度、AI 流式输出这类“服务端说、客户端听”的场景。mongoose 是一个 C/C 的网络库用事件驱动 非阻塞的方式把 TCP、UDP、HTTP、WebSocket、MQTT 都封装好了能在 Windows、Linux、Mac 以及 STM32、ESP32 这类嵌入式平台上跑甚至在国际空间站上都有它的身影。它的 HTTP 层足够薄我们可以直接拿到连接对象手动写响应头、手动往连接里灌数据这正是实现 SSE 需要的自由度。这篇要解决的问题很具体用 mongoose 搭一个 SSE 长连接服务把路由注册、事件流响应头、心跳保活这三件事配好再用 curl 验证事件流真的在推。目标是在本地跑通一条稳定的事件推送链路而不是停留在“知道 SSE 是什么”的层面。适合已经会用 mongoose 起 HTTP 服务、想加实时推送能力的同学也适合在嵌入式设备上做状态上报的开发者。2. 前置准备mongoose 工程与 TaoToken 通道先把 mongoose 拉下来。它把源码压成两个文件直接丢进工程就能编译不需要复杂的构建系统git clone https://github.com/cesanta/mongoose.git ls mongoose/mongoose.c mongoose/mongoose.h编译时把mongoose.c一起编进去即可Linux 下大概是这样gcc -o sse_server sse_server.c mongoose/mongoose.c -I mongoose -lpthread如果你在调试阶段想让 AI 工具帮你读代码、生成测试脚本、分析 curl 输出可以用 TaoToken 把模型调用统一到一个 Key 和一条 API 通道上省得每个工具单独配一遍。它的 API 地址是https://taotoken.net/api控制台里创建 Key 后在工具的 base_url 配置里填这个地址就行。模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。这一步不是必须的但调试 SSE 时让 AI 帮你解释事件流格式、写验证脚本会快很多。注意TaoToken 只是统一的模型调用通道不参与你的 SSE 服务运行别把它写进服务端逻辑里。3. 可复制的 mongoose SSE 路由骨架核心思路分三步在 HTTP 消息事件里识别/sse路由给这条连接打标记并写事件流响应头用一个定时器周期性遍历所有被标记的连接往里写data:帧心跳帧和业务帧走同一个发送函数保证连接不被中间层掐断。先看路由注册和响应头。mongoose 的 HTTP 回调里MG_EV_HTTP_MSG事件带着mg_http_message用mg_match匹配 URI#include mongoose.h static const char *s_listen_addr http://0.0.0.0:8000; // 标记连接类型E 表示 SSE 事件流 static void cb(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; if (mg_match(hm-uri, mg_str(/sse), NULL)) { c-data[0] E; // 打标记后续定时器只处理这类连接 mg_printf(c, HTTP/1.1 200 OK\r\n Cache-Control: no-cache\r\n Connection: keep-alive\r\n Access-Control-Allow-Origin: *\r\n Content-Type: text/event-stream\r\n \r\n); return; } // 其他路由走静态文件 struct mg_http_serve_opts opts {.root_dir web_root}; mg_http_serve_dir(c, ev_data, opts); } }几个响应头的作用要记牢Content-Type: text/event-stream是 SSE 的身份证缺了浏览器不会按事件流解析Cache-Control: no-cache防止中间层缓存Connection: keep-alive保持长连接Access-Control-Allow-Origin: *让跨域的前端页面也能订阅。这里用HTTP/1.1而不是HTTP/1.0因为 1.1 默认支持持久连接配合 chunked 传输更自然。接下来是发送函数和心跳。SSE 的帧格式很简单每条消息以data:开头以两个换行结束。心跳可以发一个注释帧: ping客户端会忽略内容但连接保持活跃// 向单个 SSE 连接发送一帧事件 static void sse_send(struct mg_connection *c, const char *event, const char *data) { if (event ! NULL) { mg_printf(c, event: %s\r\n, event); } mg_printf(c, data: %s\r\n\r\n, data); } // 心跳注释帧客户端不解析内容只维持连接 static void sse_heartbeat(struct mg_connection *c) { mg_printf(c, : ping\r\n\r\n); }定时器回调负责遍历连接、发心跳和业务数据。mongoose 的连接是链表mgr-conns是头指针static void timer_callback(void *arg) { struct mg_mgr *mgr (struct mg_mgr *) arg; static int tick 0; tick; for (struct mg_connection *c mgr-conns; c ! NULL; c c-next) { if (c-data[0] ! E) continue; // 只处理 SSE 连接 // 每 15 秒发一次心跳防止连接被中间设备回收 if (tick % 15 0) { sse_heartbeat(c); } // 每 2 秒推一条业务事件 if (tick % 2 0) { char buf[64]; mg_snprintf(buf, sizeof(buf), {\tick\:%d}, tick); sse_send(c, tick, buf); } } }主函数把管理器、监听、定时器串起来。定时器周期设成 1000 毫秒这样tick的计数单位就是秒int main(void) { struct mg_mgr mgr; mg_mgr_init(mgr); mg_http_listen(mgr, s_listen_addr, cb, NULL); mg_timer_add(mgr, 1000, MG_TIMER_REPEAT, timer_callback, mgr); MG_INFO((Mongoose v%s listening on %s, MG_VERSION, s_listen_addr)); for (;;) { mg_mgr_poll(mgr, 50); } mg_mgr_free(mgr); return 0; }这里有个容易踩的坑c-data[0]是 mongoose 给每个连接预留的用户数据区用它打标记最省事但别在别处覆盖它。如果你还要存别的状态用c-data[1]往后排或者自己挂一个结构体指针。4. 验证事件流curl 与浏览器双通道服务编译跑起来后先确认端口在听./sse_server # 输出类似Mongoose v7.x listening on http://0.0.0.0:8000用 curl 验证是最直接的方式。SSE 是流式响应curl 默认会一直挂着加-N关闭缓冲让输出实时刷出来curl -N http://127.0.0.1:8000/sse正常的话你会看到类似这样的输出每 2 秒来一条tick事件每 15 秒夹一条注释心跳event: tick data: {tick:2} event: tick data: {tick:4} : ping event: tick data: {tick:6}想看得更清楚可以加上响应头打印确认Content-Type真的是text/event-streamcurl -N -i http://127.0.0.1:8000/sse输出开头应该能看到HTTP/1.1 200 OK Cache-Control: no-cache Connection: keep-alive Access-Control-Allow-Origin: * Content-Type: text/event-stream浏览器端验证更贴近真实使用。写一个最小 HTML用EventSource订阅打开控制台看日志!DOCTYPE html html body script const es new EventSource(http://127.0.0.1:8000/sse); es.addEventListener(tick, (e) { console.log(收到 tick:, e.data); }); es.onerror (e) { console.log(连接异常浏览器会自动重连, e); }; /script /body /htmlEventSource自带重连连接断了会按默认间隔重试这也是 SSE 比手写轮询省心的地方。如果你在调试时想让 AI 帮你分析 curl 输出里的帧格式对不对可以把输出贴到模型对话里问走 TaoToken 的通道就行。5. 本篇常见错排查curl 一直没输出卡住不动。先确认响应头里Content-Type是text/event-stream如果写成了text/plain或漏了curl 不会按流处理。再检查mg_printf的响应头结尾是不是\r\n\r\n少一个空行浏览器和 curl 都会认为头没结束。连接几秒后断开。大概率是心跳没发或间隔太长。中间的反向代理、负载均衡、防火墙通常有 30 到 60 秒的空闲超时心跳间隔要小于这个值15 秒是比较稳的选择。另外确认定时器真的在跑mg_timer_add的周期参数单位是毫秒。浏览器控制台报 CORS 错误。检查响应头里有没有Access-Control-Allow-Origin。SSE 是跨域请求缺这个头浏览器会拦。开发阶段用*生产环境换成具体域名。事件收到了但addEventListener不触发。SSE 帧格式对换行很敏感。event:和data:各自占一行字段名后跟冒号和空格消息以空行结束。如果写成data:xxx没有空格或者两条消息之间只有一个换行解析会出问题。用curl -N看原始字节最靠谱。多个客户端连上后只有第一个收到消息。检查遍历连接的循环是不是从mgr-conns开始完整走完了链表别在中途break。另外确认每个连接的c-data[0]都被正确标记成E漏标记的连接会被continue跳过。编译报mg_match未定义。这个函数在较新版本的 mongoose 里才有老版本用mg_http_match_uri。确认你拉的是当前主分支或者按版本调整 API 名。6. 把调试链路接到 TaoTokenSSE 服务本身跑通后日常调试还有不少可以提效的地方让 AI 帮你读 mongoose 的事件循环源码、根据 curl 输出判断帧格式、生成压测脚本模拟多客户端订阅。这些场景都可以通过 TaoToken 统一接入不用每个工具单独配 Key。具体做法是在支持自定义 base_url 的 AI 工具里把接口地址填成https://taotoken.net/apiKey 从控制台创建。模型对话入口适合临时问问题比如把一段异常的 SSE 输出贴进去让它分析接入文档里有各工具的配置示例Key 管理页面负责创建和轮换。如果你要长期跑编码类任务比如持续改这个 SSE 服务的代码可以看 Coding Plan 那条线把模型调用和开发流程绑在一起。需要提醒的是TaoToken 是模型调用的统一通道和你的 SSE 服务是两回事。服务端该发的text/event-stream帧、该做的心跳保活一样都不能少别指望用模型通道去替代 SSE 本身。把服务跑稳再用 AI 工具加速调试这两件事分开做链路才清晰。