RISC-V Linux 嵌入式编程技术 · 第五章

网络与 MQTT

Broker / 主题 / 发布订阅 → 下行控灯 → 上行状态与抓包分层
— 5.1 / 5.2 / 5.3 —

板卡:荔枝派 4A 工具链:RuyiSDK 脚手架:mqtt-led

开始之前

前置条件:第四章串口命令温控已验收;荔枝派 4A 能上网(与 Broker 同网或可达)。本章用 MQTT(一种轻量消息协议)做远程控灯与状态上报。每节约 90–120 分钟。

你需要准备什么

主机

RuyiSDK 交叉工具链;可安装 Mosquitto(MQTT 中转软件)或使用课程指定 Broker 地址。

板卡 + LED

荔枝派 4A;外接 LED(GPIO 输出,默认 IO1_4)。电脑终端用 mosquitto_pub / mosquitto_sub 发命令、看状态。

代码

chapters/ch05/code/mqtt-led/:订阅控灯与发布状态的桩函数待你补全。

本章你会学到什么

5.1 Broker、主题、发布订阅

先对照 listen / connect;再画发布—Broker—订阅。Broker 是中转站,主题是频道名。

5.2 下行:订阅命令控 LED

板子订阅命令主题;收到 on/off 点灯或灭灯。

5.3 上行:发布状态

板子把 LED 状态 publish 出去,订阅端能看见。

讲义 vs 实验:讲义建立模型并跟做 Broker 与抓包步骤;「深入理解」把一次 publish 从应用到以太网拆开看。本章实验完成「电脑终端控灯 + 状态可见 + 抓一条 publish」。
5.1

Broker、主题、发布订阅

listen 与 connect

TCP 连接里有两个角色:一方在本地某个端口上 listen(等别人连进来),另一方 connect 到对方的 IP 和端口。本课用得到下面两种摆法,课堂验收用第二种。

摆法一:板上 listen,电脑终端 connect 到板的 IP 荔枝派:listen(某端口) 电脑终端:connect(板的局域网 IP, 某端口) 电脑终端要记下板的地址;连入发生在同一局域网里。 摆法二(本课):Broker listen,板和电脑终端都 connect 过去 主机 Mosquitto:listen(1883) 荔枝派 mqtt-led:connect(BROKER_HOST, 1883) 电脑终端 mosquitto_pub / mosquitto_sub:connect(同一个 BROKER_HOST, 1883) BROKER_HOST 填主机的局域网 IP。双方用主题名收发,电脑终端按主题发命令。

板子地址是 192.168.x.x 这一类局域网地址时,只在同一网段里能被 connect。本课让 Mosquitto 在主机上 listen,板和电脑终端都向外 connect,板保持作为客户去连 Broker。

发布订阅模型

MQTT 用发布订阅:大家都 connect 到中转站。Broker 是消息中转站(本课常用软件 Mosquitto):连上它的程序按主题(频道名,如 course/led/cmd)收发。publish(发布)是往某个主题发一条消息;订阅是盯着某个主题收消息。

电脑终端(发布者) │ publish course/led/cmd payload="on" ▼ Broker(Mosquitto) │ 转发给所有订阅者 ▼ 荔枝派 4A 程序(订阅者)→ GPIO → 外接 LED 反方向: 板子 publish course/led/status payload="on" → Broker → 电脑终端看见状态
电脑终端 pub / sub Broker Mosquitto :1883 荔枝派 4A mqtt-led 外接 LED cmd/on status 上行 course/led/cmd ↓ · course/led/status ↑
架构图:双方都 connect 到 Broker,用主题名收发,不必知道对方 IP
术语一句话本课例子
Broker消息中转站主机或板上的 Mosquitto,默认端口 1883
主题频道名course/led/cmd、course/led/status
publish(发布)往某主题发一条电脑终端发 on;板子报状态
订阅盯着某主题收板子订 course/led/cmd
payload消息正文on / off

启动 Mosquitto(主机示例)

板端要连上 Broker,主机上的 Mosquitto 必须监听局域网(不能只绑 127.0.0.1),并按课堂约定决定是否允许匿名。本课实机联调采用:端口 1883、监听所有网卡、允许匿名(无用户名密码)。

# Debian/Ubuntu 示例;以你发行版包名为准
$ sudo apt update
$ sudo apt install -y mosquitto mosquitto-clients
$ sudo systemctl enable --now mosquitto

局域网监听与匿名(必配,否则板连不上)

默认安装后,部分发行版只允许本机访问。在跑 Broker 的主机上新建(或编辑)片段配置,例如:

$ sudo tee /etc/mosquitto/conf.d/course-lan.conf <<'EOF'
# 监听所有网卡的 1883(板子用主机局域网 IP 连接)
listener 1883
# 本课实验允许匿名;生产环境请改用账号密码并关闭此项
allow_anonymous true
EOF
$ sudo systemctl restart mosquitto
$ sudo ss -lntp | grep 1883
# 应看到 0.0.0.0:1883 或 *:1883(不要只有 127.0.0.1:1883)

若用 Docker(本课联调实录):

