安装与基础配置

**Mongoose 简介:**Mongoose 是一个用于 C/C++ 的嵌入式网络库,提供事件驱动的非阻塞 API 来处理 TCP、UDP、HTTP、WebSocket、MQTT 等协议 (Mongoose :: Documentation)。它跨平台支持 Windows、Linux、MacOS 以及 STM32、ESP32 等嵌入式设备,并可在操作系统上运行或在裸机上使用内置的 TCP/IP 协议栈 (Mongoose :: Documentation) (GitHub - cesanta/mongoose: Embedded web server, with TCP/IP network stack, MQTT and Websocket)。自 2004 年以来,众多产品使用 Mongoose 实现联网功能,其可靠性经过了长期检验。

**获取与集成:**Mongoose 以 单个源文件库 形式提供,非常易于集成。官方推荐的集成步骤如下 (GitHub - cesanta/mongoose: Embedded web server, with TCP/IP network stack, MQTT and Websocket):

  1. **下载源码:**从官方仓库获取 mongoose.cmongoose.h 文件,并将其复制到工程的源代码目录中 (Mongoose :: Documentation)。这两个文件包含了 Mongoose 库的全部实现。
  2. **包含头文件:**在你的 C 源文件(如 main.c)中包含头文件 mongoose.h (Mongoose :: Documentation)。
  3. **编译链接:**将 mongoose.c 添加到工程一起编译。例如,使用 GCC 时,可以直接将它与应用代码一起编译:
    gcc main.c mongoose.c -o myapp
    
    Mongoose 无需其他依赖库,一般无需特殊链接选项。
  4. **基础初始化:**在代码中,创建一个事件管理器(struct mg_mgr),调用 mg_mgr_init() 初始化它 (Mongoose :: Documentation)。之后,可调用相应的监听或连接函数(例如 mg_http_listen())启动网络服务 (Mongoose :: Documentation)。最后在主循环中反复调用 mg_mgr_poll() 处理事件 (Mongoose :: Documentation)。下面给出一个最小化示例程序。
#include "mongoose.h"

// 事件回调函数,处理传入连接的各类事件
static void fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_HTTP_MSG) {                             // 收到 HTTP 请求 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=static%20void%20fn%28struct%20mg_connection%20,else))
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;  // 解析后的HTTP请求 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=static%20void%20fn%28struct%20mg_connection%20,else))
    if (mg_match(hm->uri, mg_str("/api/hello"), NULL)) {   // 判断URI是否为 /api/hello ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=if%20%28mg_match%28hm,else))
      // 返回一个 JSON 响应,内容 {"status": 1}
      mg_http_reply(c, 200, "", "{%m:%d}\n", MG_ESC("status"), 1);    // 回复 JSON ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=if%20%28mg_match%28hm,%2F%2F%20Serve%20static))
    } else {
      // 对于其他请求,作为静态文件请求处理,根目录为当前目录
      struct mg_http_serve_opts opts = {.root_dir = "."};  // 文档根目录 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=%7D%20else%20,%2F%2F%20Serve%20static%20files))
      mg_http_serve_dir(c, hm, &opts);                     // 托管目录下的静态文件 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=%7D%20else%20,%2F%2F%20Serve%20static%20files))
    }
  }
}

int main(void) {
  struct mg_mgr mgr;                     // Mongoose 事件管理器 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=struct%20mg_mgr%20mgr%3B%20%20%2F%2F,%2F%2F%20Infinite%20event%20loop))
  mg_mgr_init(&mgr);                     // 初始化事件管理器 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=struct%20mg_mgr%20mgr%3B%20%20%2F%2F,%2F%2F%20Infinite%20event%20loop))
  mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL);  // 监听 HTTP 8000 端口 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=struct%20mg_mgr%20mgr%3B%20%20%2F%2F,%2F%2F%20Infinite%20event%20loop))
  for (;;) {
    mg_mgr_poll(&mgr, 1000);             // 事件循环,每秒阻塞最多1000ms ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=mg_http_listen%28%26mgr%2C%20,return%200))
  }
  mg_mgr_free(&mgr);                     // 程序结束时清理,关闭所有连接
  return 0;
}

上述示例展示了 Mongoose 的基本集成和用法:初始化管理器,启动一个 HTTP 服务,并在事件回调中处理请求。编译运行该程序后,用浏览器访问 http://localhost:8000 即可看到目录列表或 API 响应 (Mongoose :: Documentation)。

**嵌入式构建注意:**如果在嵌入式平台上构建,建议创建一个自定义配置头文件 mongoose_config.h 并根据需要定义宏来启用特定功能或驱动。在编译时定义 -DMG_ARCH=<Arch> 指定体系架构,或在 mongoose_config.h 内定义。如需启用 Mongoose 内置的 TLS 支持或嵌入式网络驱动,可定义相应宏(后文详述) (Mongoose :: Documentation)。详情可参考官方文档的“Build options”部分 (Mongoose :: Documentation)。

事件循环与连接管理

Mongoose 使用事件驱动机制,通过一个事件管理器(struct mg_mgr)统一管理所有网络连接和事件调度 (Mongoose :: Documentation)。开发者需要提供事件回调函数来处理各种事件(如连接就绪、数据到达等),Mongoose 将在内部检测网络I/O并调用回调函数。

mg_mgr 与 mg_connection

  • struct mg_mgr(事件管理器):保存当前所有活动连接列表。使用 mg_mgr_init() 初始化后,即可用于创建监听或发起连接 (Mongoose :: Documentation)。一个进程通常只需一个全局的 mg_mgr 实例(除非需要多个独立事件循环)。
  • struct mg_connection(连接结构):表示一个网络连接,可以是监听socket、传入的客户端连接,或由本地主动发起的外连。每个连接在内部由 Mongoose 链入 mg_mgr 的链表进行管理 (Mongoose :: Documentation)。mg_connection 包含许多字段,包括连接的状态标志、缓冲区等。所有字段默认对应用代码可见,以便高级场景下获得最大灵活性 (Mongoose :: Documentation)。

**初始化事件管理器:**使用 mg_mgr_init(struct mg_mgr *mgr) 初始化事件管理器结构 (Mongoose :: Documentation)。该函数会设置内部连接列表为空、配置默认 DNS 服务器等。一般在程序开始时调用一次。对应的,在程序结束或不再需要网络功能时,应调用 mg_mgr_free(struct mg_mgr *mgr) 来关闭所有连接并释放资源 (Mongoose :: Documentation)。

struct mg_mgr mgr;
mg_mgr_init(&mgr);   // 初始化事件管理器 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=struct%20mg_mgr%20mgr%3B%20mg_mgr_init))
// ... 创建连接或监听 ...
// 事件循环(见下)...
mg_mgr_free(&mgr);   // 关闭所有连接并释放资源 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=Usage%20example%3A))

**事件循环:**Mongoose 不使用独立线程,而需要显式调用 mg_mgr_poll(struct mg_mgr *mgr, int ms) 来驱动事件循环 (Mongoose :: Documentation)。mg_mgr_poll() 会检查所有连接的网络I/O事件,行为包括:

  • 接受新的传入连接(对于监听socket)并产生 MG_EV_ACCEPT 事件 (Mongoose :: Documentation)。
  • 读取各连接的传入数据,将数据填充到连接的接收缓冲区,并向该连接发送 MG_EV_READ 事件 (Mongoose :: Documentation)。
  • 将各连接待发送缓冲区中的数据写出网络,并在成功写出后产生 MG_EV_WRITE 事件 (Mongoose :: Documentation)。
  • 关闭已标记关闭或发生错误的连接,并发送 MG_EV_CLOSE 事件。
  • 每次调用则发送一次 MG_EV_POLL 事件用于让用户执行定时操作(如定时输出) (Mongoose :: Documentation)。

参数 ms 指定最多阻塞等待的毫秒数(通常为几毫秒到几百毫秒)。如果为 0,则 mg_mgr_poll() 会立即返回(非阻塞)。通常将其置于 forwhile 无限循环中不断调用,以持续处理事件 (Mongoose :: Documentation)。例如:

while (running) {
  mg_mgr_poll(&mgr, 1000);  // 等待并处理事件,超时1秒
}

**事件类型:**事件回调函数的签名固定为 void fn(struct mg_connection *c, int ev, void *ev_data)。其中 c 是发生事件的连接,ev 是事件类型代码,ev_data 是与事件相关的数据指针(不同事件类型下指向不同类型的数据) (Mongoose :: Documentation)。常见事件及其意义如下:

事件常量 说明 ev_data 类型
MG_EV_ERROR 发生错误(连接失败等) char *(错误消息字符串) (Mongoose :: Documentation)
MG_EV_OPEN 新连接创建(调用 connect/listen) NULL (Mongoose :: Documentation)
MG_EV_POLL 每轮循环的空闲事件 uint64_t *(当前运行毫秒数) (Mongoose :: Documentation)
MG_EV_RESOLVE 名称解析完成(DNS 解析结果) NULL(解析成功/失败由后续事件表示)
MG_EV_CONNECT 连接已建立(TCP 三次握手完成) NULL (Mongoose :: Documentation)
MG_EV_ACCEPT 接受到新传入连接(监听端口) NULL (Mongoose :: Documentation)
MG_EV_TLS_HS TLS 握手成功 NULL (Mongoose :: Documentation)
MG_EV_READ 收到数据 long *(读取的字节数) (Mongoose :: Documentation)
MG_EV_WRITE 数据已写出 long *(写出的字节数) (Mongoose :: Documentation)
MG_EV_CLOSE 连接已关闭(无论正常或异常) NULL (Mongoose :: Documentation)

