1. 为什么在嵌入式项目里,我选择了Mongoose

如果你和我一样,经常在资源紧张的嵌入式设备上折腾网络通信,那你肯定理解那种“既要马儿跑,又要马儿不吃草”的纠结。设备内存可能就几十KB,Flash空间也捉襟见肘,但需求却很“丰满”:既要能通过HTTP从云端拉取个配置、上报个状态,又希望能用WebSocket维持一个长连接,实时接收服务器的指令或者推送数据。这时候,如果你去搬那些为桌面或服务器环境设计的大型网络库,光是编译出来的体积可能就比你的主程序还大,更别提那些复杂的依赖和交叉编译的坑了。

几年前我在做一个智能家居的网关项目时,就深陷这种困境。直到我遇到了 Mongoose,它简直是为嵌入式场景量身定做的“瑞士军刀”。它不是某个庞大框架的一部分,核心就是两个文件:mongoose.cmongoose.h。对,你没看错,就这两个文件。你不需要去折腾 openssllibevent 这些外部依赖,直接把这两个文件拖进你的工程,包含头文件,立刻就能开始写网络代码。这种极简的集成方式,对于嵌入式开发来说,幸福感直接拉满。我不用再担心库的版本冲突,也不用在构建系统里写一堆复杂的链接指令,整个项目的复杂度和维护成本都降下来了。

更重要的是,Mongoose 采用了一种 事件驱动、非阻塞 的模型。这听起来有点高大上,但其实很好理解。想象一下你的设备只有一个主循环(while(1)),传统的阻塞式网络调用会让整个循环“卡住”,直到收到数据或超时,这期间设备啥也干不了。而 Mongoose 的做法是,你把网络连接的管理权交给它,它会在后台默默地处理所有 socket 的监听、连接、收发数据。你只需要在一个统一的 事件处理函数 里,根据不同类型的事件(比如连接成功、收到HTTP回复、收到WebSocket消息)来写你的业务逻辑。主循环里只需要周期性地调用一个 mg_mgr_poll() 函数,Mongoose 就会去检查有没有网络事件需要处理,有就回调你的函数,没有就立刻返回,绝不拖沓。这样,你的设备主循环依然能保持流畅,同时处理网络任务,实现了高效的异步通信。

所以,总结下来,我在嵌入式里青睐 Mongoose 就三点:单文件集成,零依赖,事件驱动。它用最轻量级的方式,同时解决了 HTTP 和 WebSocket 这两大主流通信协议的需求,让我们能在资源受限的环境里,依然实现稳定、高效的双模网络通信。下面,我就带你亲手实践,看看怎么用这把“瑞士军刀”来干活。

2. 5分钟搞定你的第一个嵌入式HTTP客户端

让我们先从相对简单的 HTTP 客户端开始。假设你有一个智能传感器,需要定时向一个云服务器上报温湿度数据,这个场景用 HTTP POST 请求再合适不过了。使用 Mongoose 来实现,整个过程清晰得超乎想象。

首先,当然是引入 Mongoose。在你的项目源文件里,确保包含了头文件,并初始化一个全局的事件管理器(struct mg_mgr)。这个管理器是 Mongoose 的核心,它负责调度所有网络连接。

#include "mongoose.h"

// 全局的事件管理器
struct mg_mgr mgr;
// 一个标志位,用来控制主循环
static int s_exit_flag = 0;

接下来,是整个逻辑的灵魂——事件处理函数。这个函数是一个回调,Mongoose 会在网络连接生命周期的各个关键时刻调用它,并通过 ev 参数告诉你发生了什么事件。

