Mongoose 作为一个功能丰富的嵌入式网络库,自然也对常用的 TCP 客户端和 TCP 服务端功能进行了封装。
相信大多数有经验的开发者都有一套自己的 TCP 封装方法。我们学习 Mongoose 的实现,并非断言它是最优解,而是将其视为特定场景下的一种可靠选择。对于初学者而言,其清晰的事件驱动模型和代码结构也颇具借鉴价值。
TCP 服务端实现
实现一个 TCP 服务端的基本流程非常清晰:首先创建一个监听服务器,注册事件回调函数,然后在主循环中处理各类网络事件。这套模式与实现 HTTP Server 如出一辙,事实上,Mongoose 中各种网络服务器的实现都遵循这一范式。
以下是一个典型的服务端主函数框架:
int main(int argc, char *argv[])
{
/* 创建并初始化事件管理结构体 */
struct mg_mgr mgr;
mg_mgr_init(&mgr);
/* 创建一个tcp监听服务器,添加到指定的事件管理结构中 */
mg_listen(&mgr, "tcp://0.0.0.0:8000", fn, NULL);
/* 循环处理事件 */
for(;;){ mg_mgr_poll(&mgr, 1000); }
/* 关闭连接,释放资源 */
mg_mgr_free(&mgr);
exit(0);
}
这段代码中,核心在于 mg_listen() 接口,其他函数在前文中已有介绍。
mg_listen 接口详解
struct mg_connection *mg_listen(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data);
此接口用于创建一个监听服务器,支持 TCP 或 UDP 协议。
- 参数
mgr: 指向事件管理结构体。
- 参数
url: 指定监听的本地地址和端口,例如 tcp://127.0.0.1:1234 或 udp://0.0.0.0:9000。
- 参数
fn: 事件处理回调函数。
- 参数
fn_data: 传递给回调函数的用户自定义数据。
- 返回值: 成功返回指向已创建连接的指针,失败返回
NULL。
mg_listen 是 Mongoose 的核心 API 之一。诸如 mg_mqtt_listen() 和 mg_http_listen() 等高级协议接口,都是在它的基础上进行的简单封装。
struct mg_connection *mg_mqtt_listen(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data)
{
struct mg_connection *c = mg_listen(mgr, url, fn, fn_data);
if (c != NULL) c->pfn = mqtt_cb, c->pfn_data = mgr;
return c;
}
struct mg_connection *mg_http_listen(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data)
{
struct mg_connection *c = mg_listen(mgr, url, fn, fn_data);
if (c != NULL) c->pfn = http_cb;
return c;
}
......
我们可以深入查看 mg_listen 的部分实现细节:
struct mg_connection *mg_listen(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data)
{
struct mg_connection *c = NULL;
//申请连接结构体空间
if((c = mg_alloc_conn(mgr)) == NULL) {MG_ERROR(("OOM %s", url)); }
//执行socket创建、设置、绑定、监听等一系列操作
else if (!mg_open_listener(c, url))
{
MG_ERROR(("Failed: %s", url));
MG_PROF_FREE(c);
mg_free(c);
c = NULL;
}
//创建成功则进行连接结构体的一些参数赋值
else
{
c->is_listening = 1;
//如果URL是以 "udp:" 开头,则说明是 udp 连接
c->is_udp = strncmp(url, "udp:", 4) == 0;
//将连接结构添加到事件管理结构体中
LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c);
c->fn = fn;
c->fn_data = fn_data;
//检索 url 是否以 "wss:"、"mqtts:"、"ssl:"、"tls:"、"tcps:"、"https:" 等开头,
//若是则说明是 tls 类型的连接
c->is_tls = (mg_url_is_ssl(url) != 0);
//执行回调,传入的事件类型为 MG_EV_OPEN
mg_call(c, MG_EV_OPEN, NULL);
MG_DEBUG(("%lu %ld %s", c->id, c->fd, url));
}
return c;
}
由此可见,无论何种类型的网络连接,其底层无非基于 TCP 或 UDP,高级功能如 TLS 也是在此基础上添加。因此,底层的 socket 操作是共通的。不同类型连接的主要区别在于其专属的回调函数中对协议报文的处理逻辑。
这里曾有一个有趣的疑问:如果一个事件管理器上同时存在多个不同类型的连接(如 HTTP 和 MQTT),Mongoose 如何区分并调用对应的回调函数?是通过分析报文特征吗?
其实答案很简单:不同的服务监听不同的端口!连接本身已经通过端口区分开了。
更底层的 socket 操作在 mg_open_listener() 函数中完成,感兴趣的朋友可以自行阅读源码。
需要说明的是,类似 mg_open_listener() 这样的网络操作函数,在 Mongoose 源码中有两套实现。这是因为 Mongoose 内置了一个可选的嵌入式 TCP/IP 协议栈,用户可以通过头文件配置是否启用它:
#ifndef MG_ENABLE_TCPIP
#define MG_ENABLE_TCPIP 0 // Mongoose built-in network stack
#endif
#ifndef MG_ENABLE_SOCKET
#define MG_ENABLE_SOCKET !MG_ENABLE_TCPIP
#endif
显然,内置协议栈和传统的跨平台 socket 接口无法同时启用。关于内置 TCP/IP 协议栈的使用我们后续会单独讨论,本文聚焦于传统的 socket 接口。
回调函数事件处理
以下通过官方示例代码,说明服务端回调函数中常见事件的含义:
static void fn(struct mg_connection *c, int ev, void *ev_data)
{
//连接已创建(监听套接字),或在关闭后重新初始化
if(ev == MG_EV_OPEN && c->is_listening == 1)
{
MG_INFO(("SERVER is listening"));
}
//服务器接受了一个新连接。如果启用TLS,可在此初始化TLS。
else if(ev == MG_EV_ACCEPT)
{
MG_INFO(("SERVER accepted a connection"));
// if(mg_url_is_ssl(s_lsn)) {
// struct mg_tls_opts opts = {.ca = mg_unpacked("/certs/ss_ca.pem"),
// .cert = mg_unpacked("/certs/ss_server.pem"),
// .key = mg_unpacked("/certs/ss_server.pem")};
// mg_tls_init(c, &opts);
// }
}
//从socket接收到数据,数据存放在c->recv缓冲区中
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); // 回显数据
//消费完数据后,将长度置0,告知Mongoose可清空缓冲区
r->len = 0;
}
//连接关闭
else if(ev == MG_EV_CLOSE)
{
MG_INFO(("SERVER disconnected"));
}
//发生错误,错误信息通过ev_data(char*类型)传递
else if(ev == MG_EV_ERROR)
{
MG_INFO(("SERVER error: %s", (char *) ev_data));
}
}
mg_send 数据发送
bool mg_send(struct mg_connection *c, const void *data, size_t size);
该接口用于通过连接 c 发送指定长度的数据。成功返回 true,失败返回 false。
对于 UDP 连接,数据会立即发送。对于 TCP 连接,数据会被追加到输出缓冲区,真正的发送操作在 mg_mgr_poll() 中执行。这意味着多次调用 mg_send() 会导致输出缓冲区累积数据。
需要注意的是,官方文档说明:除非使用内置 TCP/IP 协议栈,否则该接口不会立即将数据推送到网络。但从代码实现看,UDP 数据是直接发送的。TCP 作为流式协议,mg_send() 负责写入缓冲区,mg_mgr_poll() 负责从缓冲区读取并发送,这符合其设计。
服务端代码演示
编译并运行上述服务端示例代码,使用网络调试助手作为客户端进行连接和测试。