此外,不同协议还有特定的高级事件,例如:

  • MG_EV_HTTP_MSG:收到了完整的 HTTP 请求(对服务端)或响应(对客户端)。ev_datastruct mg_http_message * (Mongoose :: Documentation)。
  • MG_EV_WS_MSG:收到 WebSocket 数据帧,ev_datastruct mg_ws_message * (Mongoose :: Documentation)。
  • MG_EV_MQTT_MSG:收到 MQTT 发布消息(PUBLISH),ev_datastruct mg_mqtt_message * (Mongoose :: Documentation)。
  • MG_EV_MQTT_OPEN:MQTT 连接成功(收到 CONNACK),ev_dataint *(CONNACK返回码) (Mongoose :: Documentation)。
  • MG_EV_SNTP_TIME:收到 SNTP 时间同步响应,ev_datauint64_t *(UNIX时间毫秒)。
  • MG_EV_WAKEUP:其他线程通过 mg_wakeup() 传递的数据到达,ev_datastruct mg_str *(参见后文) (Mongoose :: Documentation)。

在事件回调中,根据 ev 判断事件类型,并通过 ev_data 获取相关数据,编写相应的处理逻辑。例如,对 MG_EV_READ,应用可调用 mg_send()c->recv 缓冲的数据发回,实现一个简单的回显服务器 (Mongoose :: Documentation)。

连接的创建与关闭

**监听连接:**使用 mg_listen(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data) 在指定地址上创建一个监听连接 (Mongoose :: Documentation) (Mongoose :: Documentation)。url 参数采用协议://IP:端口 的形式,例如 "tcp://0.0.0.0:1234" 表示监听 TCP 1234 端口,"udp://0.0.0.0:9000" 表示监听 UDP 9000端口 (Mongoose :: Documentation)。成功时返回 mg_connection * 指针,失败返回 NULL (Mongoose :: Documentation)。创建后,对应的 fn 回调会收到传入连接的事件(如 MG_EV_ACCEPT)。例子:

mg_connection *lc = mg_listen(&mgr, "tcp://0.0.0.0:1234", fn, NULL);
if (lc == NULL) {
  printf("监听端口失败\n");
}

发起客户端连接:使用 mg_connect(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data) 发起一个主动连接 (Mongoose :: Documentation)。例如 mg_connect(&mgr, "tcp://example.com:80", fn, NULL) 尝试连接远程 80 端口。该函数也是异步的:调用后立即返回一个 mg_connection *;实际的网络连接过程在后台进行,成功建立后会向回调函数发送 MG_EV_CONNECT 事件 (Mongoose :: Documentation)。注意 url 参数可以使用协议前缀简化操作,后文 HTTP、MQTT 等模块也有各自的 connect 函数。

**数据发送:**在事件回调中,或在持有 mg_connection * 时,可使用 mg_send(c, data, size) 发送原始二进制数据 (Mongoose :: Documentation)。mg_send 将数据追加到连接的发送缓冲区,并返回成功写入缓冲的字节数 (Mongoose :: Documentation)(注意实际网络发送在稍后 poll 时完成)。对于发送文本,可使用格式化输出函数 mg_printf(c, const char *fmt, ...),其用法类似 printf,能够将格式化后的字符串直接加入发送缓冲 (Mongoose :: Documentation)。例如:

mg_send(c, "Hello", 5);  // 发送5字节的数据 "Hello" ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=Usage%20example%3A))

mg_printf(c, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s", 2, "OK");
// 上例构造并发送一个简单的HTTP响应头和主体 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=create%20responses%20manually%2C%20e,Length%60%20header))

Mongoose 所有发送函数(包括 mg_send, mg_printf 等)均是非阻塞的,仅将数据存入缓冲区。等到下一次事件循环时,框架会尝试将缓冲区数据写入套接字,并在完成后触发 MG_EV_WRITE 事件通知 (Mongoose :: Documentation)。

**关闭连接:**在回调中如果需要关闭某个连接,可以设置连接对象的标志位。mg_connection 结构有一个布尔标志 is_closing,应用可以将 c->is_closing 置为 1 来指示框架在合适的时候关闭该连接 (Mongoose :: Documentation)。一旦标记,Mongoose 将在下一个 mg_mgr_poll() 调用中关闭连接并触发 MG_EV_CLOSE 事件,然后释放相关资源 (Mongoose :: Documentation)。如果希望先发送完缓冲区再关闭,可将 c->is_draining 置1,表示发送完剩余数据后再断开。**注意:**不要直接调用系统的 close()free() 来关闭连接,而应使用上述标志方式,确保资源由 Mongoose 正确管理和释放 (Mongoose :: Documentation)。

**跨线程通知:**默认情况下,Mongoose 的所有 API 不是线程安全的,必须在创建它们的同一线程内调用(即调用 mg_mgr_poll() 的线程) (Mongoose :: Documentation)。对于需要从其他线程向事件循环发送通知的情况,可使用 mg_wakeup(struct mg_mgr *mgr, unsigned long id, const void *data, size_t size)mg_wakeup 可以从任意线程调用,它会唤醒另一个线程中的 mg_mgr_poll() 调用,并在目标事件循环中触发一个 MG_EV_WAKEUP 事件 (Mongoose :: Documentation)。参数 id 可以指定目标连接的 ID(c->id),从而由该连接收到事件,否则为0表示广播给事件管理器下所有连接。ev_data 将是携带的任意数据(长度为 size) (Mongoose :: Documentation)。这对于在多线程应用中通知网络线程执行某些任务非常有用。

定时器机制

Mongoose 提供内置软件定时器,可用来在事件循环中执行周期性或延迟任务。定时器与事件管理器关联,并在 mg_mgr_poll() 中检查触发。

**创建定时器:**使用 mg_timer_add(struct mg_mgr *mgr, uint64_t period_ms, unsigned flags, void (*fn)(void *), void *fn_data) 创建一个定时器 (Mongoose :: Documentation) (Mongoose :: Documentation)。参数含义:

  • mgr:事件管理器指针。
  • period_ms:定时间隔,单位毫秒。如果希望只执行一次,该值即延迟时间;重复执行则是周期。
  • flags:定时器标志位,可以是 0(默认,只执行一次)或 MG_TIMER_REPEAT(周期性重复)和/或 MG_TIMER_RUN_NOW(立即执行一次然后再按照周期) (Mongoose :: Documentation)。
  • fn:定时器回调函数,当定时到期时调用。
  • fn_data:传递给定时器函数的用户数据指针。

返回值是分配的 struct mg_timer * 指针,可用于取消定时器。**示例:**创建一个每秒触发的定时器,在回调中打印日志:

void my_timer_fn(void *arg) {
  printf("定时器触发!\n");
}
mg_timer_add(&mgr, 1000 /*ms*/, MG_TIMER_REPEAT, my_timer_fn, NULL);
// 每隔1秒将调用一次 my_timer_fn ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=Usage%20example%3A))

**销毁定时器:**当不再需要时,可调用 mg_timer_free(struct mg_timer **head, struct mg_timer *t) 取消并释放定时器 (Mongoose :: Documentation)。一般传入管理器的定时器链表头(&mgr.timers)和目标定时器指针 t。另外,调用 mg_mgr_free() 时会自动清理所有附属定时器,因此也可不手动释放。

TCP/UDP 模块 (底层套接字)

Mongoose 可以直接用于原生的 TCP/UDP 通信,提供较薄的抽象层。通过 URL scheme 来区分使用 TCP 或 UDP。例如:

  • mg_listen(&mgr, "tcp://IP:port", handler, NULL) 用于监听 TCP 端口 (Mongoose :: Documentation)。
  • mg_listen(&mgr, "udp://IP:port", handler, NULL) 用于监听 UDP 端口 (Mongoose :: Documentation)。
  • mg_connect(&mgr, "tcp://IP:port", handler, NULL) 创建 TCP 客户端连接 (Mongoose :: Documentation)。
  • mg_connect(&mgr, "udp://IP:port", handler, NULL) 创建 UDP 套接字并“连接”到指定远程地址(注意 UDP 无真正连接,此调用仅指定默认目标地址)。

**TCP 通信:**对 TCP 连接而言,数据通过 MG_EV_READMG_EV_WRITE 事件传递。服务器可通过监听得到 MG_EV_ACCEPT 后,每个客户端连接由独立的 mg_connection 表示。应用在 MG_EV_READ 时读取 c->recv.buf 中的数据并处理,可使用 mg_send/mg_printf将响应数据加入发送缓冲。除非发生错误,TCP 连接持续存在,双方可多次收发数据,直到一方关闭连接(对应另一方收到 MG_EV_CLOSE)。

**UDP 通信:**UDP 是无连接的,Mongoose 将每个 UDP 套接字(无论通过 mg_listen 还是 mg_connect 创建)也作为 mg_connection 表示。与TCP不同:

  • UDP 服务器(监听)会在 MG_EV_READ 事件中收到来自任意远端的报文。可以通过 c->rem 字段(远端地址)来识别数据来源,并可能需要在响应时指定目的地址。
  • 发送 UDP 数据:如果连接是由 mg_connect("udp://...") 创建的,则 mg_send 会将数据发往指定的远程主机。如果是监听获得的 mg_connection(即 c->is_listening=1 的UDP socket),建议不要直接用该连接发送数据。通常的做法是调用 mg_connect 创建一个新的 UDP 连接指向目标地址再发送,或者使用更底层的机制设置目标地址。相对而言,对于需要收发多播/广播或一对多 UDP 通信,可以考虑为每个对端维护单独的 mg_connection

示例:TCP Echo 服务器 – 下面的事件处理函数实现了一个简单的 TCP 回显服务器逻辑,将客户端发送的数据原样返回并在消息超过一定长度时关闭连接:

static void echo_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_READ) {
    // 将收到的数据原封不动写回发送缓冲区
    mg_send(c, c->recv.buf, c->recv.len);
    c->recv.len = 0;  // 清空已读取的数据
    // 可选:如果消息太长,关闭连接以防止滥用
    if (c->recv.len > 1024) {
      c->is_closing = 1;  // 超过长度,标记关闭 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=%2A%20%60c,or%20a%20clean%20TCP%20closure))
    }
  }
}