static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
    struct http_message *hm = (struct http_message *) ev_data;

    switch (ev) {
        case MG_EV_CONNECT: {
            // 连接尝试完成
            int connect_status = *(int *)ev_data;
            if (connect_status != 0) {
                printf("[ERROR] 连接服务器失败: %s\n", strerror(connect_status));
                s_exit_flag = 1; // 设置退出标志
            } else {
                printf("[INFO] TCP连接已建立,正在发送HTTP请求...\n");
            }
            break;
        }
        case MG_EV_HTTP_REPLY: {
            // 收到HTTP响应!这是我们最关心的时刻
            printf("[INFO] 收到服务器响应,状态码: %d\n", hm->resp_code);
            printf("[INFO] 响应体 (长度 %d 字节):\n%.*s\n",
                   (int)hm->body.len, (int)hm->body.len, hm->body.p);

            // 标记连接为“发送后关闭”,处理完本次响应就断开
            nc->flags |= MG_F_SEND_AND_CLOSE;
            s_exit_flag = 1; // 请求完成,准备退出循环
            break;
        }
        case MG_EV_CLOSE: {
            // 连接关闭
            printf("[INFO] 连接已关闭。\n");
            if (s_exit_flag == 0) {
                printf("[WARN] 连接意外关闭。\n");
                s_exit_flag = 1;
            }
            break;
        }
        default:
            break;
    }
}

我来解释一下这几个关键事件:

  • MG_EV_CONNECT:发生在 TCP 连接建立之后。这里的 ev_data 是一个指向连接状态码的指针。如果状态码是 0,恭喜你,到服务器的路通了;如果不是 0,那就是连接出错,我们可以根据错误码(比如 errno)打印错误信息。
  • MG_EV_HTTP_REPLY这是收到完整 HTTP 响应时触发的事件,是处理服务器返回数据的核心。ev_data 这时是一个 struct http_message 指针,里面包含了状态码、响应头、响应体等所有信息。我们可以直接通过 hm->body.phm->body.len 来访问响应正文。
  • MG_EV_CLOSE:连接关闭时触发,用于做一些清理工作或状态通知。

写好事件处理器,发起请求就很简单了。我们封装一个函数:

int http_post_sensor_data(const char *url, const char *post_data) {
    struct mg_connection *nc;

    // 1. 初始化管理器
    mg_mgr_init(&mgr, NULL);

    // 2. 建立HTTP连接。注意这里最后一个参数是额外的HTTP头,我们用来指定Content-Type
    struct mg_str extra_headers = mg_mk_str("Content-Type: application/json\r\n");
    nc = mg_connect_http(&mgr, ev_handler, url, &extra_headers, post_data);

    if (nc == NULL) {
        printf("[ERROR] 创建连接失败,请检查URL格式。\n");
        mg_mgr_free(&mgr);
        return -1;
    }

    printf("[INFO] 开始向 %s 发送数据...\n", url);

    // 3. 事件循环:不断轮询,直到请求完成(s_exit_flag被置为1)
    while (s_exit_flag == 0) {
        mg_mgr_poll(&mgr, 1000); // 每次轮询等待最多1000毫秒
    }

    // 4. 释放管理器资源
    mg_mgr_free(&mgr);
    return 0;
}

在实际调用时,你可以这样使用:

int main() {
    const char *server_url = "http://api.yourserver.com/sensor_data";
    const char *json_data = "{\"temperature\": 25.6, \"humidity\": 60}";

    http_post_sensor_data(server_url, json_data);
    return 0;
}

整个流程跑下来,你会发现代码非常紧凑。Mongoose 帮你封装了底层的 socket 操作、HTTP 协议组装与解析,你只需要关注“连接”、“收数据”这些业务事件。在资源有限的嵌入式设备上,这段代码的内存占用和性能开销都极小,却能可靠地完成 HTTP 通信任务。

2.1 处理更复杂的请求与响应

上面的例子是最简单的 POST 请求。在实际项目中,你可能需要处理更复杂的情况,比如:

  • 添加认证头:在 extra_headers 里加上 Authorization: Bearer your_token
  • 处理 chunked 编码:Mongoose 的 MG_EV_HTTP_CHUNK 事件可以帮你处理分块传输的响应体。
  • 超时控制:可以在 mg_mgr_poll 循环中加入自己的计时器,如果长时间没收到 MG_EV_HTTP_REPLY 就主动断开连接并重试。
  • SSL/TLS 支持:Mongoose 也内置了 TLS 支持(需要 MG_ENABLE_SSL 编译选项和相应的后端如 mbedTLS),只需将 URL 的协议头从 http:// 改为 https:// 即可,对于物联网设备安全连接云端至关重要。

