资讯动态

C++用mongoose实现HTTP服务端和客户端开发实践

发布时间:2026/10/15 23:02:02 来源:尧图企业网站定制
1. 为什么用 mongoose 做 HTTP 服务端和客户端1.1 它解决什么问题做 C 网络开发的人应该都清楚HTTP 这块看着简单真要自己从 socket 往上写状态机、粘包、Header 解析、Chunked 编码一大堆细节。mongoose 就是帮你把这层脏活封装好的库。它是纯 C 设计以 mongoose.c 和 mongoose.h 两个文件发布C 项目直接拿过来参与编译就行不需要安装额外的运行时也不要求 Boost 这类巨头依赖。嵌入式和服务器环境都能跑这是它最大的优势。更关键的是mongoose 同时提供 HTTP 服务端和客户端能力。一个进程里既可以mg_http_listen监听接口又可以mg_http_connect主动请求别的服务。很多场景下我们的程序既要对外暴露 API又要在内部转发请求到别的系统这种“双重角色”用同一个库处理心智负担小很多。实际项目中我最早是拿它做某设备的本地诊断服务设备上跑一个 HTTP 接口给手机端查询状态同时这个服务又要主动调用后台的鉴权接口mongoose 一个库就把两边都覆盖了。适合谁来用如果你要在 Linux、Windows、嵌入式 RTOS 上快速集成 HTTP 能力又不想引一整套厚重的框架mongoose 是很合适的选择。当然如果项目本身就是大规模分布式服务需要各种高级特性那可能还是得上更重的框架但如果只是要一个轻量、可靠、能嵌入的 HTTP 层mongoose 非常顺手。1.2 核心工作方式事件驱动回调第一次接触 mongoose 的人最不适应的是它的模型。它不是“收到请求调用一个函数返回结果”而是你在一个事件循环里注册回调当连接上有事件发生时回调被执行。程序里通常是这样运转的mg_mgr_init(mgr); mg_http_listen(mgr, http://0.0.0.0:8080, my_callback, NULL); for (;;) mg_mgr_poll(mgr, 1000);mg_mgr_poll是事件泵它每轮检查所有连接上有没有新数据、新连接、可写事件然后调用对应的回调。单线程就能处理大量并发连接不需要为每个连接开线程。用生活类比就是传统多线程方案像每个客人配一个专属服务员客人一多成本就翻倍mongoose 是餐厅里一个手脚麻利的服务员一个时间段内同时跑好几桌客人举手他就过来处理。回调里不能像线程模型那样任意阻塞因为阻塞在某个请求上整个事件循环都会被拖住。理解了这一点后面很多设计取舍你会自动明白。1.3 选型对比为什么不是其他库C 生态里 HTTP 库不少我简单列一下常见选择方案服务端客户端依赖适合场景Boost.Beast支持支持Boost.Asio较重大规模异步服务对性能极致追求cpp-httplib支持支持轻量但同步阻塞桌面工具、测试服务Drogon支持支持较重异步模型复杂Web 应用后端mongoose支持支持两个文件极轻嵌入式、网关、工具型服务我选 mongoose 主要看中三点。第一是轻嵌入式交叉编译时一个 .c 文件比一堆头文件和链接库省事太多第二是服务端和客户端 API 风格统一都是回调模型记住一套就行第三是它对系统 API 的封装很干净Windows 下自动用 WinsockLinux 下用 socket不需要你关心平台差异。影响范围上从单片机上的设备管理接口到 PC 端的本地调试服务再到后台模块之间的轻量通信都能覆盖。2. 环境准备把 mongoose 接进 C 工程2.1 获取源码与目录结构mongoose 官方提供的是“一体化”发布方式下载下来就是两个文件mongoose.c 和 mongoose.h。使用非常简单放到你的项目目录里和你的 C 源文件一起编译。我习惯把工程摆成下面这样demo/ CMakeLists.txt mongoose.c mongoose.h server.cpp client.cpp web/ index.html你可以从官网下载最新版本或者直接用你项目里已有的版本。需要注意一点mongoose 的 API 在小版本之间偶尔会调整。下面代码基于当前主流的 7.x 版本写如果你发现某个函数名对不上先看看是不是下载到了老版本。2.2 CMake 配置模板CMake 里要同时启用 C 和 C 语言因为 mongoose.c 是 C 文件。一个最小可用的配置如下cmake_minimum_required(VERSION 3.16) project(mongoose_demo C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(server server.cpp mongoose.c) add_executable(client client.cpp mongoose.c) target_include_directories(server PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})编译的时候CMake 会自动用 C 编译器编译 mongoose.c再用 C 编译器编译你的业务代码最后链接到一起。如果你的项目在其他模块里已经引入了 mongoose.c注意不要重复添加否则符号冲突。Linux 下如果链接阶段报 pthread 相关的错误可以把线程库加上find_package(Threads REQUIRED) target_link_libraries(server PRIVATE Threads::Threads) target_link_libraries(client PRIVATE Threads::Threads)2.3 编译选项与依赖mongoose 提供一些编译开关通过宏来控制功能。最常见的几个MG_ENABLE_OPENSSL1启用 TLS/HTTPS 支持链接 OpenSSL。MG_ENABLE_IPV61启用 IPv6。MG_ENABLE_LOG1启用内置日志。MG_ENABLE_MBEDTLS1嵌入式环境用 mbedTLS。如果你只需要本地 HTTP保持默认就够不需要任何外部依赖。需要 HTTPS 的时候再打开用https://开头的 URL 监听和连接。我个人建议先把不加 SSL 的版本跑通再考虑加密链路排查问题会容易得多。3. 服务端开发路由、参数与响应3.1 最小服务端代码服务端的核心就是一个回调函数加一个事件循环。收到 HTTP 请求时事件类型是MG_EV_HTTP_MSGev_data可以转成struct mg_http_message *里面有 method、uri、query、body、headers 等完整信息。#include mongoose.h static void server_cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { if (ev MG_EV_HTTP_MSG) { struct mg_http_message *hm (struct mg_http_message *) ev_data; mg_http_reply(c, 200, Content-Type: text/plain\r\n, hello\n); } } int main(void) { struct mg_mgr mgr; mg_mgr_init(mgr); mg_http_listen(mgr, http://0.0.0.0:8080, server_cb, NULL); printf(server on :8080\n); for (;;) mg_mgr_poll(mgr, 1000); mg_mgr_free(mgr); return 0; }mg_http_listen第二个参数是监听地址。0.0.0.0:8080表示监听所有网卡的 8080 端口。如果你只想本机访问可以写成127.0.0.1:8080。这个回调会在每次事件触发时被调用所以第一件事就是判断ev类型。3.2 路由按 URI 分发请求随着接口变多一个回调里写满 if-else 还是可以接受的。mongoose 提供mg_match做模式匹配支持通配符。*匹配到下一个斜杠为止#匹配剩下的全部内容。我最常用的写法是if (mg_match(hm-uri, mg_str(/api/hello), NULL)) { mg_http_reply(c, 200, Content-Type: application/json\r\n, {\code\:0,\msg\:\hello\}\n); } else if (mg_match(hm-uri, mg_str(/api/user/#), NULL)) { // 这里可以拿到 URI 后面的用户 ID } else { mg_http_reply(c, 404, Content-Type: application/json\r\n, {\code\:404}\n); }mg_match的第一个参数是被匹配的字符串第二个参数是规则。第三个参数可以传入一个struct mg_str *保存匹配的通配符片段比如/api/user/#中#捕获的部分。这种匹配方式够用而且比正则快很多。实际项目里我会把路由表抽出来用一张表维护路径和处理函数避免回调函数膨胀成几百行。需要注意hm-uri是struct mg_str不是 C 字符串它不保证以\0结尾。如果要转成std::string要用std::string(hm-uri.ptr, hm-uri.len)不能直接printf(%s, hm-uri.ptr)。3.3 读取 GET 和 POST 参数mongoose 提供了mg_http_get_var来解析 URL query 和表单 body。它接收一个struct mg_str *所以 query、body、甚至 cookie 都可以喂给它。示例char name[128] {0}; int len mg_http_get_var(hm-query, name, name, sizeof(name)); if (len 0) { // 用 name }如果是 POST 表单参数在 body 里把hm-query换成hm-body即可mg_http_get_var(hm-body, username, user, sizeof(user)); mg_http_get_var(hm-body, password, pass, sizeof(pass));这里有个容易被坑的点mg_http_get_var解析的是application/x-www-form-urlencoded格式如果你的客户端发的是application/json那它不会帮你解出 JSON 字段。JSON 的解析不在 mongoose 的职责范围内。对于简单的 JSON 接口我的做法是配合 nlohmann/json 这类轻量库服务端收到 body 后转成对象再处理。3.4 返回 JSON 和提供静态文件返回 JSON 用mg_http_reply很简单。注意它是 printf 风格所以如果你要输出的 JSON 里包含特殊字符要用%.*s传入数据不要直接把字符串放进格式串里否则一旦内容里有%整个输出就乱了。例如回显一个 POST 请求的原始 bodymg_http_reply(c, 200, Content-Type: application/json\r\n, {\code\:0,\echo\:\%.*s\}\n, (int) hm-body.len, hm-body.ptr);静态文件方面mongoose 提供了mg_http_serve_dir。配置一个根目录然后把非 API 的请求交给它处理struct mg_http_serve_opts opts; memset(opts, 0, sizeof(opts)); opts.root_dir ./web; mg_http_serve_dir(c, hm, opts);它会自行处理 MIME 类型、路径拼接和目录索引。更重要的是它内置了路径穿越防护不会因为你把 URI 里的../拼到本地路径上就泄露文件。如果你曾经吃过手写 file server 的亏用这个函数是最省心的做法。3.5 在回调里做简单鉴权很多接口不希望裸奔最轻量的方案是在回调开头检查请求头。mongoose 提供mg_http_get_headerstruct mg_str *auth mg_http_get_header(hm, Authorization); if (auth NULL || !mg_match(*auth, mg_str(Bearer *), NULL)) { mg_http_reply(c, 401, Content-Type: application/json\r\n, {\code\:401}\n); return; }这种检查是同步的适合 token 本地校验。如果你需要回调里查数据库、调外部接口那就别直接等否则整个事件循环会被卡住。合理的做法是把任务丢给工作线程完成后用mg_mgr_wakeup把结果投递回 mongoose 的事件循环再由回调把响应发出去。这一点在实际项目中救过大命。4. 客户端开发异步请求与同步封装4.1 客户端也是同一套事件模型很多人以为 mongoose 只有服务端其实客户端的思路完全对称。mg_http_connect发起连接之后依然是通过回调接收事件。连接建立时触发MG_EV_CONNECT收到 HTTP 响应时触发MG_EV_HTTP_MSG出错时触发MG_EV_ERROR。一个最简单的客户端骨架struct mg_mgr mgr; mg_mgr_init(mgr); struct mg_connection *c mg_http_connect(mgr, http://127.0.0.1:8080, client_cb, NULL); if (c ! NULL) { mg_printf(c, GET /api/hello HTTP/1.0\r\nHost: 127.0.0.1:8080\r\n\r\n); } for (;;) mg_mgr_poll(mgr, 100);注意mg_http_connect是非阻塞的它并不会等 TCP 三次握手完成才返回。调用mg_http_connect后连接会异步建立。上面这种写法把请求数据直接排到发送缓冲里等连接就绪后 mongoose 会自动发出去。4.2 构造 GET 和 POST 请求HTTP 请求说白了就是控制好回车换行和 Content-Length。GET 请求最简单mg_printf(c, GET %s HTTP/1.0\r\nHost: %s\r\n\r\n, path, host);POST 请求需要带 body必须把Content-Length算对否则服务端会一直等 bodyconst char *body {\user\:\admin\,\pass\:\123\}; mg_printf(c, POST /api/login HTTP/1.0\r\n Host: 127.0.0.1:8080\r\n Content-Type: application/json\r\n Content-Length: %d\r\n\r\n %s, (int) strlen(body), body);这里我用 HTTP/1.0是因为客户端场景下响应结束连接就关闭省去了 Keep-Alive 和连接复用的处理最简单可靠。如果你的服务端要求 HTTP/1.1或者你想复用连接可以改用 1.1 并处理Host、Connection头但初学阶段没必要。4.3 回调里解析响应数据响应到达时struct mg_http_message *hm里装的是完整 HTTP 响应。状态码可以从hm-message的开头解析body 在hm-body。我习惯先取状态码再根据状态判断是否解析 bodystatic void client_cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { if (ev MG_EV_HTTP_MSG) { struct mg_http_message *hm (struct mg_http_message *) ev_data; int status 0; sscanf(hm-message.ptr, HTTP/%*d.%*d %d, status); printf(status%d\n, status); printf(body%.*s\n, (int) hm-body.len, hm-body.ptr); } }mg_http_get_header在客户端响应上同样能用比如读取响应里的Content-Type或Set-Cookie。要记住hm-body.ptr不一定是\0结尾拷贝到std::string时必须带长度。4.4 封装成同步请求帮助函数纯异步回调写起来灵活但很多业务代码还是想“调一个函数拿到返回结果”。这种需求完全可以封装。我平时的做法是给每个请求建一个上下文结构体回调通过fn_data拿到它请求完成后置一个完成标记主循环等到标记或超时struct ReqCtx { int done; int status; std::string body; }; static void client_cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { ReqCtx *ctx (ReqCtx *) fn_data; if (ev MG_EV_CONNECT) { int status *(int *) ev_data; if (status ! 0) ctx-done 2; } else if (ev MG_EV_HTTP_MSG) { struct mg_http_message *hm (struct mg_http_message *) ev_data; sscanf(hm-message.ptr, HTTP/%*d.%*d %d, ctx-status); ctx-body.assign(hm-body.ptr, hm-body.len); ctx-done 1; } else if (ev MG_EV_ERROR) { ctx-done 2; } }然后封装函数内部自己建一个mg_mgr循环 poll 直到超时。注意不要在循环里让mg_mgr_poll的 timeout 设成 0那会导致 CPU 空转。50 毫秒是一个比较合适的值既不会迟钝也不会太吃 CPU。封装函数里同样需要注意回调是在mg_mgr_poll内部触发的所以千万不要在回调里再去访问和修改主线程正在用的非线程安全数据除非你用互斥锁保护。5. 完整可运行示例服务端和客户端联调5.1 服务端一组简单的 API 集合下面是一个完整的服务端示例包含接口和静态文件服务。你把它存成server.cpp#include mongoose.h #include stdio.h #include string.h #include stdlib.h static void server_cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { if (ev ! MG_EV_HTTP_MSG) return; struct mg_http_message *hm (struct mg_http_message *) ev_data; if (mg_match(hm-uri, mg_str(/api/hello), NULL)) { mg_http_reply(c, 200, Content-Type: application/json\r\n, {\code\:0,\msg\:\hello\}\n); } else if (mg_match(hm-uri, mg_str(/api/sum), NULL)) { char a[16] {0}, b[16] {0}; mg_http_get_var(hm-query, a, a, sizeof(a)); mg_http_get_var(hm-query, b, b, sizeof(b)); int sum atoi(a) atoi(b); mg_http_reply(c, 200, Content-Type: application/json\r\n, {\code\:0,\sum\:%d}\n, sum); } else { struct mg_http_serve_opts opts; memset(opts, 0, sizeof(opts)); opts.root_dir ./web; mg_http_serve_dir(c, hm, opts); } } int main(void) { struct mg_mgr mgr; mg_mgr_init(mgr); mg_http_listen(mgr, http://0.0.0.0:8080, server_cb, NULL); printf(server started on 8080\n); for (;;) mg_mgr_poll(mgr, 1000); mg_mgr_free(mgr); return 0; }注意mg_http_get_var返回的是字符串所以数值参数我用atoi转成整数。这个接口适合演示 query 解析实际项目中建议多做参数校验不要直接信任 input。5.2 客户端调用服务端接口客户端示例存成client.cpp发起一次 GET 和一次 POST#include mongoose.h #include chrono #include cstdio #include cstring #include string struct ReqCtx { int done 0; int status 0; std::string body; }; static void client_cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { ReqCtx *ctx (ReqCtx *) fn_data; if (ev MG_EV_CONNECT) { int status *(int *) ev_data; if (status ! 0) { fprintf(stderr, connect failed, status%d\n, status); ctx-done 2; } } else if (ev MG_EV_HTTP_MSG) { struct mg_http_message *hm (struct mg_http_message *) ev_data; sscanf(hm-message.ptr, HTTP/%*d.%*d %d, ctx-status); ctx-body.assign(hm-body.ptr, hm-body.len); ctx-done 1; } else if (ev MG_EV_ERROR) { fprintf(stderr, http error: %s\n, (char *) ev_data); ctx-done 2; } } static std::string extract_host(const char *url) { const char *p strstr(url, ://); const char *start (p ! NULL) ? p 3 : url; const char *slash strchr(start, /); if (slash ! NULL) return std::string(start, slash - start); return std::string(start); } static bool send_request(const char *url, const char *method, const char *path, const char *body, int timeout_ms, ReqCtx *out) { struct mg_mgr mgr; mg_mgr_init(mgr); ReqCtx ctx; struct mg_connection *c mg_http_connect(mgr, url, client_cb, ctx); if (c NULL) { mg_mgr_free(mgr); return false; } std::string host extract_host(url); if (strcmp(method, GET) 0) { mg_printf(c, GET %s HTTP/1.0\r\nHost: %s\r\n\r\n, path, host.c_str()); } else { mg_printf(c, POST %s HTTP/1.0\r\n Host: %s\r\n Content-Type: application/json\r\n Content-Length: %d\r\n\r\n %s, path, host.c_str(), (int) strlen(body), body); } auto begin std::chrono::steady_clock::now(); while (ctx.done 0) { auto elapsed std::chrono::duration_caststd::chrono::milliseconds( std::chrono::steady_clock::now() - begin).count(); if (elapsed timeout_ms) break; mg_mgr_poll(mgr, 50); } *out ctx; mg_mgr_free(mgr); return ctx.done 1; } int main(void) { ReqCtx r1; if (send_request(http://127.0.0.1:8080, GET, /api/sum?a10b20, NULL, 3000, r1)) { printf(GET /api/sum - status%d body%s\n, r1.status, r1.body.c_str()); } else { printf(GET /api/sum failed\n); } ReqCtx r2; const char *body {\msg\:\hello from client\}; if (send_request(http://127.0.0.1:8080, POST, /api/echo, body, 3000, r2)) { printf(POST /api/echo - status%d body%s\n, r2.status, r2.body.c_str()); } else { printf(POST /api/echo failed\n); } return 0; }这里的send_request每次创建独立的mg_mgr好处是互不干扰坏处是每次调用都有建连开销。高频调用场景建议改用全局 manager把连接和事件循环常驻。5.3 编译、运行和验证按下面步骤操作mkdir -p build web echo htmlbodyh1mongoose demo/h1/body/html web/index.html cd build cmake .. make ./server ./client curl http://127.0.0.1:8080/api/sum?a1b2预期输出curl返回{code:0,sum:3}客户端程序打印 GET 和 POST 的请求结果浏览器打开http://127.0.0.1:8080/能看到刚才的 HTML 页面第一次跑通整个链路后再改接口、加鉴权、接数据库就有稳定的地基了。6. 常见问题与排查实录6.1 高频问题速查表现象可能原因处理方式编译报错找不到mg_http_listen版本太老或头文件不对更新到 7.x确保 include 的是同一个 mongoose.h链接失败出现一堆undefined referencemongoose.c 没有参与编译确认 CMake 里把 mongoose.c 加进了 target并启用 C 语言服务端能编译但启动报端口占用端口被其他进程占用lsof -i:8080或netstat找出占用进程客户端连不上MG_EV_CONNECT的 status 非 0服务端没启动、地址错了、防火墙拦截先用 curl 验证服务端再查监听地址客户端请求发出去了但一直收不到响应请求头里Content-Length不正确或没写Host用 HTTP/1.0 简化仔细构造请求消息回调里做耗时操作整个服务卡死事件循环被阻塞耗时任务放到工作线程用mg_mgr_wakeup回投CPU 占用居高不下mg_mgr_poll的 timeout 为 0改成 10~100ms或者按业务需要给出合理值中文乱码Content-Type 缺少 charset 或长度单位算错Header 写text/html; charsetutf-8body 长度按字节算6.2 我踩过的几个坑第一个坑是把回调里的hm-uri.ptr直接当成char *用。它并不是以\0结尾的直接strlen会越界。之后我养成了习惯所有从struct mg_str转字符串的操作都带长度。第二个坑是在回调里把fn_data指向了一个栈上对象但请求还没完成函数就返回了结果回调触发时访问到非法内存。现在我的规则是用堆对象或者把上下文的生命周期拉长到整个请求结束宁可多一句delete也不能让回调踩悬空指针。第三个坑和mg_http_reply的格式串有关。有一次我在 JSON 里动态拼客户输入直接用了用户输入当格式串结果遇到内容里的%就崩溃或乱码。正确做法是永远把动态内容作为参数传给格式串比如%.*s。这在服务端和客户端都适用。最后分享一个小技巧调试阶段服务端先用 curl 验证客户端再单独用一个小测试服务验证。两边都跑通后再联调出问题就能立刻判断是服务端的问题还是客户端的问题不用在两端之间反复猜。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