对于 UDP,处理函数类似,但可能要检查 c->rem 获取发送者信息,并在需要回复时创建新连接。例如实现“UDP 回显”,可以在 MG_EV_READ 中使用 mg_connect() 创建一个 UDP 客户端指向 c->rem 地址,然后用该连接 mg_send() 将数据发回。

HTTP 模块

HTTP 模块是 Mongoose 最常用的部分之一,包含构建 HTTP 服务器和客户端的功能。HTTP 服务器可以方便地提供嵌入式设备的 Web 控制界面或 REST API;HTTP 客户端可以用来在设备端发起 HTTP 请求(如与云服务交互)。

HTTP 服务端

**启动HTTP服务器:**使用 mg_http_listen(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data) 在指定地址启动 HTTP 服务 (Mongoose :: Documentation)。url 通常形如 "http://0.0.0.0:8000"(监听所有网卡的8000端口)或 "http://192.168.1.100:80"(监听特定IP和端口) (Mongoose :: Documentation)。成功返回 mg_connection *(监听连接)。与 mg_listen 不同,mg_http_listen 内部会自动设置HTTP协议的处理逻辑,因此传入回调 fn 会收到高级别的 HTTP 事件,而无需手动解析请求 (Mongoose :: Documentation)。例如:

mg_http_listen(&mgr, "http://0.0.0.0:80", http_fn, NULL);

如需同时支持 HTTPS,使用 "https://..." 前缀即可监听 TLS 端口(更多细节见下文 TLS 支持) (Mongoose :: Documentation)。

**HTTP 请求处理:**当客户端发来 HTTP 请求时,监听连接会产生一个新的连接 c 表示该会话,然后在用户回调中收到 MG_EV_HTTP_MSG 事件 (Mongoose :: Documentation)。ev_data 指向 struct mg_http_message,其中包含了解析后的请求信息(请求行、头部、正文等) (Mongoose :: Documentation)。开发者可以通过该结构访问请求的方法、URI、查询参数、HTTP版本等字段,以及请求体和头部列表:

  • hm->method:请求方法(如 GET、POST),类型为 struct mg_str
  • hm->uri:请求的 URI 路径部分(不含查询参数)。
  • hm->query:URI 中的查询字符串(若有,以?后面的部分)。
  • hm->body:HTTP 请求体数据。
  • hm->headers:HTTP 头部数组,可以通过遍历或使用辅助函数获取特定头。

例如,要判断请求 URI 是否为某接口,可以使用 mg_match(hm->uri, mg_str("/api/target"), NULL) 进行匹配 (Mongoose :: Documentation)。要读取某个头的值,可使用 mg_http_get_header(hm, "Content-Type"),返回指向相应头值的 mg_str (Mongoose :: Documentation)。若要提取查询参数或表单数据,可使用 mg_http_get_var(&hm->query, "name", buf, buf_len)mg_http_get_var(&hm->body, "field", buf, len),该函数将查找参数并复制值到提供的缓冲区。

**发送HTTP响应:**Mongoose 提供多种方式发送响应:

  • mg_http_reply(struct mg_connection *c, int status, const char *headers, const char *body_fmt, ...)
    一步构造并发送HTTP响应 (Mongoose :: Documentation)。其中 status 是状态码(如200),headers 是额外头部字符串(可为 "" 表示无额外头部),body_fmt 是可选的响应体格式化字符串(如 "%s")和后续参数。如果 body_fmt 为 NULL 则表示仅发送头部而无正文。例如:
    mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not found");
    
    会发送:状态行"HTTP/1.1 404 Not Found",然后 Content-Type 头,空行,和文本"Not found"作为响应体。
  • 手动发送:使用 mg_printf 等手动发送响应行和各部分。这适用于需要逐步发送或自定义传输(如分块编码)。例如手动发送一个简单响应:
    mg_printf(c,
      "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s",
      (int) hm->body.len, hm->body.ptr);
    
    但要注意正确计算 Content-Length 或使用 chunked 编码。
  • 分块发送:mg_http_printf_chunk(c, fmt, ...)mg_http_write_chunk(c, buf, len) 提供了发送 HTTP 分块编码(chunked transfer)的便利函数。当响应体较大或需要流式输出时,可以在发送响应头时指定 "Transfer-Encoding: chunked",然后多次调用 mg_http_printf_chunk 发送任意数量的块,最后以发送一个零长度的块表示结束 (Mongoose :: Documentation)。

**静态文件服务:**Mongoose 可以直接将本地文件或目录映射为 HTTP 服务,非常适合构建文件服务器或提供设备内置的网页。主要有两个API:

  • mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm, const struct mg_http_serve_opts *opts)
    将请求映射到指定目录下的文件。 (Mongoose :: Documentation)使用此函数时,应提供一个 mg_http_serve_opts 结构,其中至少设置 opts.root_dir 为要服务的本地目录路径 (Mongoose :: Documentation)。调用该函数后,Mongoose 会根据 hm->uri 请求路径在指定目录查找文件。如果找到文件则自动发送文件内容(包含正确的Content-Type等),如果路径是目录则返回目录索引或错误。示例:在回调中处理其他 URI 时使用:
    struct mg_http_serve_opts opts = {.root_dir = "/www"}; 
    mg_http_serve_dir(c, hm, &opts);
    
    可选的 opts 字段还包括 opts.extra_headers (附加响应头,比如CORS设置)、opts.mime_types(自定义扩展名到 MIME 类型映射)、opts.page404(指定404页面文件)等。
  • mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm, const char *path, struct mg_http_serve_opts *opts)
    直接发送指定路径的文件作为响应 (Mongoose :: Documentation)。如果文件不存在则返回404。它类似于 mg_http_serve_dir,但用于单个文件场景,常用于将某些请求固定映射到一个文件。例如将所有对根路径的请求返回同一个 HTML 文件(单页应用的入口)。

使用上述函数时,Mongoose 内部会处理范围请求(HTTP Range)、GZIP压缩(如果请求带有 Accept-Encoding: gzip 且目录下有对应 .gz 文件,则自动发送压缩文件) (Mongoose :: Documentation)、缓存控制等常见静态服务细节,让开发者无需重复造轮子。

**HTTP 服务器完整示例:**下面代码展示了一个完整的 HTTP 服务程序,它提供两个功能:对 /api/time 请求返回当前时间的 JSON(REST API),其他路径下则提供静态文件服务(假设当前目录有网页文件)。

#include "mongoose.h"

static const char *s_root_dir = ".";  // 静态文件所在目录

static void http_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_HTTP_MSG) {
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;
    if (mg_match(hm->uri, mg_str("/api/time"), NULL)) {
      // 如果是 /api/time 请求,返回JSON时间
      time_t now = time(NULL);
      mg_http_reply(c, 200, "Content-Type: application/json\r\n",
                    "{\"time\": %lu}\n", (unsigned long) now);
    } else {
      // 其他请求当作文件访问
      struct mg_http_serve_opts opts = {.root_dir = s_root_dir};
      mg_http_serve_dir(c, hm, &opts);
    }
  }
}

int main(void) {
  struct mg_mgr mgr;
  mg_mgr_init(&mgr);
  // 监听 HTTP (8080端口) 和 HTTPS (8443端口),共用同一个事件回调
  mg_http_listen(&mgr, "http://0.0.0.0:8080", http_fn, NULL);
  mg_http_listen(&mgr, "https://0.0.0.0:8443", http_fn, NULL);  // HTTPS 监听 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=match%20at%20L106%20mg_http_listen%28%26mgr%2C%20,%2F%2F%20Setup%20HTTPS%20listener))
  printf("HTTP服务器已启动:http://localhost:8080 ,HTTPS://localhost:8443\n");
  for (;;) {
    mg_mgr_poll(&mgr, 1000);
  }
  mg_mgr_free(&mgr);
  return 0;
}

