- 嵌入式
- 网络
- 通信
- 物联网
【免费下载链接】mongoose
Embedded web server, with TCP/IP network stack, MQTT and Websocket
导读
本指南围绕 Mongoose 内置 TCP/IP 协议栈(Mongoose Embedded Network Stack)在 Linux TUN/TAP 设备上的接入实战展开:示例程序通过标准 socket 接口读写一个 TAP 设备,让 Mongoose 的 HTTP/HTTPS、DHCP、SNTP 等网络能力直接跑在一个虚拟网卡之上。文章完整覆盖 TAP 接口的创建、三种典型的宿主机组网方式(纯虚拟接口、桥接到局域网、转发与 NAT 伪装)以及背后的驱动回调与栈事件原理。读完本文,你将掌握如何把 Mongoose 内置协议栈挂到任意"能收发以太网帧"的字符设备上,并能在自己的 Linux 工作站上复现一套完整的嵌入式 Web 服务环境。
示例概览:为什么用 TAP 接口跑 Mongoose
tutorials/tcpip/tap-driver/是一个把 Mongoose 内置 TCP/IP 协议栈 接入 Linux TUN/TAP 设备的完整示例,核心思路非常直接:
- Mongoose 内置协议栈只关心"能否从底层拿到原始以太网帧"——只要实现了收发帧的驱动回调,它就能独立完成 ARP、IP、TCP/UDP、DHCP 客户端、DNS 与 SNTP 请求等全套网络工作,不依赖操作系统协议栈;
- TUN/TAP 是 Linux 提供的虚拟网络设备,其中TAP 工作在二层(以太网帧),恰好与 Mongoose 内置栈的 L2 接口天然匹配;
- 示例使用标准 socket 接口(
open/read/write/ioctl)打开并读写 TAP 设备,因此既不需要内核模块,也不需要第三方驱动,make即可运行。
示例程序(main.c)在启动后会监听两个端口:
Mongoose version : v<MG_VERSION> Listening on : http://0.0.0.0/ Listening on : https://0.0.0.0/这里的HTTP_URL/HTTPS_URL由 Makefile 通过编译宏注入,默认值为http://0.0.0.0/与https://0.0.0.0/(见 net.h 中的回退定义http://0.0.0.0:8000与https://0.0.0.0:8443)。也就是说,一旦 TAP 接口就绪,这台"虚拟设备"就同时提供明文 HTTP 与 TLS HTTPS 服务。
构建配置要点
Makefile 中有一段关键编译选项:
CFLAGS_MONGOOSE += -DMG_ENABLE_LINES=1 -DMG_ENABLE_TCPIP=1 -DMG_ENABLE_SOCKET=0 -DMG_ENABLE_TCPIP_DRIVER_INIT=0其含义是:
| 编译宏 | 作用 |
|---|---|
MG_ENABLE_TCPIP=1 | 启用 Mongoose 内置 TCP/IP 协议栈(即net_builtin.c实现的那套协议) |
MG_ENABLE_SOCKET=0 | 关闭对操作系统 socket API 的依赖,网络完全由内置栈接管 |
MG_ENABLE_TCPIP_DRIVER_INIT=0 | 禁止栈自动调用驱动init,因为本示例自己负责打开 TAP 设备 |
MG_ENABLE_LINES=1 | 日志输出附带源码文件名与行号,便于调试 |
启动示例时如果不带任何参数,它默认打开名为tap0的接口、使用 MAC 地址02:00:01:02:03:77。可用的命令行参数(解析逻辑见 main.c):
| 参数 | 说明 | 默认值 |
|---|---|---|
-i ifcname | 指定要打开的 TAP 接口名称 | tap0 |
-mac XX:XX:XX:XX:XX:XX | 指定网卡 MAC 地址 | 02:00:01:02:03:77 |
-v level | 设置日志级别(mg_log_set) | 由MG_LL_*默认值决定 |
接口的创建由示例之外完成(见下文 Linux 配置),示例只是"打开"一个已存在的接口;接口会随示例退出而消失,这正是文档建议"先建好接口、再做 DHCP/联网准备"的原因。
源码级解读:TAP 驱动与内置栈的对接方式
驱动接口:三个回调函数
Mongoose 内置栈通过struct mg_tcpip_driver与硬件/虚拟设备解耦,其定义在 mongoose.h:
struct mg_tcpip_driver { bool (*init)(struct mg_tcpip_if *); // One-time hardware init; return false on failure size_t (*tx)(const void *, size_t, struct mg_tcpip_if *); // Transmit one raw frame; return bytes sent size_t (*rx)(void *buf, size_t len, struct mg_tcpip_if *); // Read one raw frame; return frame size, 0 if none bool (*poll)(struct mg_tcpip_if *, bool); // Check link status; second arg is true once per second };TAP 示例在 main.c 中实现了其中三个(init因MG_ENABLE_TCPIP_DRIVER_INIT=0被跳过,改由主函数自行完成):
tap_tx:调用write()把一帧原始以太网数据写入 TAP 设备文件描述符,写入失败时打印MG_ERROR并返回 0;tap_rx:调用read()从 TAP 设备读取一帧;读取后usleep(1)让出 CPU,避免轮询把单核跑到 100%(注释原文 "This is to avoid 100% CPU");tap_poll:轮询链路状态——return s1 && ifp->driver_data ? true : false;,即只要每秒一次的周期 tick 到来且驱动数据(文件描述符)有效,就认为链路 UP。
随后把这三个回调组装进struct mg_tcpip_driver,连同文件描述符一起塞进struct mg_tcpip_if:
struct mg_tcpip_driver driver = {.tx = tap_tx, .poll = tap_poll, .rx = tap_rx}; struct mg_tcpip_if mif = {.driver = &driver, .driver_data = &fd, .enable_req_dns = true, .enable_req_sntp = true, .fn = mif_fn};driver_data指向 TAP 设备的文件描述符,驱动回调通过ifp->driver_data取回它——这是 Mongoose 驱动体系的标准做法,任何外部硬件(W5500、w5100、PHY 等,见 src/drivers 目录)都遵循同一模式。
打开 TAP 设备的细节
main.c 展示了 Linux 与 OpenBSD 两种打开方式:
#ifndef __OpenBSD__ const char *tuntap_device = "/dev/net/tun"; #else const char *tuntap_device = "/dev/tap0"; #endif int fd = open(tuntap_device, O_RDWR); ... ifr.ifr_flags = IFF_TAP | IFF_NO_PI; // Linux: TAP 模式,去掉额外的 4 字节包头 if (ioctl(fd, TUNSETIFF, (void *) &ifr) < 0) { ... } ... fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); // 非阻塞模式关键点:
- Linux 下统一打开字符设备
/dev/net/tun,用TUNSETIFFioctl 把 fd 绑定到指定名称的 TAP 接口; IFF_TAP表示二层以太网设备,IFF_NO_PI去掉 Linux 默认附加的 4 字节包信息头,让 Mongoose 收到的就是纯以太网帧;- 随后用
O_NONBLOCK设为非阻塞,这样tap_rx在无数据时read返回负值,驱动返回 0 即可。
内置栈初始化与事件回调
mg_tcpip_init(&mgr, &mif)完成栈的初始化。从 src/net_builtin.c 的实现可以看到初始化时的几个自动行为:
- 若
mif.ip == 0(未配置静态 IP)且 L2 类型不是 PPP/PPPoE,则自动开启 DHCP 客户端(ifp->enable_dhcp_client = true)——这就是为什么示例默认依赖 DHCP 分配地址; - 未设置
dhcp_name时默认使用主机名"mip"参与 DHCP 请求; - 未指定
recv_queue.size时,轮询式驱动(有rx回调)默认队列大小等于framesize; mg_tcpip_init会调用driver->init(本示例已通过编译宏禁用,改由用户代码打开设备)。
示例还注册了网络事件回调mif_fn(main.c),处理两类事件:
if (ev == MG_TCPIP_EV_STATE_CHANGE) { MG_INFO(("State change: %u", *(uint8_t *) ev_data)); } else if (ev == MG_TCPIP_EV_DHCP_DNS) { MG_INFO(("Got DNS from DHCP: %M", mg_print_ip4, (uint32_t *) ev_data)); } else if (ev == MG_TCPIP_EV_DHCP_SNTP) { ... // 构造 udp://<sntp-ip>:123 的 SNTP 服务器地址 }MG_TCPIP_EV_*事件族在 mongoose.h 中有完整定义,本示例用到的有:
| 事件 | ev_data 类型 | 含义 |
|---|---|---|
MG_TCPIP_EV_STATE_CHANGE | uint8_t *(指向ifp->state) | IPv4 链路状态变化 |
MG_TCPIP_EV_DHCP_DNS | uint32_t * | DHCP 分配了 DNS 服务器 |
MG_TCPIP_EV_DHCP_SNTP | uint32_t * | DHCP 分配了 SNTP 服务器 |
链路状态机取值(MG_TCPIP_STATE_*,见 mongoose.h):DOWN(0) →LINK_UP(1) →UP(2) →REQ(3,等待 DHCP offer) →IP(4,已获 IP、解析网关 MAC) →READY(5,完全就绪)。测试代码 test/mip_tap_test.c 正是轮询mif.state == MG_TCPIP_STATE_READY来判断协议栈是否完全可用。
DHCP 请求参数:DNS 与 SNTP
示例把enable_req_dns与enable_req_sntp都置为true,这意味着 DHCP 客户端会在请求中携带选项 6(DNS)与选项 42(SNTP)。对应实现见 src/net_builtin.c(DHCP 请求构造时追加*p++ = 6; *p++ = 42;)以及 src/net_builtin.c(解析 DHCP 响应后触发MG_TCPIP_EV_DHCP_DNS/MG_TCPIP_EV_DHCP_SNTP事件)。示例在收到 SNTP 服务器后,还会动态拼出udp://<ip>:123地址供 SNTP 同步时间使用(并在 net.c 中通过定时器周期性调用mg_sntp_connect与time.google.com同步)。
示例自带的 Web 应用
示例的"业务层"在 net.c 中实现,它监听 HTTP 与 HTTPS 两个端口(mg_http_listen),并实现了完整的 REST API:
POST /api/login/POST /api/logout:基于用户名口令或 access_token 的登录态管理(authenticate从Authorization: Basic或Cookie: access_token中取凭据),登录成功通过 HttpOnly Cookie 下发 token,TLS 连接下 Cookie 还会带Secure属性;GET /api/stats/get:返回温度、湿度等模拟遥测数据;GET /api/events/get:分页返回模拟告警事件流(每页 20 条,最多 400 条);GET/POST /api/settings/get|set:读取/修改设备设置(日志开关、日志级别、亮度、设备名);POST /api/firmware/upload:通过mg_ota_begin/mg_ota_write/mg_ota_end演示固件在线升级(OTA);- 其余路径由
mg_http_serve_dir提供静态文件服务:工作站上读取web_root目录,嵌入式环境则改用mg_fs_packed打包文件系统。
这些 API 与 Mongoose 的 HTTP 路由、JSON 解析(mg_json_get_*)、格式化输出(mg_xprintf与%M自定义打印)深度绑定,可作为在 TAP 网络里验证端到端连通性的现成测试目标。
Linux 端准备:创建 TAP 接口
首先创建并启用一个 TAP 接口(示例默认使用tap0):
$ sudo ip tuntap add dev tap0 mode tap $ sudo ip link set tap0 up注意事项:
- 某些系统上示例程序需要超级用户权限才能打开该设备;
- 也可以在创建接口时直接授予用户/组访问权限,例如
sudo ip tuntap add dev tap0 mode tap user yourusername或group yourgroup; - 接口生命周期:如果由示例自行创建接口,进程退出后接口会随之消失;而网络准备工作(DHCP 服务器、对外连接)通常需要提前就位,因此文档强烈建议先手动建好接口、再启动示例——这样接口会一直存在,DHCP 服务器等设施可以常驻。
组网方式一:纯虚拟接口(仅宿主机可达)
这种模式下,Mongoose 只能从宿主机访问,但后续可以随时叠加转发与 NAT。步骤:
- 给 TAP 接口分配 IP:
$ sudo ip addr add 192.168.0.1/24 dev tap0 - 在该接口上启动一个为
192.168.0.0/24子网服务的 DHCP 服务器;或者反过来,给 Mongoose 配置该子网内的固定地址(静态 IP 场景下在struct mg_tcpip_if中设置mif.ip、mif.mask、mif.gw,参考 test/mip_tap_test.c 的写法); - 启动示例,打开同一个 TAP 接口:
$ make -C tutorials/tcpip/tap-driver/ clean all如果接口名不是
tap0,追加ARGS="-i ifcname"(使用实际接口名)。
拓扑示意:
[DHCP server for 192.168.0.x] 192.168.0.1 192.168.0.x ┌─────────┐ │ tap0 ├──────────────────── tap-driver └─────────┘由于这种模式下宿主机之外的主机都无法访问该接口,DNS 等必要服务需要同样部署在宿主机上,并在 Mongoose 侧配置正确(DHCP 下发的 DNS 由enable_req_dns自动获取)。这是后续所有组网方案的基础形态。
组网方式二:桥接到局域网
如果你已经有虚拟网桥(例如用于连接多个虚拟机的br0/virbr0),可以把 TAP 接口挂到网桥上,Mongoose 将获得整个局域网的访问权,同时也能被工作台和网内其他主机访问。
准备网桥(没有网桥时)
把物理网卡(NIC)纳入网桥时,IP 地址要从网卡迁移到网桥接口上;如果走 DHCP,DHCP 客户端也要运行在网桥接口而不是网卡上。示例(网卡enp9s0,原 IP10.1.0.10/24):
$ sudo ip link add virbr0 type bridge $ sudo ip link set virbr0 up $ sudo ip addr del 10.1.0.10/24 dev enp9s0 $ sudo ip link set enp9s0 master virbr0 $ sudo ip addr add 10.1.0.10/24 dev virbr0用ifconfig检查,并尝试 ping 网内主机验证桥接是否生效。
把 TAP 挂到网桥
$ sudo ip link set tap0 master virbr0注意:此时tap0的状态在示例尚未打开它之前会显示为 DOWN(NO-CARRIER),这是正常现象:
$ ip link show master virbr0 2: enp9s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master virbr0 state UP mode DEFAULT group default qlen 1000 link/ether 30:5a:3a:08:db:90 brd ff:ff:ff:ff:ff:ff 6: tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast master virbr0 state DOWN mode DEFAULT group default qlen 1000 link/ether 66:91:e2:5f:d7:ed brd ff:ff:ff:ff:ff:ff启动示例
$ make -C tutorials/tcpip/tap-driver/ clean all接口名不同时同样追加ARGS="-i ifcname"。一切正常的话,示例运行后tap0会变为UP(LOWER_UP出现):
$ ip link show master virbr0 2: enp9s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master virbr0 state UP mode DEFAULT group default qlen 1000 link/ether 30:5a:3a:08:db:90 brd ff:ff:ff:ff:ff:ff 6: tap0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master virbr0 state UP mode DEFAULT group default qlen 1000 link/ether 66:91:e2:5f:d7:ed brd ff:ff:ff:ff:ff:ff拓扑示意:
10.1.0.10 virbr0 │ ┌──────────────────┴──────────────────┐ │ │ 10.1.0.x │ ┌────────┐ │ │ │ tap0 ├─────────────┼─────── tap-driver │ └────────┘ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ vnet1 │ │ enp9s0 │ │ vnet2 │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ └──────┼───────────┼───────────┼──────┘ │ │ │ │ Ethernet │ Local LAN 10.1.0.x │ DHCP server, router ▼ Internet桥接模式下无需像方式一那样手动加 IP,除非你的局域网里没有 DHCP 服务器——那时才需要给 Mongoose 配一个子网内的固定地址。
注意:如果宿主机运行着 Docker,它引入的防火墙规则可能会破坏桥接(Docker 默认的 FORWARD 链策略会拦截桥接流量)。另外,桥接通常无法用于 Wi-Fi,原因见文末附录。
组网方式三:转发与 NAT(Masquerade)
当虚拟接口已有 IP 时,可以让 Linux 充当 NAT/转发网关,把 Mongoose 的流量转发到另一张接口(例如上网用的enp9s0)。这种模式下 Mongoose 能访问 Internet,工作台也能访问它,但局域网内其他主机访问不到(如需隔离场景非常合适)。
- 配置伪装与转发(前提:你已经配好了防火墙):
$ sudo iptables -t nat -A POSTROUTING -o enp9s0 -j MASQUERADE $ echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward注意把
enp9s0换成实际的出网接口名。 - 启动示例:
$ make -C tutorials/tcpip/tap-driver/ clean all接口名不同时追加
ARGS="-i ifcname"。
拓扑示意:
[DHCP server for 192.168.0.x] ┌────────────► 192.168.0.1 192.168.0.x │ forwarding ┌─────────┐ │ masquerading │ tap0 ├──────────────────── tap-driver │ └─────────┘ ▼ Your IP (LAN / Public) ┌─────────┐ │ enp9s0 │ └────┬────┘ │ │ Ethernet [WiFi] │ ▼ Internet变体:NIC 本身已在网桥中
如果你的物理网卡是网桥的一部分(例如你在跑虚拟机),则用网桥接口做伪装而不是物理网卡(此时 IP 属于网桥而不是网卡):
$ sudo iptables -t nat -A POSTROUTING -o virbr0 -j MASQUERADE $ echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward记得使用正确的网桥接口名。
拓扑示意:
[DHCP server for 192.168.0.x] 10.1.0.10 ◀──────────────► 192.168.0.1 192.168.0.x virbr0 forwarding ┌─────────┐ │ masquerading │ tap0 ├──────────────────── tap-driver │ └─────────┘ ┌──────────────────┴──────────────────┐ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ vnet1 │ │ enp9s0 │ │ vnet2 │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ └──────┼───────────┼───────────┼──────┘ │ │ │ │ Ethernet │ Local LAN 10.1.0.x │ DHCP server, router ▼ Internet同样地,Wi-Fi 场景下桥接通常不可用,参见附录。
附录:为什么 Wi-Fi 上桥接通常行不通
由于无线标准的工作方式,桥接往往要求能够深入配置接入点/无线路由器(AP),原因在于地址字段的差异:
- 以太网帧只需要 Sender Address(SA)与 Destination Address(DA)两个 MAC 地址;
- Wi-Fi 额外要求 Transmitter Address(TA)与 Receiver Address(RA)字段,一共 4 个 MAC 地址;
- 常见场景(STA 到 AP 的帧 TA = SA,AP 到 STA 的帧 DA = RA)实际上只用了 3 个地址,因此在 AP 上做桥接通常没有问题——AP 只需为 AP→STA 帧改 SA、为 STA→AP 帧改 DA,仍然只用 3 个地址;
- 但在 STA(无线工作站)上做桥接时:STA→AP 帧的 SA 与 TA 不再相等,AP→STA 帧的 DA 与 RA 也不同,必须用满 4 个地址——而大多数 STA 与 AP 默认都不支持这种模式。STA 侧可能需要配置工具开启
4addr模式,AP 侧则类似两台 LAN 桥接的场景,需要启用 WDS(Wireless Distribution System)。即便两侧都能配置,由于不同厂商实现方式存在兼容性问题,仍然无法保证一定可用。
因此本文档的三种组网方式中,桥接方案默认面向有线网络;无线环境请优先考虑方式一(纯虚拟接口)或方式三(转发 + NAT)。
延伸阅读
- 示例入口与 TAP 驱动实现:tutorials/tcpip/tap-driver/main.c、tutorials/tcpip/tap-driver/net.c
- 驱动与接口结构体、事件定义:mongoose.h
- 内置协议栈核心实现:src/net_builtin.c(
mg_tcpip_init见 src/net_builtin.c) - 官方测试中基于同一 TAP 驱动做协议栈测试与 TCP 丢包重传验证:test/mip_tap_test.c
- 同一思路的抓包版驱动示例(基于 pcap 读原始帧):tutorials/tcpip/pcap-driver
- 更多内置栈驱动的参考实现(W5500、W5100、PHY 等):src/drivers
- 嵌入式
- 网络
- 通信
- 物联网
【免费下载链接】mongoose
Embedded web server, with TCP/IP network stack, MQTT and Websocket
相关推荐
RP2040/RP2350 上的 Mongoose 内置 TCP/IP 协议栈:基于 PIO 的 RMII 以太网驱动实战
RP2040/RP2350 上的 Mongoose 内置 TCP/IP 协议栈:基于 PIO 的 RMII 以太网驱动实战 本文围绕 tutorials/rp/
嵌入式网络通信物联网Mongoose 内置 TCP/IP 协议栈(net_builtin)安全审查实战指南:攻击面、边界校验与驱动防护
Mongoose 内置 TCP/IP 协议栈(net_builtin)安全审查实战指南:攻击面、边界校验与驱动防护 导读 本文面向嵌入式固件开发者与安全工程师,
嵌入式网络通信物联网RP2040/RP2350 + RM2 WiFi 模块跑通 Mongoose 内置 TCP/IP 协议栈:从 PIO SPI 驱动到 HTTP 服务器实战
RP2040/RP2350 + RM2 WiFi 模块跑通 Mongoose 内置 TCP/IP 协议栈:从 PIO SPI 驱动到 HTTP 服务器实战 本指
嵌入式网络通信物联网
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考