$ cat > /tmp/mosquitto-course.conf <<'EOF'
listener 1883
allow_anonymous true
EOF
$ docker run -d --name course-mosquitto --restart unless-stopped \
    -p 1883:1883 \
    -v /tmp/mosquitto-course.conf:/mosquitto/config/mosquitto.conf:ro \
    eclipse-mosquitto:2
# 本机自测:一终端订阅,另一终端发布(先确认本机通)
$ mosquitto_sub -h 127.0.0.1 -t 'course/led/cmd' -v
$ mosquitto_pub -h 127.0.0.1 -t 'course/led/cmd' -m 'on'
网络:在跑 Mosquitto 的主机上查本机局域网 IP:
$ hostname -I
# 或:ip -4 addr show | grep inet
板端把该 IP 交给程序:改源码里的 DEFAULT_BROKER_HOST,或运行时 BROKER_HOST=<该IP> ./mqtt-led(环境变量优先)。板上的 127.0.0.1 只指向板自己。防火墙放行 1883(如 sudo ufw allow 1883/tcp,以你主机防火墙为准)。

课堂若统一提供 Broker:不必自装;记下老师给出的 BROKER_IP、1883(或当堂端口)、是否匿名/账号密码,板端用同一套参数连接即可。
本节成果:能对照「板上 listen」和「双方都 connect 到 Broker」· 能画发布—Broker—订阅三角 · Mosquitto 能收发一条测试消息 · 记下 Broker IP 与主题名。
5.2

下行:订阅命令驱动外接 LED

主题与 payload 约定

本课默认:

电脑终端 mosquitto_pub │ topic=course/led/cmd msg=on ▼ Broker ▼ mqtt-led 收到消息 │ 比较 payload 是否为 on / off ▼ gpiod_line_set_value(LED, 1 或 0)
mosquitto_pub Broker on_message led_set() topic=course/led/cmd · payload=on/off
下行路径:一条 publish 经 Broker 触发回调,最后落到 GPIO

脚手架 chapters/ch05/code/mqtt-led/ 用 libmosquitto 提供连接与消息处理骨架;实验一的解析已链进工程,你在回调里调用即可。板端需有 libmosquitto 与 libgpiod;编部署优先板端原生 make。

外接 LED 接线

默认脚位:丝印 IO1_4。限流电阻串在 GPIO 与 LED 正极之间;短脚回 GND。完整摘录见 chapters/ch05/manuals/。

串联:电阻与 LED 在面包板上的接法示意
商品详情:串联在面包板上的接法示意
串联链接图
商品详情「串联链接图」
本课回路: IO1_4 → 限流电阻 → LED 长脚(正极) GND / 电源轨「−」 → LED 短脚(负极)

跟做步骤

  1. 1
    接线与配置
    外接 LED 默认 IO1_4(限流电阻串在 GPIO 与 LED 正极之间,短脚回 GND)。极性与串/并联对照见 chapters/ch05/manuals/。主机上 hostname -I 得到 Broker IP;板端用环境变量 BROKER_HOST 或改 DEFAULT_BROKER_HOST。
  2. 2
    编译并部署
    优先板端 make;需要时再用如意 SDK 交叉后 scp。file 确认 RISC-V。
  3. 3
    主机发布
    mosquitto_pub -h <broker> -t course/led/cmd -m on,板端 LED 应亮;再发 off 应灭。
# 主机:记下局域网 IP
$ hostname -I

# 板端原生编译(推荐跟做)
$ cd chapters/ch05/code/mqtt-led
$ make clean && make
$ file mqtt-led
$ BROKER_HOST=<主机局域网IP> ./mqtt-led

# 或在 x86 主机交叉后再拷到板:
# make CROSS_COMPILE=riscv64-ruyisdk-linux-gnu- && scp mqtt-led user@board-ip:~/

# 主机另一终端(BROKER_IP 用上面 hostname -I 的结果)
$ mosquitto_pub -h BROKER_IP -t 'course/led/cmd' -m 'on'
$ mosquitto_pub -h BROKER_IP -t 'course/led/cmd' -m 'off'
权限:GPIO 打不开时按第三章方式授权。MQTT 连不上时先在板端 ping BROKER_IP,再查端口与主题拼写。
本节成果:板子能订阅并响应 on/off · 外接 LED 可见动作 · 日志有收到主题与 payload 的记录。
5.3

上行:发布状态

灯态变化后 publish

灯亮灭改变后,板子向 course/led/status publish(发布)当前状态(如 on/off)。订阅该主题的电脑终端即可确认「命令是否落地」。脚手架中 publish_status() 为桩,实验里接到点灯成功之后调用。

/* 伪代码 */
led_set(1);
publish_status("on");   /* TODO: mosquitto_publish(...) */
# 主机订阅状态
$ mosquitto_sub -h BROKER_IP -t 'course/led/status' -v

应用层「发布成功」只说明库函数返回了;它在网上究竟怎么走,要靠下一节用抓包把分层拆开看。