在这个例子中,使用 mg_http_listen 启动了两个监听,一个 HTTP、一个 HTTPS。对于 HTTPS,我们需要提供证书才能正常工作,下文会介绍如何配置 TLS。在回调中,根据 URI 区分 API 请求和文件请求,分别使用 mg_http_replymg_http_serve_dir 处理。这样,一个简单的 Web 服务就搭建完成了。

HTTP 客户端

Mongoose 也能充当 HTTP 客户端,即主动向别的服务器发出请求。典型用例如:设备启动后向云端汇报状态(HTTP POST),或者定期拉取配置。

**建立HTTP客户端连接:**使用 mg_http_connect(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data) 创建一个 HTTP 客户端连接 (Mongoose :: Documentation)。url 可以包含 http://https:// 前缀以及路径,例如 "http://example.com/api"。调用后会返回一个连接指针(或 NULL 表示失败)。连接建立和 HTTP 协议握手(发送请求)不是自动完成的,还需要在回调中进一步动作:

  • 当底层TCP连接建立时,会触发 MG_EV_CONNECT 事件。这时应用可以通过 mg_printf 等发送HTTP请求行和头部。
  • **简化方式:**更方便的是,在调用 mg_http_connect() 的同时,利用其返回值 mg_connection *(或在 MG_EV_OPEN/MG_EV_CONNECT 事件中)调用 mg_printf 发送请求。例如:
    struct mg_connection *c = mg_http_connect(&mgr, "http://httpbin.org/get", client_fn, NULL);
    if (c) {
      mg_printf(c, "GET /get HTTP/1.1\r\nHost: httpbin.org\r\n\r\n");
    }
    
    上述代码将在连接建立后立即发送一个 GET 请求。
  • 当服务器响应到达且完整解析后,会触发 MG_EV_HTTP_MSG 事件,此时 ev_data 中的 struct mg_http_message 包含HTTP响应信息(status码、响应头、响应体等) (Mongoose :: Documentation)。应用可以检视 hm->body 或使用 mg_http_status(hm) 获取状态码 (Mongoose :: Documentation)。
  • 读取完需要的数据后,可以关闭连接或保持连接复用(根据 HTTP Keep-Alive 情况,Mongoose 默认支持HTTP/1.1 Keep-Alive)。

**HTTP 客户端示例:**以下代码发起一个 HTTP POST 请求,将 JSON 数据发送到服务器并打印返回结果:

static void client_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_CONNECT) {
    // 连接成功,发送HTTP POST请求
    mg_printf(c,
      "POST /api/data HTTP/1.1\r\n"
      "Host: example.com\r\n"
      "Content-Type: application/json\r\n"
      "Content-Length: %d\r\n\r\n"
      "%s",
      13, "{\"key\":\"val\"}");
  } else if (ev == MG_EV_HTTP_MSG) {
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;
    int code = mg_http_status(hm);  // 获取响应状态码 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=mg_http_status))
    printf("服务器返回状态: %d, 响应体长度: %d\n", code, (int) hm->body.len);
    // 打印响应体(假设是文本)
    printf("响应内容:\n%.*s\n", (int) hm->body.len, hm->body.ptr);
    c->is_closing = 1;  // 完成后关闭连接
  }
}

// 在某处调用 mg_http_connect 发起请求
mg_http_connect(&mgr, "http://example.com/api/data", client_fn, NULL);

需要注意,如果使用 https:// URL,则必须在连接建立后设置 TLS(参考下文 TLS 部分),否则无法正常完成 SSL/TLS 握手。

HTTP 认证和其它工具

Mongoose 提供了一些辅助函数便于实现 HTTP 常见功能:

  • Basic Auth 认证:mg_http_creds() 可从HTTP头中解析出 Basic Authentication 的用户名密码 (Mongoose :: Documentation);mg_http_bauth(struct mg_connection *c, const char *user, const char *pass) 则可以向HTTP请求添加 Basic Auth 的认证头 (Mongoose :: Documentation)。例如在客户端请求前调用 mg_http_bauth(c, "admin", "123") 会在发送缓冲区添加 Authorization: Basic ... 头 (Mongoose :: Documentation)。服务端可用 mg_http_creds(hm, user_buf, userlen, pass_buf, passlen) 提取出提供的用户名密码 (Mongoose :: Documentation)。
  • URL 编码/解码:mg_url_decode() 可对 URL 中的%xx进行解码。mg_http_get_var() 则可直接获取解码后的变量值 (Mongoose :: Documentation)。
  • 文件上传:mg_http_upload() 辅助实现 HTTP 文件上传(基于 multipart/form-data)。它能够将接收到的大文件分块写入磁盘,避免占用大量内存。典型用法是在接收到上传请求时反复调用 mg_http_upload(c, hm, &mg_fs_posix, "/upload_dir", MAX_SIZE)。该函数会将文件数据写入 /upload_dir 下相应文件,并自动处理分块 (Mongoose :: Documentation)。
  • **Web 表单解析:**对于 multipart/form-data 类型,mg_http_next_multipart() 可逐个解析表单部件 (Mongoose :: Documentation)。调用时传入请求体和偏移,不断迭代获取每个 mg_http_part(包含 part 的 name、filename、body 等) (Mongoose :: Documentation)。

通过这些工具函数,可以方便地扩展 HTTP 服务端的能力,如增加身份验证、处理文件上传下载等。

WebSocket 模块

WebSocket 是在单个 TCP 连接上进行全双工通讯的协议,常用于网页与设备间的实时数据交换。Mongoose 对 WebSocket 提供了简单的支持,包括服务器端的升级握手和客户端的连接。

WebSocket 服务器

WebSocket 握手是基于 HTTP 的升级过程。Mongoose 的设计是:先使用 mg_http_listen() 提供 HTTP 服务,然后在收到特定 HTTP 握手请求时,调用 mg_ws_upgrade() 切换协议,使该连接进入 WebSocket 模式 (Mongoose :: Documentation) (Mongoose :: Documentation)。流程如下:

  1. HTTP 握手请求:客户端发送带有 Upgrade: websocket 头的 HTTP GET 请求。Mongoose 解析后在回调中产生 MG_EV_HTTP_MSG 事件。应用需要检测这是否为 WebSocket 请求。通常可以检查 hm->headers 中的 "Upgrade""Sec-WebSocket-Key" 等头是否存在。
  2. 执行协议升级:调用 mg_ws_upgrade(c, hm, extra_headers_fmt, ...) 完成握手响应 (Mongoose :: Documentation)。c 是当前连接,hm 是收到的HTTP请求。可选的 extra_headers_fmt 可传入额外的响应头(一般传 NULL)。成功调用后,Mongoose 会发送符合 WebSocket 协议的101状态响应并完成握手 (Mongoose :: Documentation)。此时该连接的协议处理器切换为 WebSocket 模式,随后应用回调将改为接收 WebSocket 事件而非 HTTP 事件。
  3. 握手完成事件:一旦升级完成,用户回调会收到 MG_EV_WS_OPEN 事件(表示 WebSocket 握手成功,可以开始通信) (Mongoose :: Documentation)。ev_datastruct mg_http_message *,即初始的握手HTTP请求,可以从中获取 WebSocket 子协议等信息。
  4. 数据通信:接下来,服务器与客户端可相互发送消息。收到消息时触发 MG_EV_WS_MSG 事件,ev_datastruct mg_ws_message * (Mongoose :: Documentation)。mg_ws_message 结构包含:
    • data:消息内容(一个 mg_str,可包括文本或二进制数据) (Mongoose :: Documentation)。
    • flags:标志位,最低4位表示 WebSocket opcode(消息类型) (Mongoose :: Documentation)。常用的操作码有:WEBSOCKET_OP_TEXT(1) 文本消息、WEBSOCKET_OP_BINARY(2) 二进制消息、WEBSOCKET_OP_CLOSE(8) 关闭帧、WEBSOCKET_OP_PING(9) Ping、WEBSOCKET_OP_PONG(10) Pong (Mongoose :: Documentation)。 应用可以检查 wm->flags & 0x0F 来判断消息类型 (Mongoose :: Documentation) (Mongoose :: Documentation)。通常,对于文本或二进制消息,取出 wm->data 处理内容;对于 Ping 可以回复 Pong(Mongoose 可能已自动处理Ping/Pong,但应用可选择处理)。
  5. **发送消息:**使用 mg_ws_send(struct mg_connection *c, const void *buf, size_t len, int op) 发送 WebSocket 消息 (Mongoose :: Documentation)。其中 op 是操作码,例如 WEBSOCKET_OP_TEXTWEBSOCKET_OP_BINARY 等。该函数会按照 WebSocket 格式封装数据帧并加入发送缓冲,返回发送的字节数 (Mongoose :: Documentation)。也可以使用 mg_ws_printf(c, op, fmt, ...) 直接格式化发送文本 (Mongoose :: Documentation)。发送后,对端会收到相应的 MG_EV_WS_MSG 事件。
  6. **关闭连接:**当需要关闭 WebSocket 时,可以调用 mg_ws_send(c, "", 0, WEBSOCKET_OP_CLOSE) 发送关闭帧,或者直接设置 c->is_closing 关闭TCP连接(对端会相应收到关闭事件)。

**WebSocket 服务器示例:**下面示例程序基于 HTTP 服务器升级 WebSocket,在任何 WebSocket 连接上实现echo功能(收到消息后原样发送回去)。

#include "mongoose.h"