通过灵活配置事件处理器和连接参数,你可以用这同一套轻量级的框架,应对绝大多数嵌入式场景下的 HTTP 客户端需求。

3. 实现一个交互式嵌入式WebSocket客户端

如果说 HTTP 是“一问一答”的写信,那 WebSocket 就是“随时畅聊”的电话。对于需要服务器主动向设备推送数据(如实时控制指令、告警通知)或者设备需要高频上报数据的场景,WebSocket 的长连接特性优势巨大。用 Mongoose 实现一个 WebSocket 客户端,同样优雅而简单。

我们先来看核心的事件处理器。WebSocket 的生命周期事件和 HTTP 有所不同,主要关注握手、收发消息和连接状态。

#include "mongoose.h"

static int s_done = 0; // 主循环退出标志
static int s_is_connected = 0; // WebSocket连接状态标志

static void ws_ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
    switch (ev) {
        case MG_EV_CONNECT: {
            // 底层TCP连接结果
            int status = *((int *)ev_data);
            if (status != 0) {
                printf("[ERROR] WebSocket连接错误: %d\n", status);
                s_done = 1;
            }
            break;
        }
        case MG_EV_WEBSOCKET_HANDSHAKE_DONE: {
            // WebSocket握手成功!连接正式建立
            struct http_message *hm = (struct http_message *) ev_data;
            if (hm->resp_code == 101) { // 101 Switching Protocols
                printf("[INFO] WebSocket连接成功!\n");
                s_is_connected = 1;
                // 连接成功后,可以立即发送一条问候消息
                mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT, "Hello Server!", 13);
            } else {
                printf("[ERROR] WebSocket握手失败,HTTP状态码: %d\n", hm->resp_code);
                s_done = 1;
            }
            break;
        }
        case MG_EV_WEBSOCKET_FRAME: {
            // 收到WebSocket数据帧
            struct websocket_message *wm = (struct websocket_message *) ev_data;
            printf("[RECV] %.*s\n", (int)wm->size, wm->data);
            // 这里可以解析JSON指令,或者进行其他业务处理
            // 例如:if (strstr(wm->data, "TURN_ON")) { gpio_set(LED_PIN, 1); }
            break;
        }
        case MG_EV_CLOSE: {
            // 连接关闭
            if (s_is_connected) {
                printf("[INFO] WebSocket连接断开。\n");
            }
            s_done = 1;
            break;
        }
        default:
            break;
    }
}

关键事件解读:

  • MG_EV_WEBSOCKET_HANDSHAKE_DONE:这是 WebSocket 连接建立的标志。握手成功后,连接就从普通的 HTTP 升级为 WebSocket 协议了。此时可以设置连接状态标志,并开始发送数据。
  • MG_EV_WEBSOCKET_FRAME这是接收数据的核心事件。每当收到一帧 WebSocket 数据(无论是文本还是二进制),都会触发此事件。ev_datastruct websocket_message,通过 wm->datawm->size 就能拿到数据内容。
  • 发送数据使用 mg_send_websocket_frame 函数,可以指定操作码(WEBSOCKET_OP_TEXTWEBSOCKET_OP_BINARY)和数据。

主函数负责建立连接和运行事件循环:

int main(int argc, char *argv[]) {
    struct mg_mgr mgr;
    struct mg_connection *nc;
    // 默认连接本地服务器的8000端口,也可以通过命令行参数指定
    const char *ws_server_url = argc > 1 ? argv[1] : "ws://127.0.0.1:8000";

    mg_mgr_init(&mgr, NULL);

    // 发起WebSocket连接。第四个参数是子协议(如“chat”, “mqtt”),可以为NULL
    nc = mg_connect_ws(&mgr, ws_ev_handler, ws_server_url, NULL, NULL);
    if (nc == NULL) {
        fprintf(stderr, "[ERROR] 无效的WebSocket地址: %s\n", ws_server_url);
        return 1;
    }

    printf("[INFO] 正在连接WebSocket服务器: %s\n", ws_server_url);

    // 主事件循环
    while (!s_done) {
        mg_mgr_poll(&mgr, 100); // 每100毫秒轮询一次,响应更及时
    }

    mg_mgr_free(&mgr);
    printf("[INFO] 程序退出。\n");
    return 0;
}

