免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Mongoose 开源 web 服务器:C 语言轻量级网络库的嵌入式实践

Mongoose 开源 web 服务器:C 语言轻量级网络库的嵌入式实践 1. 为什么嵌入式项目里会需要一个 C 语言 web 服务器做嵌入式或者 IoT 网关开发的人大概都遇到过这样一个需求设备跑在板子上资源紧张但客户或者测试同事希望能通过浏览器看一眼设备状态或者用 curl 调一个接口把配置读出来。这时候如果搬一个 Nginx 上去先不说交叉编译的麻烦光是内存占用就够呛自己用 socket 手写 HTTP 解析又要处理请求行、头部、keep-alive、分块传输写着写着就变成一个半成品框架。Mongoose 就是在这个缝隙里活得很好的一类东西。它是一个用 C 语言写的嵌入式 web 服务器和网络库核心思路是事件驱动、非阻塞把 TCP、UDP、HTTP、WebSocket、MQTT、CoAP、DNS 这些协议都收进一套 API 里。对嵌入式场景来说它最大的吸引力是「单文件集成」实际项目里通常只需要mongoose.c和mongoose.h两个文件不用把整个src目录搬进工程头文件里已经通过宏把跨平台相关的实现都包进来了。它适合谁适合那些在 MCU、Linux 小板子、RTOS 上做设备端开发想给设备加一个轻量 HTTP 接口又不想引入重型依赖的人。你可以在本地 Linux 上先跑通一个最小服务再把这套代码原样挪到目标平台上改的主要是编译工具链而不是业务逻辑。这篇就按「本地跑通 → 看懂事件模型 → 加 REST 接口 → 排错」的顺序来写所有代码都可以直接复制。文中会用到 TaoToken 的 API 来做一次接口联调演示把设备端服务和外部模型调用串起来看效果但核心还是 Mongoose 本身。2. Mongoose 的前置准备与单文件集成思路2.1 拿到 mongoose.c 和 mongoose.hMongoose 的源码在 GitHub 上以 release 包的形式发布下载下来解压后你会看到mongoose.c和mongoose.h这两个文件。实际工程里把这两个文件复制到你的项目目录比如third_party/mongoose/然后在编译时把它们一起编进去就行。这里有个细节值得说清楚mongoose.h并不是一个简单的函数声明集合它内部通过宏机制把src/common/platforms下针对不同平台的实现都「内联」进来了。所以你在初始化mg_mgr句柄的时候库会根据CS_PLATFORM的值去选择对应的初始化函数。这也是为什么不需要把整个src目录嵌套进项目的原因。2.2 打开日志宏方便排查默认情况下Mongoose 的日志是被宏关掉的。如果你在调试阶段想看它打印的连接、请求信息需要在编译前定义#define CS_ENABLE_STDIO 1这个宏可以写在你的源文件最上面或者通过编译参数-DCS_ENABLE_STDIO1传进去。我一般习惯在 Makefile 里加这样不用改代码。2.3 事件驱动模型到底是怎么回事Mongoose 的核心是一个mg_mgrmanager结构体你可以把它理解成一个「事件循环的管理器」。所有的连接、监听、读写都挂在这个 manager 上。主循环里反复调用mg_mgr_poll(mgr, timeout)这个函数会去检查所有连接上有没有事件发生有的话就回调你注册的处理函数。这种模型和传统的「一个连接一个线程」完全不同。它是单线程或者你手动多线程跑一个循环所有连接共享这个循环靠非阻塞 IO 和事件回调来推进。对嵌入式来说这意味着内存占用可控不需要为每个连接开栈。回调函数的签名是固定的static void ev_handler(struct mg_connection *nc, int ev, void *p);ev是事件类型比如MG_EV_HTTP_REQUEST表示收到一个完整的 HTTP 请求p指向事件相关的数据。你在这个函数里根据ev做分支处理就行。2.4 用 TaoToken 做一次外部接口联调的准备设备端服务跑起来之后很多时候需要和外部服务通信比如把采集到的数据发给一个模型接口做分析。这里我用 TaoToken 的 API 来做演示它的接入地址是https://taotoken.net/api兼容常见的 OpenAI 风格调用方式。你需要先在控制台创建一个 API Key然后就可以在代码里用 HTTP 客户端去请求了。Mongoose 本身也带 HTTP 客户端能力所以设备端可以既做服务端又做客户端这一点在后面会体现。3. 可复制的 Mongoose 配置与最小 HTTP 服务代码3.1 目录结构先约定一个简单的目录结构方便你对照mongoose_demo/ ├── Makefile ├── simplest_web_server.c ├── mongoose.c ├── mongoose.h └── web_root/ └── index.htmlweb_root是静态资源目录index.html里随便写点内容比如h1Mongoose OK/h1。3.2 最小 HTTP 服务代码下面这份代码可以直接复制成simplest_web_server.c#define CS_ENABLE_STDIO 1 #include mongoose.h static const char *s_http_port 8000; static struct mg_serve_http_opts s_http_server_opts; static void ev_handler(struct mg_connection *nc, int ev, void *p) { if (ev MG_EV_HTTP_REQUEST) { mg_serve_http(nc, (struct http_message *) p, s_http_server_opts); } } int main(void) { struct mg_mgr mgr; struct mg_connection *nc; mg_mgr_init(mgr, NULL); printf(Starting web server on port %s\n, s_http_port); nc mg_bind(mgr, s_http_port, ev_handler); if (nc NULL) { printf(Failed to create listener\n); return 1; } mg_set_protocol_http_websocket(nc); s_http_server_opts.document_root web_root; s_http_server_opts.enable_directory_listing yes; for (;;) { mg_mgr_poll(mgr, 1000); } mg_mgr_free(mgr); return 0; }这段代码做了几件事初始化 manager绑定 8000 端口注册回调设置 HTTP 协议指定静态资源根目录然后进入无限循环。mg_mgr_poll的第二个参数是超时时间单位毫秒1000 表示每次 poll 最多阻塞 1 秒。3.3 Makefile 配置CC gcc CFLAGS -O2 -Wall -DCS_ENABLE_STDIO1 TARGET simplest_web_server SRCS simplest_web_server.c mongoose.c $(TARGET): $(SRCS) $(CC) $(CFLAGS) -o $(TARGET) $(SRCS) clean: rm -f $(TARGET)编译命令就是make生成可执行文件后直接./simplest_web_server运行。3.4 用 JSON 描述一份接入配置如果你要把设备端服务和 TaoToken 的接口串起来可以准备一份配置文件比如config.json{ http_port: 8000, document_root: web_root, taotoken: { base_url: https://taotoken.net/api, api_key: sk-你的Key, model_id: gpt-4o-mini } }这份配置里base_url、api_key、model_id三件套是调用外部接口时必须的。设备端读取这份配置后就可以用 Mongoose 的 HTTP 客户端去请求了。注意api_key不要硬编码进固件实际项目里建议放在安全存储或者运行时注入。3.5 加一个 REST 接口光有静态资源还不够设备端通常需要动态接口。下面在回调里加一个/api/status的处理static void ev_handler(struct mg_connection *nc, int ev, void *p) { if (ev MG_EV_HTTP_REQUEST) { struct http_message *hm (struct http_message *) p; if (mg_vcmp(hm-uri, /api/status) 0) { const char *body {\device\:\sensor-01\,\status\:\ok\}; mg_printf(nc, HTTP/1.1 200 OK\r\n Content-Type: application/json\r\n Content-Length: %d\r\n \r\n %s, (int) strlen(body), body); nc-flags | MG_F_SEND_AND_CLOSE; return; } mg_serve_http(nc, hm, s_http_server_opts); } }这里用mg_vcmp比较 URI匹配到/api/status就返回一段 JSON。MG_F_SEND_AND_CLOSE表示发完就关闭连接简单直接。4. 用 curl 验证静态资源与 REST 接口4.1 启动服务编译运行后终端会打印Starting web server on port 8000。这时候服务已经在监听。4.2 验证静态资源打开另一个终端执行curl -v http://127.0.0.1:8000/index.html你应该能看到web_root/index.html的内容响应头里会有Content-Type: text/html。如果enable_directory_listing设成了yes访问http://127.0.0.1:8000/还能看到目录列表。4.3 验证 REST 接口curl -s http://127.0.0.1:8000/api/status预期输出{device:sensor-01,status:ok}用-i参数可以看到完整的响应头curl -i http://127.0.0.1:8000/api/status会看到HTTP/1.1 200 OK和Content-Type: application/json。4.4 用 TaoToken 接口做一次联调设备端服务跑通后可以写一个简单的客户端程序用 Mongoose 的 HTTP 客户端去请求 TaoToken 的接口。核心代码大致是这样static void http_client_handler(struct mg_connection *nc, int ev, void *p) { if (ev MG_EV_HTTP_REPLY) { struct http_message *hm (struct http_message *) p; printf(Response: %.*s\n, (int) hm-body.len, hm-body.p); nc-flags | MG_F_CLOSE_IMMEDIATELY; } } void call_taotoken(const char *api_key, const char *model_id) { struct mg_mgr mgr; struct mg_connection *nc; char buf[1024]; mg_mgr_init(mgr, NULL); nc mg_connect_http(mgr, http_client_handler, https://taotoken.net/api/v1/chat/completions, NULL, NULL); snprintf(buf, sizeof(buf), {\model\:\%s\,\messages\:[{\role\:\user\,\content\:\hello\}]}, model_id); mg_printf(nc, POST /api/v1/chat/completions HTTP/1.1\r\n Host: taotoken.net\r\n Authorization: Bearer %s\r\n Content-Type: application/json\r\n Content-Length: %d\r\n \r\n %s, api_key, (int) strlen(buf), buf); mg_mgr_poll(mgr, 2000); mg_mgr_free(mgr); }这段代码把请求发出去然后在回调里打印响应。实际跑的时候你会看到模型返回的 JSON。这一步验证了设备端既能做服务端也能做客户端。如果你更想直接在浏览器里试模型对话可以打开https://taotoken.net/models看看效果再决定要不要在设备端集成。5. 本篇常见错误排查5.1 编译报错 undefined reference tomg_mgr_init这个错误通常是因为mongoose.c没有参与编译。检查你的 Makefile 或者编译命令确认mongoose.c在源文件列表里。另一个可能是mongoose.h和mongoose.c版本不匹配从同一个 release 包里取出来的两个文件才能配对使用。5.2 启动后 curl 返回 404先确认document_root指向的目录存在并且里面有对应的文件。如果你设的是web_root但实际目录叫www那自然找不到。另外注意相对路径是相对于进程的工作目录不是可执行文件所在目录。可以用绝对路径排除这个问题。5.3 端口被占用bind 失败如果mg_bind返回 NULL先看日志有没有打印Failed to create listener。常见原因是 8000 端口已经被别的进程占了。用lsof -i :8000或者netstat -tlnp | grep 8000查一下换个端口或者杀掉占用进程。5.4 请求 TaoToken 接口返回 401401 一般和 Key 有关。检查三件事Authorization头是不是Bearer sk-xxx格式Key 有没有多余空格以及这个 Key 是不是在控制台里被禁用或者删除了。如果用的是环境变量注入确认变量名拼写正确。你可以到https://taotoken.net/api-keys重新生成一个 Key 试试。5.5 日志里出现 local proxy failed 或者连接超时这类报错通常出现在设备端请求外部接口时。先确认设备本身能解析域名、能出网。如果是在受限网络环境里检查 DNS 配置。Mongoose 自带异步 DNS 解析器但前提是系统网络栈正常。另外注意 HTTPS 请求需要 TLS 支持确认编译时没有把相关宏关掉。5.6 响应体读取不完整出现 reading choices 之类的解析错误如果你在解析模型返回的 JSON 时遇到字段读不到先打印完整的响应体看看。常见原因是Content-Length和实际 body 长度不一致或者响应是分块传输chunked而你的解析代码没处理。Mongoose 的MG_EV_HTTP_REPLY事件里hm-body已经是完整的 body直接用就行不要自己去拼。5.7 OAuth 相关报错如果你用的是需要 OAuth 的接口报错信息里可能会提到 token 过期或者 scope 不足。这时候需要重新走授权流程拿新的 token。对于 TaoToken 的 API Key 方式一般不会遇到 OAuth 问题除非你接的是别的服务。6. 把设备端服务和模型能力接起来Mongoose 在嵌入式场景里的价值不只是「能跑一个 web 服务器」而是它把服务端和客户端能力放在同一套事件循环里。你可以在一个进程里既对外提供 HTTP 接口又主动去请求外部服务代码结构还保持一致。实际落地时我建议先把静态资源和本地 REST 接口跑通确认板子上的网络栈没问题再去接外部接口。接外部接口的时候把 Base URL、API Key、Model ID 这三样东西做成可配置的不要写死在代码里。调试阶段可以用curl直接打 TaoToken 的接口确认 Key 和网络都正常再回到设备端代码里排查。如果你打算长期在设备端做模型调用或者 Agent 相关的功能可以了解一下 Coding Plan 这类方案它更适合需要持续调用、有额度管理需求的场景。接入文档在https://taotoken.net/doc里面有各个接口的详细说明。最后留一个实用技巧Mongoose 的mg_mgr_poll超时时间不要设得太短否则 CPU 会空转也不要设得太长否则事件响应会迟钝。1000 毫秒在大多数嵌入式场景里是个比较稳的折中值你可以根据实际负载调整。
返回列表