// WebSocket和HTTP共用的事件处理函数
static void ws_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_HTTP_MSG) {
    // 收到HTTP请求,检查是否为WebSocket握手
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;
    if (mg_http_match_uri(hm, "/ws")) {
      // 如果URI匹配 /ws,则执行WebSocket协议升级
      mg_ws_upgrade(c, hm, NULL);  // 升级为WebSocket ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=%2F%2F%20Mongoose%20events%20handler%20void,))
    } else {
      // 非 /ws 请求,返回404
      mg_http_reply(c, 404, "", "Not found\n");
    }
  } else if (ev == MG_EV_WS_OPEN) {
    // WebSocket 握手成功
    printf("客户端 %p WebSocket 已连接\n", c);
    // 可选:发送一条欢迎消息
    mg_ws_send(c, "welcome", 7, WEBSOCKET_OP_TEXT);
  } else if (ev == MG_EV_WS_MSG) {
    // 收到 WebSocket 数据帧
    struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
    // 将消息内容再发回给客户端(原样echo)
    mg_ws_send(c, wm->data.ptr, wm->data.len, WEBSOCKET_OP_TEXT);
  } else if (ev == MG_EV_CLOSE) {
    if (c->is_websocket) {  // 仅在WebSocket连接关闭时打印
      printf("客户端 %p WebSocket 连接关闭\n", c);
    }
  }
}

int main(void) {
  struct mg_mgr mgr;
  mg_mgr_init(&mgr);
  // 监听 HTTP 端口并采用 ws_fn 处理,其中包含 WebSocket 升级逻辑
  mg_http_listen(&mgr, "http://0.0.0.0:8000", ws_fn, NULL);
  printf("WebSocket服务器已在 ws://localhost:8000/ws 等待连接...\n");
  for (;;) mg_mgr_poll(&mgr, 1000);
  mg_mgr_free(&mgr);
}

在此程序中,HTTP 请求被限制为只有路径为 /ws 的才升级为 WebSocket,其它路径返回 404。当然实际应用中可以根据需要调整路径和处理逻辑。一旦 WebSocket 连接建立,就可以在 MG_EV_WS_MSG 中处理消息。在上例中,实现了最简单的回显功能。

WebSocket 客户端

WebSocket 客户端的使用类似 HTTP 客户端。Mongoose 提供 mg_ws_connect(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data, const char *fmt, ...) 函数来发起 WebSocket 连接 (Mongoose :: Documentation)。参数 url 形如 "ws://echo.server.com/chat""wss://..."(HTTPS的WebSocket)等。可选的 fmt 参数可用于指定额外的握手头部,比如指定 Sec-WebSocket-Protocol 子协议等 (Mongoose :: Documentation)。

mg_ws_connect() 调用后会立即返回一个 mg_connection * 或 NULL(错误)。连接建立过程发生在后台:

  • TCP 连接建立成功时会触发 MG_EV_CONNECT
  • 紧接着 Mongoose 会发送 WebSocket 握手请求(HTTP GET 带 Upgrade: websocket)。无需应用手动发送,mg_ws_connect 已经完成了此步骤。
  • 收到服务器的握手应答(101 切换协议)后,触发 MG_EV_WS_OPEN,表示 WebSocket 通道建立 (Mongoose :: Documentation)。
  • 之后通信过程与服务器端相同,会收到 MG_EV_WS_MSG 等事件。

使用 WebSocket 客户端需要注意:如果 url 使用了 wss://(TLS),则需要在 MG_EV_CONNECT 事件中调用 mg_tls_init() 设置 TLS(与HTTPS客户端类似)。

**WebSocket 客户端示例:**以下代码连接到一个公共 echo WebSocket 服务,发送一条消息并打印回显:

static const char *s_ws_url = "ws://echo.websocket.events";  // 公共测试WebSocket

static void ws_client_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_WS_OPEN) {
    printf("WebSocket 连接建立成功!发送测试消息...\n");
    mg_ws_send(c, "hello", 5, WEBSOCKET_OP_TEXT);
  } else if (ev == MG_EV_WS_MSG) {
    struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
    printf("收到服务回显: %.*s\n", (int) wm->data.len, wm->data.ptr);
    c->is_closing = 1;  // 收到一次回显后关闭
  } else if (ev == MG_EV_CLOSE) {
    printf("WebSocket 客户端连接已关闭\n");
  }
}

// 发起WebSocket连接
mg_ws_connect(&mgr, s_ws_url, ws_client_fn, NULL, NULL);

MG_EV_WS_OPEN 中,我们知道握手成功,可以安全地调用 mg_ws_send 发送文本。这段代码将在收到服务端回显后关闭连接。

MQTT 模块

MQTT 是轻量级发布/订阅协议,广泛用于物联网。Mongoose 对 MQTT 提供了内置支持,包括 MQTT 客户端和服务端(Broker)。主要接口封装在 mg_mqtt_* 系列函数中。

MQTT 客户端

**建立MQTT连接:**使用 mg_mqtt_connect(struct mg_mgr *mgr, const char *url, const struct mg_mqtt_opts *opts, mg_event_handler_t fn, void *fn_data) 来连接到 MQTT 服务器 (Mongoose :: Documentation)。url 可以是 "mqtt://broker-address:port",若使用 TLS 则 "mqtts://..."opts 是一个包含MQTT连接参数的结构,常用字段有:

  • opts.client_id:客户端标识(若留空,服务器可能分配随机ID)。
  • opts.user / opts.pass:用户名和密码(用于鉴权,选填) (Mongoose :: Documentation)。
  • opts.clean:布尔值,是否要求清除会话(类似 MQTT clean session 标志) (Mongoose :: Documentation)。
  • opts.keepalive:保持存活时间(秒),不发送消息时的心跳间隔 (Mongoose :: Documentation)。
  • opts.will_topic / opts.will_message:遗嘱消息的主题和内容(可选,在客户端异常断开时由服务器发布)。

mg_mqtt_connect() 调用后即返回连接指针或 NULL(错误)。和 HTTP 类似,它只是初始化连接,真正的操作在事件中:

  • 当底层TCP建立后,产生 MG_EV_CONNECT 事件 (Mongoose :: Documentation)。Mongoose 随即会发送 MQTT Connect 报文(包含 opts 中的参数),不需要用户手动构造。事实上,mg_mqtt_connect 内部已经根据提供的 opts 构建并发送了 CONNECT 消息 (Mongoose :: Documentation)。
  • 当收到服务器的 CONNACK 响应后,产生 MG_EV_MQTT_OPEN 事件,表示MQTT握手成功建立 (Mongoose :: Documentation)。ev_data 指向一个 int,表示 CONNACK 返回码(0为连接成功,非0表示失败原因) (Mongoose :: Documentation)。
  • 此后便可订阅/发布主题。

**订阅主题:**使用 mg_mqtt_sub(struct mg_connection *c, const struct mg_mqtt_opts *opts) 订阅主题 (Mongoose :: Documentation)。需要在 opts 中设置 .topic 字段为要订阅的主题名,以及 .qos 为QoS等级(0、1或2) (Mongoose :: Documentation)。调用后Mongoose会发送 SUBSCRIBE 报文。可通过监听 MG_EV_MQTT_CMD 事件来确认订阅是否成功(它在收到 SUBACK 时触发)。

**发布消息:**使用 mg_mqtt_pub(struct mg_connection *c, const struct mg_mqtt_opts *opts) 发布消息 (Mongoose :: Documentation)。opts 中需设置 .topic 为目标主题,.message 为消息内容,.qos.retain(是否保留消息)根据需要设置 (Mongoose :: Documentation)。函数返回一个 uint16_t 报文标识(MQTT PUBLISH报文的ID) (Mongoose :: Documentation)。注意QoS 0不需要ACK,QoS1需要等待PUBACK事件(对应 MG_EV_MQTT_CMD),QoS2更复杂(PUBREC/PUBREL/PUBCOMP)。

**接收消息:**当订阅的主题有消息发布时,客户端会收到 MG_EV_MQTT_MSG 事件 (Mongoose :: Documentation)。ev_datastruct mg_mqtt_message *,包含此次 PUBLISH 的详细信息 (Mongoose :: Documentation)。常用字段:

  • mm->topic:消息主题(mg_str)。
  • mm->data:消息内容(mg_str)。
  • mm->qos:QoS等级。
  • mm->retain:是否为保留消息(在 mg_mqtt_message 里可能通过 flags 或其他方式表示)。 应用应根据需要处理该消息,例如将其内容解析后采取动作。

**断开连接:**可以调用 mg_mqtt_disconnect(c, opts) 来发送 DISCONNECT 报文并关闭连接 (Mongoose :: Documentation)。实际上直接关闭TCP连接也会由服务器检测断开,但发送DISCONNECT可以更优雅地告知服务器。

**MQTT 客户端示例:**下面示例连接公共测试服务器,订阅主题并收到后再发布回应:

static const char *s_mqtt_url = "mqtt://broker.hivemq.com:1883";
static struct mg_connection *s_mqtt_conn = NULL;  // 保存MQTT连接全局指针