3.1 处理跨平台用户输入与心跳保活

一个真正的交互式客户端,除了接收服务器消息,往往还需要能发送用户输入。在嵌入式环境中,“用户输入”可能来自串口、按键,或者像在桌面测试时来自标准输入(stdin)。Mongoose 官方示例提供了一个巧妙的思路:在 MG_EV_POLL 事件里检查输入。

MG_EV_POLL 事件在每次 mg_mgr_poll() 被调用时都会触发,我们可以在这里非阻塞地检查是否有输入可用。下面的代码展示了如何兼容 Windows 和 Linux/Unix 平台读取 stdin:

        case MG_EV_POLL: {
            // 这是一个在每次poll时都会触发的事件,适合做非阻塞的输入检查
            char msg[128];
            int n = 0;

#if defined(_WIN32)
            // Windows控制台输入处理(略,见原始示例)
#else
            // Linux/Unix/嵌入式系统使用select检查stdin
            fd_set read_set;
            struct timeval timeout = { 0, 0 }; // 立即返回,不阻塞
            FD_ZERO(&read_set);
            FD_SET(STDIN_FILENO, &read_set); // 监视标准输入

            if (select(STDIN_FILENO + 1, &read_set, NULL, NULL, &timeout) == 1) {
                n = read(STDIN_FILENO, msg, sizeof(msg) - 1); // 留一位给'\0'
            }
#endif
            if (n > 0) {
                msg[n] = '\0'; // 确保字符串终止
                // 去掉末尾的换行符
                while (n > 0 && (msg[n-1] == '\r' || msg[n-1] == '\n')) {
                    msg[--n] = '\0';
                }
                if (n > 0 && s_is_connected) {
                    printf("[SEND] %s\n", msg);
                    mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT, msg, n);
                }
            }
            break;
        }

在实际的嵌入式设备上,你可以把 FD_SET(STDIN_FILENO, &read_set)read(STDIN_FILENO, ...) 替换成对你自己的输入设备(如串口 uart_fd)的检查,逻辑是完全通用的。

另一个生产环境中必不可少的功能是 心跳保活(Ping/Pong)。WebSocket 协议本身支持 Ping/Pong 帧来探测连接是否存活。Mongoose 可以自动发送 Ping,你只需要在初始化连接后设置一个定时器,定期发送 Ping 帧,或者处理服务器发来的 Ping。更常见的做法是,在应用层定义一个简单的“心跳”JSON消息,定期发送,服务器回复,以此来判断网络和业务层的连通性。这可以在 MG_EV_POLL 事件中结合一个硬件定时器或软件计数器来实现。

4. 进阶技巧:在同一个项目中驾驭HTTP与WebSocket

很多实际项目并不是二选一,而是需要同时使用 HTTP 和 WebSocket。比如,设备启动时用 HTTP 获取初始化配置,然后建立一个 WebSocket 长连接进行实时数据交互和指令接收。用 Mongoose 管理这两种连接,不仅可行,而且非常高效,因为它们共享同一个 mg_mgr 事件管理器。

关键在于设计好你的 事件分发器。你只有一个事件处理函数入口,但需要根据连接的类型或你自己设置的标记,来区分这个事件是来自 HTTP 连接还是 WebSocket 连接。

一个实用的方法是利用 mg_connection 结构体的 user_data 字段。这是一个 void* 指针,你可以在创建连接时,给它绑定一个自定义的数据结构,用来标识连接的类型和上下文。

// 定义一个枚举来标识连接类型
typedef enum {
    CONN_TYPE_UNKNOWN,
    CONN_TYPE_HTTP_CLIENT,
    CONN_TYPE_WS_CLIENT
} conn_type_t;

// 一个简单的连接上下文结构
struct conn_context {
    conn_type_t type;
    void *specific_data; // 可以指向更具体的HTTP或WS上下文
};