终端编译运行结果,显示服务器启动、接受连接、接收并回显“hello world”数据的过程。

网络调试助手成功连接服务器,发送“hello world”并收到相同回复。
TCP 客户端实现
首先了解客户端的连接建立接口:
mg_connect 接口
struct mg_connection *mg_connect(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data);
此接口创建一个客户端连接并启动连接流程。实际的连接动作在 mg_mgr_poll() 中异步执行,连接状态通过事件回调通知。
其参数含义与 mg_listen() 基本一致,此处不再赘述。
客户端回调函数事件处理
static void fn(struct mg_connection *c, int ev, void *ev_data)
{
//连接结构体已初始化
if (ev == MG_EV_OPEN)
{
MG_INFO(("CLIENT has been initialized"));
}
//已成功连接到服务器。可在此初始化TLS。
else if (ev == MG_EV_CONNECT)
{
MG_INFO(("CLIENT connected"));
}
//接收到服务器数据
else if (ev == MG_EV_READ)
{
struct mg_iobuf *r = &c->recv;
MG_INFO(("CLIENT got data: %.*s", r->len, r->buf));
r->len = 0; // 告知Mongoose数据已消费
}
//连接关闭
else if (ev == MG_EV_CLOSE)
{
MG_INFO(("CLIENT disconnected"));
}
//发生错误
else if (ev == MG_EV_ERROR)
{
MG_INFO(("CLIENT error: %s", (char *) ev_data));
}
//事件管理器每次轮询都会产生此事件,可用于执行定时任务
else if (ev == MG_EV_POLL)
{
// 可在此处执行定期操作,如检查发送队列
}
}
可见,在单线程模型下,数据的收发、断线重连等逻辑主要都在回调函数中处理。例如,可以将待发送数据置于全局缓冲区,在 MG_EV_POLL 事件中检查并发送;通过一个定时器任务定期检查网络状态,实现断线自动重连。
客户端完整代码演示
以下是一个实现了简易回声(echo)及断线重连功能的客户端示例:
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include "mongoose.h"
//客户端信息结构体
static struct client_t{
struct mg_connection *c;
bool s_connect;
bool s_send;
uint16_t sendlen;
uint8_t sendbuf[128];
}client;
static void fn(struct mg_connection *c, int ev, void *ev_data)
{
//连接结构体已初始化
if(ev == MG_EV_OPEN)
{
printf(("CLIENT has been initialized"));
}
//已成功连接到服务器。可在此初始化TLS。
else if(ev == MG_EV_CONNECT)
{
printf(("CLIENT connected"));
// if (mg_url_is_ssl(s_conn)) {
// struct mg_tls_opts opts = {.ca = mg_unpacked("/certs/ss_ca.pem"),
// .cert = mg_unpacked("/certs/ss_client.pem"),
// .key = mg_unpacked("/certs/ss_client.pem")};
// mg_tls_init(c, &opts);
// }
client.s_connect = true;
}
//接收到服务器数据
else if(ev == MG_EV_READ)
{
struct mg_iobuf *r = &c->recv;
printf("CLIENT got data: %.*s\r\n", (int)r->len, r->buf);
if(client.s_send == false)
{
memcpy(client.sendbuf, r->buf, r->len);//简单拷贝,未做校验
client.sendlen = r->len;
client.s_send = true;
}
r->len = 0;
}
//连接关闭
else if(ev == MG_EV_CLOSE)
{
printf("CLIENT disconnected\r\n");
client.s_connect = false;
client.c = NULL;//客户端连接清空
}
//发生错误
else if(ev == MG_EV_ERROR)
{
printf("CLIENT error: %s\r\n", (char *) ev_data);
}
//在MG_EV_POLL事件中处理数据发送
else if(ev == MG_EV_POLL)
{
if(client.s_connect == true && client.s_send == true && client.sendlen > 0)
{
mg_send(c, client.sendbuf, client.sendlen);
client.s_send = false;
}
}
}
static void timer_fn(void *arg)
{
struct mg_mgr *mgr = (struct mg_mgr *) arg;
if(client.c == NULL)
{
//初始化客户端状态
client.s_send = false;
client.s_connect = false;
client.sendlen = 0;
memset(client.sendbuf, 0, sizeof(client.sendbuf));
//发起连接
mg_connect(mgr, "tcp://192.168.11.236:8000", fn, NULL);
}
}
int main(int argc, char *argv[])
{
/* 创建并初始化事件管理结构体 */
struct mg_mgr mgr;
mg_mgr_init(&mgr);
/* 创建一个定时器,用于初始连接和断线重连 */
mg_timer_add(&mgr, 15000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_fn, &mgr);
/* 循环处理事件 */
for(;;){ mg_mgr_poll(&mgr, 1000); }
/* 关闭连接,释放资源 */
mg_mgr_free(&mgr);
exit(0);
}
运行上述客户端代码,并搭配一个独立的 TCP Server 工具进行测试。

客户端运行输出,显示成功连接并接收数据。

网络调试助手作为服务端,显示客户端连接上线,并完成双向数据收发。
需要注意的是,在这种架构下,mg_mgr_poll() 的轮询间隔时间直接影响了数据发送和状态检查的频率。
通过本文对 Mongoose 库中 TCP 客户端和服务端核心接口及事件模型的剖析,我们可以看到其清晰的事件驱动设计。这种模式在资源受限的嵌入式环境或需要处理大量并发连接的场景中尤其有效。希望这篇结合代码实例的讲解能帮助你更高效地使用 Mongoose 进行网络开发。如果你想深入探讨更多网络编程或嵌入式开发实践,欢迎在 云栈社区 交流分享。