static void mqtt_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_MQTT_OPEN) {
    // MQTT 连接成功(CONNACK 收到,ev_data 是 connack 返回码)
    int conn_status = *(int *) ev_data;
    if (conn_status == 0) {
      printf("MQTT成功连接到服务器!\n");
      // 订阅主题 "test/topic"
      struct mg_mqtt_opts sub_opts = {.topic = mg_str("test/topic"), .qos = 1};
      mg_mqtt_sub(c, &sub_opts);
      printf("已订阅主题: %s\n", "test/topic");
    } else {
      printf("MQTT连接失败,返回码 %d\n", conn_status);
    }
  } else if (ev == MG_EV_MQTT_MSG) {
    // 收到订阅的消息
    struct mg_mqtt_message *mm = (struct mg_mqtt_message *) ev_data;
    printf("收到主题 [%.*s] 消息: %.*s\n",
           (int) mm->topic.len, mm->topic.ptr,
           (int) mm->data.len, mm->data.ptr);
    // 构造回应消息,将内容加上前缀后发布到另一个主题
    char msg[100];
    snprintf(msg, sizeof(msg), "Echo: %.*s", (int) mm->data.len, mm->data.ptr);
    struct mg_mqtt_opts pub_opts = {
      .topic = mg_str("test/echo"),
      .message = mg_str(msg),
      .qos = 1
    };
    mg_mqtt_pub(c, &pub_opts);
    printf("已发布响应到主题: test/echo\n");
  } else if (ev == MG_EV_CLOSE) {
    printf("MQTT连接已关闭\n");
    s_mqtt_conn = NULL;
  }
}

int main(void) {
  struct mg_mgr mgr;
  mg_mgr_init(&mgr);
  // 使用 clean session 连接
  struct mg_mqtt_opts opts = {.clean = true, .client_id = mg_str("mg_demo")};
  s_mqtt_conn = mg_mqtt_connect(&mgr, s_mqtt_url, &opts, mqtt_fn, NULL);
  if (s_mqtt_conn == NULL) {
    printf("无法创建 MQTT 连接\n");
    return 1;
  }
  printf("连接到 MQTT broker 中...\n");
  for (;;) {
    mg_mgr_poll(&mgr, 1000);
  }
  mg_mgr_free(&mgr);
}

此程序连接到 test broker,订阅 "test/topic" 并将收到的消息加上前缀后发布到 "test/echo" 主题。通过该例,可以看出 MG_EV_MQTT_OPENMG_EV_MQTT_MSG 事件的处理。

MQTT 服务器(Broker)

Mongoose 也允许充当 MQTT Broker(服务器)。通过 mg_mqtt_listen(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data) 可以在指定地址监听 MQTT 协议 (Mongoose :: Documentation)。之后在事件回调中处理 MQTT 事件:

  • 新客户端连入时,会收到 MG_EV_ACCEPTMG_EV_MQTT_CMD 事件,其中 Connect 报文可在 MG_EV_MQTT_CMD 检测(mm->cmd == MQTT_CMD_CONNECT)。
  • 可以调用 mg_mqtt_login(c, opts) 来发送 CONNACK (Mongoose :: Documentation)。
  • 客户端发布消息会触发 MG_EV_MQTT_MSG,服务器可视需要将消息转发给订阅了该主题的其他客户端。
  • 订阅请求将以 MG_EV_MQTT_CMD 事件出现(MQTT_CMD_SUBSCRIBE),可通过 mg_mqtt_sub() 确认订阅(发送 SUBACK)。
  • 基于以上机制,开发完整的 Broker 逻辑需要维护订阅表,将 PUBLISH 分发给匹配的连接等。

实现一个完整 MQTT Broker 较为复杂,需要处理协议细节。由于问题重点在参考手册,详细实现不展开。开发者可利用 Mongoose 提供的底层 MQTT 消息解析能力(通过 mg_mqtt_message 和各事件)实现自己的 Broker。

TLS/SSL 支持

Mongoose 在 7.x 版本中内置了一个轻量的 TLS 1.3 实现,也支持集成第三方 TLS 库(如 mbedTLS、OpenSSL)。这意味着既可以在没有外部依赖的情况下启用HTTPS/MQTTS等安全协议,也可以使用系统已有的TLS库。

启用 TLS

默认情况下,Mongoose 未启用 TLS。可通过编译选项或配置宏启用:

  • **使用内置TLS实现:**编译时定义 -DMG_TLS=MG_TLS_BUILTIN 或在 mongoose_config.h 中定义 #define MG_TLS MG_TLS_BUILTIN 来启用自带的 TLS 1.3 支持 (Mongoose :: Documentation)。内置实现基于 ECC,非常适合嵌入式设备使用。
  • **使用OpenSSL等外部库:**将 MG_TLS 设置为 MG_TLS_OPENSSLMG_TLS_MBED 等,并确保链接对应库。比如 -DMG_TLS=MG_TLS_OPENSSL 并链接 -lssl -lcrypto
  • 如果不需要TLS,可保持默认 MG_TLS_NONE,则 https://mqtts:// URL 将无法使用。

加载证书和密钥

无论使用哪种 TLS 实现,服务器模式通常需要提供证书和私钥,客户端模式通常需要提供 CA 证书以验证服务器。

Mongoose 将证书/密钥通过一个 struct mg_tls_opts 结构传递 (Mongoose :: Documentation):

struct mg_tls_opts {
  struct mg_str ca;    // CA证书(PEM或DER格式),用于验证对端证书 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=struct%20mg_str%20ca%3B%20%20,PEM%20or%20DER))
  struct mg_str cert;  // 自身证书(PEM或DER)
  struct mg_str key;   // 自身私钥(PEM或DER)
  struct mg_str name;  // 服务器名字,用于验证主机名(可选)
};

服务器通常设置 certkey 字段(以及可能的 ca 用于双向认证) (Mongoose :: Documentation);客户端通常设置 ca(CA 根证书,用于验证服务器),以及 name(服务器域名,用于主机名验证) (Mongoose :: Documentation)。

**注意:**可以将 PEM 文件的内容读入内存并用 mg_str() 封装,例如:

const char *pem_cert = "-----BEGIN CERTIFICATE-----\n..."; 
struct mg_tls_opts opts = {
  .ca = mg_str(ca_pem_data),
  .cert = mg_str(cert_pem_data),
  .key = mg_str(key_pem_data)
};

Mongoose 不会自行读取文件,因此需要应用层提供证书数据。

初始化 TLS 会话

对于使用 TLS 的连接(无论客户端或服务器),需要在连接建立时调用 mg_tls_init(struct mg_connection *c, const struct mg_tls_opts *opts) (Mongoose :: Documentation)。可以在以下时机调用:

  • **服务器:**在收到新连接的 MG_EV_ACCEPT 事件时,调用 mg_tls_init(c, &opts) 将该连接切换为 TLS 模式 (Mongoose :: Documentation)。这会触发 TLS 握手过程,并在成功后产生 MG_EV_TLS_HS 事件(TLS handshake succeeded)。
  • **客户端:**在检测到 MG_EV_CONNECT 事件时调用 (Mongoose :: Documentation)。例如对于 https://mqtts:// 连接,在 MG_EV_CONNECT 时执行 mg_tls_init(c, &opts) 开始握手。
  • 也可以在调用 mg_http_listen()mg_mqtt_connect() 返回后立即对监听连接或主动连接调用 mg_tls_init。但通常推荐在事件回调里处理,以确保底层连接已就绪。

**示例:**HTTP+TLS 服务器为监听8443端口的连接配置证书:

// 全局加载证书和私钥(例如从文件或定义的字符串)
struct mg_tls_opts tls_opts;
tls_opts.cert = mg_str(server_cert_pem);
tls_opts.key = mg_str(server_key_pem);

static void fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_ACCEPT) {
    // 为每个新接受的连接初始化 TLS
    mg_tls_init(c, &tls_opts);   // 执行 TLS 握手 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=%2F%2F%20server%20event%20handler%3A%20if,mg_tls_init%28c%2C%20%26opts))
  } else if (ev == MG_EV_HTTP_MSG) {
    // ... HTTP请求处理 ...
  }
}

// 创建HTTPS监听
mg_connection *lc = mg_http_listen(&mgr, "https://0.0.0.0:8443", fn, NULL);
if (lc == NULL) {
  printf("TLS监听端口失败\n");
}

客户端例如访问 HTTPS:

struct mg_tls_opts client_tls = {.ca = mg_str(my_ca_pem)};
static void client_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_CONNECT) {
    mg_tls_init(c, &client_tls);  // 为客户端连接设置CA证书验证 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=Usage%20example%3A))
  } else if (ev == MG_EV_HTTP_MSG) {
    // ... 处理HTTP响应 ...
  }
}
mg_http_connect(&mgr, "https://example.com/path", client_fn, NULL);

以上在 MG_EV_CONNECT 阶段调用了 mg_tls_init,这样后续通信都在TLS之上进行了。

