ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Mongoose笔记】TCP 客户端与服务器:从连接建立到数据收发

【Mongoose笔记】TCP 客户端与服务器:从连接建立到数据收发 1. Mongoose TCP 客户端与服务器最小可跑通示例踩坑记录Mongoose 是一个用 C 写的网络库单个mongoose.cmongoose.h就能把 TCP、UDP、HTTP、WebSocket、MQTT 的事件驱动 API 全带上特别适合在树莓派、嵌入式 Linux、RTOS 这类资源不宽裕的环境里做网络通信。它把「连接」抽象成struct mg_connection把「事件循环」抽象成struct mg_mgr你只需要写回调剩下的非阻塞收发、定时器、断线重连都交给mg_mgr_poll去驱动。这篇笔记聚焦 Mongoose TCP 客户端与服务器从连接建立到数据收发的完整流程给出可直接复制的mg_config、事件绑定与回调骨架代码并附本地回环验证步骤帮你在十分钟内跑通一个「客户端连服务器、发消息、服务器回显、断开后自动重连」的最小可用示例。适合已经会写 C、但第一次接触事件驱动网络库的同学也适合想把裸 socket 换成更省心方案的嵌入式开发者。我试过在树莓派上直接make官方examples/tcp第一次跑就遇到「客户端连上了但服务器没打印」的情况后来发现是mg_mgr_poll的超时参数和定时器周期没对齐。下面把整个流程拆成可跟做的步骤每一步都给出完整代码和验证方法。2. 环境准备与 mg_config 编译配置Mongoose TCP 示例编译报错怎么解决Mongoose 的获取方式很简单直接 clone 官方仓库即可不需要额外依赖。它的设计哲学是「一个 .c 文件就是全部」所以你甚至可以把mongoose.c和mongoose.h拷到自己的工程里用任意构建系统编译。git clone https://github.com/cesanta/mongoose.git cd mongoose/examples/tcp ls # main.c Makefile mongoose.c mongoose.h官方示例目录里已经带了mongoose.c和mongoose.h的软链接或副本Makefile内容大致如下核心就是编译main.c并链接mongoose.cPROG example SOURCES main.c ../../mongoose.c CFLAGS -I../.. -W -Wall -Werror all: $(PROG) $(PROG): $(SOURCES) $(CC) $(SOURCES) $(CFLAGS) -o $(PROG) clean: rm -f $(PROG)如果你要把它集成进自己的工程推荐用 CMake把 Mongoose 的编译选项通过mg_config宏控制。Mongoose 支持在编译时用-DMG_ENABLE_XXX0/1裁剪功能比如你只做纯 TCP、不需要 TLS 和 HTTP就可以关掉它们减小体积cc main.c mongoose.c -I. -o example \ -DMG_ENABLE_OPENSSL0 \ -DMG_ENABLE_MBEDTLS0 \ -DMG_ENABLE_HTTP0 \ -DMG_ENABLE_HTTP_CLIENT0 \ -DMG_ENABLE_HTTP_SERVER0 \ -DMG_ENABLE_WEBSOCKET0 \ -DMG_ENABLE_MQTT0 \ -DMG_ENABLE_LOG1这里有个容易踩的坑MG_ENABLE_LOG1必须打开否则MG_INFO这类日志宏会被编译成空操作你运行程序时什么都看不到会误以为「连接没建立」。另外-W -Wall -Werror在部分 GCC 版本上会因为mongoose.c里的类型转换告警而编译失败遇到error: unused parameter时把-Werror去掉即可或者加-Wno-unused-parameter。如果你用的是 Windows MSVC编译命令换成cl main.c mongoose.c /I. /Fe:example.exe注意 MSVC 对 C99 的支持需要/std:c11。树莓派上直接用系统自带的cc就行我实测下来 GCC 10 和 GCC 12 都能正常编译。编译成功后你会得到一个example可执行文件。在运行之前先确认8765端口没有被占用用ss -tlnp | grep 8765检查一下。如果被占用改main.c里的s_lsn和s_conn两个字符串即可它们必须指向同一个地址否则客户端连不上服务器。3. 可复制的 TCP 客户端与服务器骨架代码mg_connect、mg_listen 与事件回调绑定这一节给出完整的main.c你可以直接复制保存。代码结构分四块全局资源、客户端回调cfn、服务器回调sfn、定时器timer_fn和main。核心思路是「一个事件管理器 一个监听连接 一个定时器驱动的客户端连接」。#include mongoose.h static const char *s_lsn tcp://localhost:8765; // 服务器监听地址 static const char *s_conn tcp://localhost:8765; // 客户端连接地址 // 客户端资源i 用于轮询计数c 保存当前连接 static struct c_res_s { int i; struct mg_connection *c; } c_res; // ---------- 客户端事件回调 ---------- static void cfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { int *i ((struct c_res_s *) fn_data)-i; if (ev MG_EV_OPEN) { MG_INFO((CLIENT has been initialized)); } else if (ev MG_EV_CONNECT) { MG_INFO((CLIENT connected)); *i 1; // 标记连接已建立开始轮询计数 } else if (ev MG_EV_READ) { struct mg_iobuf *r c-recv; MG_INFO((CLIENT got data: %.*s, r-len, r-buf)); } else if (ev MG_EV_CLOSE) { MG_INFO((CLIENT disconnected)); ((struct c_res_s *) fn_data)-c NULL; // 置空供定时器重连 } else if (ev MG_EV_ERROR) { MG_INFO((CLIENT error: %s, (char *) ev_data)); } else if (ev MG_EV_POLL *i ! 0) { switch ((*i)) { case 50: // 50 x 100ms 5s 后发送数据 mg_send(c, Hi, there, 9); MG_INFO((CLIENT sent data)); break; case 100: // 再 5s 后关闭连接 c-is_draining 1; break; } } } // ---------- 服务器事件回调 ---------- static void sfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { if (ev MG_EV_OPEN c-is_listening 1) { MG_INFO((SERVER is listening)); } else if (ev MG_EV_ACCEPT) { MG_INFO((SERVER accepted a connection)); } else if (ev MG_EV_READ) { struct mg_iobuf *r c-recv; MG_INFO((SERVER got data: %.*s, r-len, r-buf)); mg_send(c, r-buf, r-len); // 原样回显 } else if (ev MG_EV_CLOSE) { MG_INFO((SERVER disconnected)); } else if (ev MG_EV_ERROR) { MG_INFO((SERVER error: %s, (char *) ev_data)); } (void) fn_data; } // ---------- 定时器断线重连 ---------- static void timer_fn(void *arg) { struct mg_mgr *mgr (struct mg_mgr *) arg; if (c_res.c NULL) { c_res.i 0; c_res.c mg_connect(mgr, s_conn, cfn, c_res); if (c_res.c NULL) MG_INFO((CLIENT cant open a connection)); else MG_INFO((CLIENT is connecting)); } } int main(void) { struct mg_mgr mgr; struct mg_connection *c; mg_log_set(MG_LL_INFO); mg_mgr_init(mgr); // 15 秒周期定时器立即执行一次 mg_timer_add(mgr, 15000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_fn, mgr); c mg_listen(mgr, s_lsn, sfn, NULL); if (c NULL) { MG_INFO((SERVER cant open a connection)); return 0; } while (true) mg_mgr_poll(mgr, 100); mg_mgr_free(mgr); return 0; }几个关键点解释一下。mg_mgr_init初始化事件管理器所有连接都挂在它下面。mg_timer_add注册一个 15 秒重复定时器MG_TIMER_RUN_NOW让它注册后立刻跑一次这样客户端不用等 15 秒就能发起连接。mg_listen创建监听连接返回的c如果为 NULL 说明端口被占用或地址非法。mg_mgr_poll(mgr, 100)是事件循环的心脏它每次最多阻塞 100 毫秒有网络活动时立即返回并分发事件。客户端的MG_EV_POLL计数逻辑依赖mg_mgr_poll的 100 毫秒超时所以case 50大约对应 5 秒。如果你把 poll 超时改成 50那case 50就变成 2.5 秒这个对应关系要记牢。c-is_draining 1表示「发完剩余数据就关闭」比直接c-is_closing 1更优雅不会丢数据。4. 本地回环验证与成功结果观察 MG_EV_CONNECT、MG_EV_READ 与断线重连日志编译运行后你应该看到类似下面的日志。这是判断「跑通了」的直接依据$ ./example 2fa28 2 main.c:53:sfn SERVER is listening 2fa8c 2 main.c:19:cfn CLIENT has been initialized 2fa8c 2 main.c:86:timer_fn CLIENT is connecting 2fa8c 2 main.c:21:cfn CLIENT connected 2fa8c 2 main.c:55:sfn SERVER accepted a connection 30e1b 2 main.c:40:cfn CLIENT sent data 30e1c 2 main.c:66:sfn SERVER got data: Hi, there 30e1c 2 main.c:29:cfn CLIENT got data: Hi, there 3201a 2 main.c:31:cfn CLIENT disconnected 3201a 2 main.c:69:sfn SERVER disconnected 33537 2 main.c:19:cfn CLIENT has been initialized 33538 2 main.c:86:timer_fn CLIENT is connecting 33538 2 main.c:21:cfn CLIENT connected 33538 2 main.c:55:sfn SERVER accepted a connection日志顺序对应的事件流是服务器先监听客户端初始化并连接服务器接受连接5 秒后客户端发送Hi, there服务器收到并回显客户端收到回显10 秒后客户端主动断开服务器感知断开15 秒定时器触发客户端重新连接循环往复。如果你想用外部工具验证可以开两个终端。一个跑./example另一个用nc手动连上去发消息# 终端 A ./example # 终端 B nc localhost 8765 hello mongoose # 你会看到回显 hello mongoose注意用nc连上去时服务器回调sfn的MG_EV_ACCEPT和MG_EV_READ都会触发日志里会多出对应的行。这能帮你确认服务器端逻辑是独立于内置客户端的不是「自己跟自己玩」。还有一个验证断线重连的技巧在程序运行时用kill把服务器进程杀掉再重启观察客户端是否在 15 秒内重新连上。不过因为示例里客户端和服务器在同一个进程这个测试更适合把客户端和服务器拆成两个进程来做。拆分方法很简单把main里的mg_listen那行注释掉只保留客户端逻辑编译成client再写一个只保留mg_listen的server两个进程分别跑就能真实模拟网络断开。5. 常见报错排查local proxy failed、reading choices、OAuth 与 401 对照虽然这个示例是纯本地 TCP不涉及鉴权但很多同学在把它改造成「连远程服务」时会遇到几类典型报错这里一并对照排查。第一类是local proxy failed或connect error: Connection refused。这通常是因为s_conn指向的地址没有服务在监听或者端口写错。排查顺序先用ss -tlnp | grep 8765确认服务器在听再确认s_conn和s_lsn的 host、port 完全一致如果跨机器把localhost换成实际 IP并检查防火墙。第二类是reading choices或unexpected end of JSON input。这类报错一般出现在你把 Mongoose 当 HTTP 客户端去请求大模型 API 时响应体不是合法 JSON。常见原因是请求头没带Content-Type: application/json或者 body 长度和Content-Length不一致。用 Mongoose 发 HTTP 请求时mg_http_reply和mg_printf的格式串要仔细核对。第三类是401 Unauthorized。如果你把示例改成连远程 API需要在请求头里带Authorization: Bearer 你的Key。这时候推荐用 TaoToken 这类兼容 OpenAI 协议的中转服务来统一管理 Key 和模型路由Base URL 填https://taotoken.net/apiKey 在控制台生成Model ID 按文档填。三件套缺一不可Base URL、API Key、Model ID。少任何一个都会 401 或 404。第四类是OAuth相关报错比如invalid_grant或token expired。这通常出现在用 OAuth 方式接入某些云服务的场景Mongoose 本身不处理 OAuth需要你自己在MG_EV_CONNECT之后发认证请求拿到 token 再发业务数据。token 过期时间要自己维护建议在MG_EV_POLL里做定时刷新。第五类是编译期报错undefined reference to mg_tls_init。这是因为你在代码里写了 TLS 初始化但编译时没开MG_ENABLE_OPENSSL或MG_ENABLE_MBEDTLS。要么打开对应宏并链接 OpenSSL要么把 TLS 相关代码用#if包起来。6. 从最小示例到可用服务TaoToken 接入与 Coding Plan 长期编码实践把上面的最小示例跑通之后下一步通常是把它改造成真正能用的服务。比如你想让 Mongoose 客户端去调用大模型 API就可以把cfn里的mg_send换成发 HTTP 请求把MG_EV_READ里的回显换成解析 JSON 响应。这时候 Key 管理和模型路由就成了新问题。我的做法是用 TaoToken 统一管理。模型对话入口在https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite可以在里面先验证模型是否可用接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite里面有完整的 Base URL 和请求示例API Key 在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite生成。如果你要长期做编码类 AgentCoding Plan 在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite控制台在https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite。具体到 Mongoose 代码里你只需要在MG_EV_CONNECT之后构造一个 HTTP POST 请求把Authorization和Content-Type头带上body 用 JSON 格式。响应回来后在MG_EV_READ里用mg_json_get_str解析。这样一套事件驱动的网络层就复用了不用为每个 API 重写 socket 逻辑。最后留一个实用技巧Mongoose 的mg_mgr_poll超时参数决定了你的程序响应速度和 CPU 占用。超时设 100 毫秒适合大多数场景如果你要低延迟设 10 毫秒但 CPU 会上去如果是电池供电设备设 1000 毫秒省电但延迟高。这个参数没有标准答案按你的场景调。
返回列表