本节成果:状态主题可被订阅端看见 · 知道上行与下行用不同主题 · 带着「一次 publish 在网上长什么样」进入深入理解。
深入理解

一次 publish:从应用到以太网,再用 tcpdump 看见

先把本节会反复用到的词说清楚:Broker 是 MQTT 中转站;主题 是频道名;publish 是往某主题发一条消息;tcpdump 是在网卡上抓原始包、打印协议字段的工具。默认明文 MQTT 跑在 TCP 端口 1883 上。

网络四层模型:一条消息套几层「信封」

你发的消息不是光着身子出门的——它被一层层装进「信封」,每层只负责自己的地址信息。这就是网络分层(简化的四层模型):

层这一层的「信封」本课看到的证据
应用层(MQTT)MQTT 报文:主题名 + payload + 报文类型tcpdump 用 -A 看到的 course/led/status / on
传输层(TCP)TCP 段:源/目的端口(1883)、序号、载荷….1883 目的端口、Flags [P.]
网络层(IP)IP 包:源/目的 IP 地址IP 板子IP > BrokerIP
链路层(以太网/Wi-Fi)帧:MAC 地址、类型、载荷有线抓包的 MAC 行(本课可选看)

分层的好处:每一层只需要「在信封上写自己的地址,然后把信封原样传给下一层」——应用不管 TCP 怎么重传,网络不管帧在哪个网段。下面把一次 publish 顺着这四层走一遍。

原因 → 机制:一次 publish 怎么一层层包下去

你在板子上调用 mosquitto_publish(...) 时,应用只交出「主题 + payload」。库和内核再把它装进 MQTT 报文,塞进 TCP 连接(目的端口 1883),外面再套 IP,最外层是以太网帧(有线)或 Wi-Fi 帧。Broker 拆包后按主题转发给订阅者。

应用:mosquitto_publish(topic="course/led/status", payload="on") │ ▼ MQTT 报文(应用层:主题名、payload、报文类型等) ▼ TCP 段(传输层:源端口、目的端口 1883、序号、载荷) ▼ IP 包(网络层:源 IP、目的 IP) ▼ 以太网帧(链路层:MAC、类型、帧载荷) │ 网线 / Wi-Fi ──► Broker 主机

图说明:每一层只多加自己的头尾,并不改写你业务上的「on」含义;tcpdump 能同时看到这些头,所以能对照「应用里的一次 publish」和「网上的一串包」。

验证:标注一份看起来像 tcpdump 的输出

在 Broker 主机或板端(以能看到板→Broker 流量为准)开抓包,再触发一次上行 publish:

$ sudo tcpdump -i any -nn -A port 1883 -c 20

下面是一份示意输出(字段形状与真实 tcpdump 相近;IP/时间以你机器为准)。右侧注释标出各层:

14:02:11.123456 IP 192.168.1.50.45678 > 192.168.1.10.1883: Flags [P.], ...
                 │                │              │     │
                 │                │              │     └─ 目的端口 1883 → TCP 上的 MQTT
                 │                │              └─ Broker 主机 IP(网络层目的)
                 │                └─ 板子临时源端口(传输层)
                 └─ 板子 IP(网络层源)

    …… TCP 载荷的一部分(-A 以 ASCII 显示)……
    ....course/led/status..on....
         │                 │
         │                 └─ payload 痕迹(明文 MQTT 时可能露出)
         └─ 主题名痕迹(应用层 MQTT)

逐项对照:

你在输出里看到的对应哪一层说明什么
IP a.b.c.d > e.f.g.h网络层(IP)谁发给谁:板子 → Broker
.1883(目的端口)传输层(TCP)这是默认 MQTT 端口,不是随便一个网页流量
Flags [P.] 等TCP段在推送数据;握手/ACK 包也可能出现在同一段抓取里
ASCII 里的主题 / on应用层(MQTT)业务内容落在 TCP 载荷里;明文时可见片段
(有线抓包时的 MAC 行,若开了相应选项)链路层(以太网)帧在本地网段上传递;本课用 -nn -A port 1883 已够指认 TCP/IP/MQTT
  1. 1
    开抓包
    sudo tcpdump -i any -nn -A port 1883 -c 20
  2. 2
    触发一次上行
    让板子 publish 状态(或主机 mosquitto_pub 发一条到状态主题)。
  3. 3
    对照上表
    指出 1883、双方 IP,以及 ASCII 区是否露出主题或 payload;用自己的话复述「MQTT → TCP → IP → 出网卡」。
验收话术:「这次 publish 的内容在 MQTT 应用层;它装在 TCP(1883)里,再装在 IP 里,最后由以太网/Wi-Fi 发出。tcpdump 让我看见端口与载荷痕迹,而不是只相信库函数返回成功。」
抓包需相应权限;只抓本课端口、限制 -c 包数。不要在公共网络长时间无过滤抓包。若走 TLS(常 8883),ASCII 一般看不到明文 payload——那是加密层在起作用,分层关系不变。
本节成果:能画出 publish 的分层包装 · 能标注一份 tcpdump 片段里各字段属于哪一层 · 保存一段含 1883 的输出进入本章实验验收。