TLS 选项与注意事项

  • **双向认证:**如果需要客户端证书认证,在服务器端设置 opts.ca(用于验证客户端证书的CA),并在客户端设置自己的 opts.certopts.key(客户端证书和密钥)。当两端都提供证书时,即启用了双向 TLS(Mutual TLS) (Mongoose :: Documentation)。
  • RNG:Mongoose 内置 TLS 使用 mg_random() 作为随机数生成器。如需自定义随机源,可定义 MG_ENABLE_CUSTOM_RANDOM=1 并提供自定义的 mg_random() 实现 (Mongoose :: Documentation)。
  • 证书验证:客户端默认会验证服务器证书是否由给定 CA 签发以及是否匹配主机名(通过 opts.name 指定或通过所连主机自动匹配)。如果未设置 CA(opts.ca 为空),则不会验证证书 (Mongoose :: Documentation)(不安全)。开发中可临时不设CA跳过验证,但正式产品请务必配置正确的 CA 证书或证书链,以防中间人攻击。
  • SNI:如客户端连接需要 Server Name Indication,可在 opts.name 中提供期望的主机名;服务器端若使用通配证书,可通过此字段获知客户端请求的域名(不过 Mongoose 未暴露回调获取 SNI名称的接口,仅用于验证)。

总之,在嵌入式环境中,Mongoose TLS 提供了免外部依赖的安全通信能力,但也允许灵活切换到成熟的TLS库,在调试和安全需求更高时使用。

嵌入式系统与 RTOS 适配

Mongoose 旨在在各类嵌入式平台上轻松运行,其架构对操作系统和网络栈具有良好适应性 (GitHub - cesanta/mongoose: Embedded web server, with TCP/IP network stack, MQTT and Websocket)。下面概述在常见嵌入式环境下的使用注意事项:

  • **裸机 (Bare Metal):**在无操作系统的微控制器上,可使用 Mongoose 提供的内置 TCP/IP 协议栈和驱动。需在编译时启用 MG_ENABLE_TCPIP=1 (Mongoose :: Documentation)并选择对应的 MAC/PHY 驱动(例如 MG_ENABLE_DRIVER_STM32 等)。Mongoose 内置支持了一些常见芯片的以太网外设和独立网络芯片(如 Wiznet W5500) (GitHub - cesanta/mongoose: Embedded web server, with TCP/IP network stack, MQTT and Websocket)。这样可以不依赖 lwIP 等外部网络库,实现轻量网络功能。例如在 STM32 上,使用内置栈可以用极少的文件实现一个 Web 服务 (GitHub - cesanta/mongoose: Embedded web server, with TCP/IP network stack, MQTT and Websocket)。当然,使用内置栈意味着 Mongoose 接管网络中断等,需要适配具体平台的底层接口(Mongoose已实现部分芯片的驱动)。
  • **RTOS + 自带协议栈:**许多 RTOS(如 FreeRTOS 结合 lwIP,Zephyr 自带网络栈等)提供了套接字或类BSD Socket接口。Mongoose 可以直接在这些环境上运行,将 MG_ARCH 设置为相应值(例如 FreeRTOS+lwIP 可定义 MG_ARCH=MG_ARCH_FREERTOS,Zephyr 可定义 MG_ARCH=MG_ARCH_ZEPHYR 等,在 arch.h 中有枚举 (raw.githubusercontent.com))。如果 RTOS 已有线程调度,可以选择在一个单独的任务中运行 mg_mgr_poll() 循环,使其不阻塞其它任务。注意 Mongoose 的 API 要在同一个任务中调用,其他任务若需要操作连接,可用 mg_wakeup 通知。
  • **ESP32 (FreeRTOS + lwIP):**ESP32 的 IDF 环境自带 FreeRTOS 和 lwIP。Mongoose 可被编译为 ESP-IDF 的组件,或直接集成源码。一般将 MG_ARCH=MG_ARCH_ESP32(在 mongoose.h 中已有针对ESP32的适配宏定义 (raw.githubusercontent.com) (raw.githubusercontent.com))。ESP32 默认使用 FreeRTOS 底层,也可以开启 Mongoose 内置栈通过 SPI驱动兼容 Wi-Fi 模块,但通常没必要。使用时需注意 ESP32 的日志机制:可以重定向 Mongoose 的日志输出到 IDF 日志(通过实现 mg_log_set_fn() 自定义打印,或利用 ESP32 自动把 printf 输出转向UART的特性 (Mongoose :: Documentation))。
  • **STM32 + FreeRTOS + lwIP:**在这种常见组合下,有两种选择:1)使用 lwIP 套接字接口:将 MG_ARCH=MG_ARCH_UNIXMG_ARCH_FREERTOS 均可,只要 lwIP 的 BSD socket API 可用。这样 Mongoose 所有网络操作走 lwIP 的接口,不需要特殊改动。需要确保在一个线程调用 mg_mgr_poll() 并在有数据时调用,这通常通过 FreeRTOS线程或主循环配合 sys_check_timeouts() 来处理 lwIP 定时器。2)使用内置栈 + 驱动:关闭 lwIP,启用 MG_ENABLE_TCPIP 以及相应驱动宏(如 MG_ENABLE_DRIVER_STM32)。然后提供网卡底层初始化(MAC地址配置、PHY引脚等)。Mongoose 将直接控制以太网外设进行收发。对于资源受限或快速启动项目,这方案很有吸引力 (GitHub - cesanta/mongoose: Embedded web server, with TCP/IP network stack, MQTT and Websocket)。官方提供了针对 STM32 的 CubeIDE 和 Keil 示例,可以参考。
  • **RTOS 定时器与集成:**如果希望将 Mongoose 集成进现有事件循环或定时器系统,也可以每隔一定时间调用一次 mg_mgr_poll(mgr, 0) 非阻塞地处理事件。比如在系统主循环或RTOS心跳里调用。只要调用足够频繁(小于网络响应要求的间隔),也能正常运行。在硬实时系统里,这种用法可以避免一个专门线程。
  • **内存与性能:**嵌入式系统通常RAM紧张。可以根据需要调整 MG_IO_SIZE(默认8192)来改变收发缓冲区大小 (Mongoose :: Documentation);禁用不需要的协议(通过宏,如不需要 MQTT 则可以禁用MG_ENABLE_MQTT减小体积等)。Mongoose 本身非常小巧,在不开启高级协议时约50KB左右代码大小 (GitHub - cesanta/mongoose: Embedded web server, with TCP/IP network stack, MQTT and Websocket)。另外,可通过 MG_ENABLE_HEXDUMP 控制调试功能,防止因调试日志占用大量空间。
  • **多任务并发:**如果必须在多个任务分别处理网络,可为每个任务创建独立的 mg_mgr 实例各自调用 mg_mgr_poll。但要确保不同任务的 Mongoose 实例使用不同套接字或端口,且任务间完全隔离。一般推荐集中在单任务处理网络,这样维护一个连接表更简单,也避免多个网络栈实例。

**官方嵌入式示例:**Mongoose 官方网站提供了大量针对嵌入式平台的示例和向导。例如针对 ESP32、STM32(CubeIDE, Keil)、Zephyr RTOS 等都有现成项目,可以参考快速上手 (Mongoose :: Documentation) (Mongoose :: Documentation)。这些示例展示了如何配置 Mongoose 在特定平台运行,包括网卡初始化、与系统启动的结合等,对于开发者是很好的参考。

常见问题与解答

Q1: 为什么我的事件回调没有收到数据/事件?
A1: 最常见的原因是遗漏调用 mg_mgr_poll() 或调用频率不足。确保在主循环中不断调用 mg_mgr_poll(),否则网络事件无法触发 (Mongoose :: Documentation)。另一个可能原因是监听或连接未成功创建(mg_listen/mg_connect 返回了 NULL)。此时可以检查返回值并使用 MG_EV_ERROR 事件调试错误信息 (Mongoose :: Documentation)。

Q2: 编译时提示 “MG_ARCH 未定义,无法猜测” 错误怎么办?
A2: 这是因为 Mongoose 无法自动确定目标架构。解决办法是在编译选项加入适当定义,例如Linux环境定义 -DMG_ARCH=MG_ARCH_UNIX,FreeRTOS 上定义 -DMG_ARCH=MG_ARCH_FREERTOS 等 (raw.githubusercontent.com) (raw.githubusercontent.com)。或者建立 mongoose_config.h 手动定义 #define MG_ARCH MG_ARCH_CUSTOM 并根据平台包含所需头文件。大多数情况下,Mongoose 会根据宏自动选择,如 _WIN32 对应 WIN32,__unix__ 对应 UNIX (raw.githubusercontent.com)。但某些嵌入式编译器可能没有这些宏,需要手动指定。

Q3: 我的 HTTP 服务无法处理超过一定大小的 POST 数据,该如何解决?
A3: 默认情况下,Mongoose 的接收缓冲区大小为 MG_IO_SIZE(默认8KB)。如果POST数据超过此大小且没有分块传输,可能需要增大缓冲区以一次性容纳完整请求,或者使用 mg_http_upload() 将数据分块写入文件。你可以通过在编译定义例如 -DMG_IO_SIZE=16384 来增大缓冲区到16KB。但务必考虑嵌入式内存限制。如果数据非常大(如上传固件几MB),最佳实践是使用 HTTP 分块传输并在应用层逐块处理,这样不需要大的内存缓存。

Q4: 为什么我的嵌入式TLS服务器浏览器访问时不被信任?
A4: 很可能是因为使用了自签名证书。浏览器会警告不受信任,这是正常的。为了解决,需使用受信任CA颁发的证书,或者在浏览器中安装自签名证书的CA。另一个可能原因是未正确配置证书链,浏览器无法验证完整链。如果是测试环境可以临时跳过,但生产环境建议获取有效证书。