// 统一的全局事件处理器
static void global_ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
    struct conn_context *ctx = (struct conn_context *)nc->user_data;

    if (ctx == NULL) {
        // 没有上下文,可能是监听连接或其他,简单处理或忽略
        return;
    }

    // 根据连接类型,分发到不同的处理函数
    switch (ctx->type) {
        case CONN_TYPE_HTTP_CLIENT:
            http_client_handler(nc, ev, ev_data, ctx);
            break;
        case CONN_TYPE_WS_CLIENT:
            websocket_client_handler(nc, ev, ev_data, ctx);
            break;
        default:
            break;
    }
}

// 创建HTTP客户端连接时
struct mg_connection *create_http_conn(struct mg_mgr *mgr, const char *url) {
    struct mg_connection *nc = mg_connect_http(mgr, global_ev_handler, url, NULL, NULL);
    if (nc) {
        struct conn_context *ctx = (struct conn_context *)calloc(1, sizeof(*ctx));
        ctx->type = CONN_TYPE_HTTP_CLIENT;
        // 可以初始化specific_data,比如存放请求ID、回调函数等
        nc->user_data = ctx;
    }
    return nc;
}

// 创建WebSocket客户端连接时
struct mg_connection *create_ws_conn(struct mg_mgr *mgr, const char *url) {
    struct mg_connection *nc = mg_connect_ws(mgr, global_ev_handler, url, NULL, NULL);
    if (nc) {
        struct conn_context *ctx = (struct conn_context *)calloc(1, sizeof(*ctx));
        ctx->type = CONN_TYPE_WS_CLIENT;
        nc->user_data = ctx;
    }
    return nc;
}

这样,在 http_client_handlerwebsocket_client_handler 这两个具体的处理函数里,你就可以专注于各自协议的事件逻辑了。主循环依然只有一个 mg_mgr_poll(&mgr, 100),Mongoose 会帮你处理所有连接上的事件,并回调到 global_ev_handler,再由它进行精准分发。

4.1 资源管理、调试与性能考量

在嵌入式环境中,资源管理必须小心翼翼。这里有几点我踩过坑后的经验:

  1. 连接超时与重连:网络是不稳定的。无论是 HTTP 请求还是 WebSocket 连接,都必须实现超时机制。对于 HTTP,可以在发起请求时记录时间戳,在 MG_EV_POLL 中检查是否超时,超时则关闭连接(nc->flags |= MG_F_CLOSE_IMMEDIATELY)并触发重试。对于 WebSocket,除了连接超时,还要处理断线重连。可以在 MG_EV_CLOSE 事件中,启动一个重连定时器,几秒后尝试重新调用 mg_connect_ws
  2. 内存释放:记得在连接关闭(MG_EV_CLOSE)时,释放你通过 nc->user_data 分配的自定义上下文内存。同时,在程序退出前,确保调用 mg_mgr_free(&mgr) 来释放 Mongoose 内部管理的所有资源。
  3. 调试输出:Mongoose 本身有丰富的调试选项。在编译时定义 MG_ENABLE_DEBUG=1MG_ENABLE_LOG=1,并在代码中调用 mg_set_log_level(LOG_LEVEL_VERBOSE),可以在串口或日志中看到详细的网络事件和内部状态,对于排查握手失败、数据收发问题非常有帮助。
  4. 性能与内存mg_mgr_poll 的第二个参数是超时时间(毫秒)。在只有网络任务的系统中,可以设置一个较小的值(如 50-100ms)以获得更快的响应。但在一个复杂的、有大量其他任务(如传感器采样、显示刷新)的系统中,需要平衡网络响应性和 CPU 占用率,可能需要设置更大的超时(如 200-500ms)。监控你的堆栈使用情况,确保事件处理函数不会执行太耗时的操作,以免阻塞其他网络连接的处理。

将 Mongoose 集成到你的嵌入式 RTOS(如 FreeRTOS)中也是完全可行的。你可以创建一个独立的网络任务,在这个任务中运行 while(1) { mg_mgr_poll(&mgr, 10); vTaskDelay(5); } 这样的循环。确保这个任务的堆栈足够大,并且注意线程安全,如果从其他任务向连接发送数据,可能需要使用队列或信号量进行同步。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