Q5: 如何优雅地关闭服务器并断开所有客户端?
A5: 可以调用 mg_mgr_free(&mgr) 来关闭管理器 (Mongoose :: Documentation)。该函数内部会关闭所有连接并释放资源。如果希望逐步通知客户端,可以先发送关闭通知(例如WebSocket发关闭帧,或HTTP长连接发一个指定头等),然后稍微延时再调用 mg_mgr_free。由于 Mongoose 是非阻塞的,没有类似 mg_close_immediately 之外的接口,标记关闭也是在下一次 mg_mgr_poll 时执行,因此常用做法是在需要停止时:先停止接受新连接(可以调用 mg_listen 返回的监听连接的 c->is_closing=1 停止监听socket),然后等待一段时间或立即调用 mg_mgr_free

Q6: UDP 广播/多播怎么实现?
A6: 可以使用 Mongoose 的 UDP 支持来实现。比如发送广播:用 mg_connect(&mgr, "udp://255.255.255.255:port", fn, NULL) 获取 UDP 连接,然后 mg_send() 数据即可。接收广播则用 mg_listen(&mgr, "udp://0.0.0.0:port", fn, NULL) 监听对应端口,所有广播包会触发 MG_EV_READ。对于多播,暂时需要自行加入多播组:可以在建立 UDP socket 后,通过 setsockopt (如果在支持BSD socket的平台) 加入组。目前 Mongoose 未封装多播组加入接口,可能需要在 MG_EV_OPEN 时获取底层 socket 描述符 (c->fd) 执行系统调用。若使用内置栈,可能需要额外实现多播功能(7.x版本内置栈对多播支持有限)。

Q7: Mongoose 可以同时处理多少连接?
A7: Mongoose 本身没有硬编码连接数上限,受限于系统资源(内存、FD数等)。每个 mg_connection 大小大约几百字节,加上缓冲区。嵌入式设备内存小,能处理的并发连接有限,但在 PC 上上千连接也是可以的。需要注意的是默认配置下每个连接都有8K缓冲区,所以1000连接可能占用约8MB内存。可以调小 MG_IO_SIZE 或根据应用模式(比如大部分闲置连接不需要大缓冲)来自行优化。事件循环每次遍历所有连接,若连接数非常多可以考虑分成多个 mg_mgr 线程以减少单线程负担,但一般几千级别连接单线程事件模型仍能应付。

调试技巧

调试网络应用可能较为复杂,Mongoose 提供了一些辅助手段:

  • **日志级别控制:**使用 mg_log_set(const char *level_spec) 调整日志级别 (Mongoose :: Documentation) (Mongoose :: Documentation)。日志分为 ERROR, INFO, DEBUG, VERBOSE 四级。调用例如 mg_log_set("3") 将全局日志级别设为 DEBUG(0=NONE,1=ERROR,2=INFO,3=DEBUG,4=VERBOSE) (Mongoose :: Documentation)。也可以针对特定源文件设置不同级别,例如:mg_log_set("2,mongoose.c=4") 表示全局INFO级,mongoose.c文件输出VERBOSE级别 (Mongoose :: Documentation)。缺省日志通过 printf 输出,可在编译时重定向 printf,或者使用 mg_log_set_fn(mg_pfn_t fn, void *param) 提供自定义日志函数,将日志字符流发送到UART、文件或其它系统日志 (Mongoose :: Documentation) (Mongoose :: Documentation)。
  • **调试输出 (Hexdump):**如果定义编译选项 MG_ENABLE_HEXDUMP=1(默认开启),Mongoose 会在日志中输出每个TCP连接的收发数据的十六进制转储,非常有助于定位协议问题。可以在初始化时调用 mg_hexdump(c->recv.buf, c->recv.len) 手动输出缓冲区内容 (Mongoose :: Documentation) (Mongoose :: Documentation)。或者设置任意连接的 c->is_hexdumping = 1,则该连接后续所有数据I/O都会以hex形式打印日志(需DEBUG级日志有效)。
  • **事件追踪:**通过在回调中打印 ev 及相关数据,可跟踪状态机。由于 MG_EV_POLL 过于频繁,通常忽略,仅关心 READ/WRITE/CONNECT/CLOSE 等。MG_EV_ERROR 提供了错误信息字符串,可在调试时打印出来定位问题 (Mongoose :: Documentation)。
  • **Wireshark 分析:**对于标准协议如 HTTP/MQTT,借助抓包工具Wireshark也是必要手段。在PC上直接抓本机流量,在嵌入式上可以通过串口输出(hex dump)或将设备网络接入PC以桥接方式抓取。Mongoose hex dump 输出可通过 xxd -r 转换为二进制再让 Wireshark 分析,或者更简单的方法是将日志拷贝,手工解析关键部分。
  • **单步调试:**Mongoose 源码精简,完全可以在调试器中单步。可以设置断点在事件触发处(例如特定 ev 值),或者在 mg_http_serve_dir 等关键函数检查参数。了解一些关键内部状态如 c->send.len, c->recv.len, c->is_connecting 等,也有助于理解执行流程。

利用以上技巧,可以较快定位多数常见问题。特别是在封闭环境中调试网络,日志和hexdump是最有效的方法。

安全建议

在嵌入式联网应用中,安全性至关重要。以下是使用 Mongoose 时的一些安全考虑:

  • **及时更新版本:**Mongoose 不断发布新版本修复漏洞和改进功能(例如 Mongoose 7.x 相比老版本6.x提升了TLS支持和安全性)。请关注官方发布的版本更新日志并尽量使用最新稳定版本。当前 7.17 版在 2025 年发布 (Tags · cesanta/mongoose · GitHub)。如果条件允许,评估升级到后续主版本(如8.x),但需注意兼容性改变。
  • **使用加密协议:**尽可能使用 TLS 来保护通信安全。在 HTTP 场景,尽量使用 HTTPS 替代明文 HTTP,防止敏感数据泄露。Mongoose 提供内置 TLS,嵌入式设备也应尽量加载证书以实现加密通信。如果设备算力有限难以跑TLS,可以考虑架设安全网关等方案,但敏感信息一定要加密传输。
  • **验证证书与主机名:**在 TLS 客户端中,一定提供 CA 证书并验证服务器证书有效性 (Mongoose :: Documentation)。禁用验证虽方便调试但留下隐患。可通过 opts.name 开启主机名校验 (Mongoose :: Documentation),防止 MITM 攻击伪造证书。
  • **身份认证:**若设备提供控制接口(HTTP API 等),应加认证机制避免未授权访问。可以使用 HTTP Basic Auth(mg_http_creds/mg_http_bauth)或更安全的基于令牌的认证。至少在局域网外的接口需要密码保护或签名校验。
  • **输入验证:**Mongoose 不会自动过滤应用层的数据,开发者需对收到的数据进行合理验证。例如:
    • 检查 HTTP 请求的路径和参数是否符合预期,避免恶意构造输入造成缓冲区溢出或非法文件访问(mg_http_serve_dir 默认已经处理了..路径跳转问题,但自行操作文件时要小心)。
    • 在使用 mg_http_get_var 时,提供的缓冲区长度要足够,函数会避免溢出但如果长度不够会返回 -1,需正确处理这种情况 (Mongoose :: Documentation)。
    • 对于 JSON、XML 等格式数据,使用健壮的解析库或函数(Mongoose 有简易的 mg_json_* 函数,但功能有限),并验证内容逻辑正确。
  • **防御DoS攻击:**嵌入式服务器可能受到恶意大量请求或长连接拖垮。可采取:
    • 限制每客户端的并发连接数。
    • 设置请求超时。例如利用 mg_timer 给连接计时,超时未完成握手或未发送数据则关闭。
    • 对于长连接(如 WebSocket、MQTT),定期发送心跳并监测对方响应,以断开失效连接释放资源。
    • 调整缓冲区以平衡内存和性能,例如对于只发送小通知的 IoT 设备,可将 MG_IO_SIZE 调小,防止单连接占用过多RAM。
  • **最小权限原则:**如果设备文件系统有敏感信息,避免将其目录暴露通过 mg_http_serve_dir。只开放必要的文件目录。使用 opts.auth_file 等机制(Mongoose 旧版本支持 .htpasswd 认证文件,这在7.x改为由应用自行实现Basic Auth)。
  • **防火墙和网络隔离:**在网络架构上,为嵌入式设备部署防火墙策略。例如限制只接受特定IP范围的连接,或使用VPN将控制流量加密。即使设备Web界面本身有认证,也建议网络层做隔离,减少暴露面。
  • **定期测试:**对设备的网络接口做渗透测试,例如使用 nmap、OpenSSL 工具测试TLS握手、测试HTTP的常见漏洞(XSS/SQL注入等虽然主要针对web服务器后台,但如设备直接把请求参数用于系统命令就会有风险)。确保 Mongoose 提供的服务没有明显漏洞。

综合来说,Mongoose 提供了实现网络功能的便利,但最终安全取决于使用它的应用逻辑是否严谨。通过遵循安全开发的最佳实践,可以构建一个既功能丰富又安全可靠的嵌入式联网应用。 (GitHub - cesanta/mongoose: Embedded web server, with TCP/IP network stack, MQTT and Websocket) (GitHub - cesanta/mongoose: Embedded web server, with TCP/IP network stack, MQTT and Websocket)

参考资料: