开始之前
你需要准备什么
主机
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 出去,订阅端能看见。
Broker、主题、发布订阅
listen 与 connect
TCP 连接里有两个角色:一方在本地某个端口上 listen(等别人连进来),另一方 connect 到对方的 IP 和端口。本课用得到下面两种摆法,课堂验收用第二种。
板子地址是 192.168.x.x 这一类局域网地址时,只在同一网段里能被 connect。本课让 Mosquitto 在主机上 listen,板和电脑终端都向外 connect,板保持作为客户去连 Broker。
发布订阅模型
MQTT 用发布订阅:大家都 connect 到中转站。Broker 是消息中转站(本课常用软件 Mosquitto):连上它的程序按主题(频道名,如 course/led/cmd)收发。publish(发布)是往某个主题发一条消息;订阅是盯着某个主题收消息。
| 术语 | 一句话 | 本课例子 |
|---|---|---|
| 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'
$ 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(或当堂端口)、是否匿名/账号密码,板端用同一套参数连接即可。
下行:订阅命令驱动外接 LED
主题与 payload 约定
本课默认:
- 命令主题:
course/led/cmd - payload:
on→ 灯亮;off→ 灯灭 - 未知 payload → 打
[ERR],保持当前电平
脚手架 chapters/ch05/code/mqtt-led/ 用 libmosquitto 提供连接与消息处理骨架;实验一的解析已链进工程,你在回调里调用即可。板端需有 libmosquitto 与 libgpiod;编部署优先板端原生 make。
外接 LED 接线
默认脚位:丝印 IO1_4。限流电阻串在 GPIO 与 LED 正极之间;短脚回 GND。完整摘录见 chapters/ch05/manuals/。
跟做步骤
- 1接线与配置
外接 LED 默认IO1_4(限流电阻串在 GPIO 与 LED 正极之间,短脚回 GND)。极性与串/并联对照见chapters/ch05/manuals/。主机上hostname -I得到 Broker IP;板端用环境变量BROKER_HOST或改DEFAULT_BROKER_HOST。 - 2编译并部署
优先板端make;需要时再用如意 SDK 交叉后 scp。file确认 RISC-V。 - 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'
ping BROKER_IP,再查端口与主题拼写。
上行:发布状态
灯态变化后 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:从应用到以太网,再用 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 拆包后按主题转发给订阅者。
图说明:每一层只多加自己的头尾,并不改写你业务上的「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开抓包
sudo tcpdump -i any -nn -A port 1883 -c 20 - 2触发一次上行
让板子 publish 状态(或主机mosquitto_pub发一条到状态主题)。 - 3对照上表
指出 1883、双方 IP,以及 ASCII 区是否露出主题或 payload;用自己的话复述「MQTT → TCP → IP → 出网卡」。
-c 包数。不要在公共网络长时间无过滤抓包。若走 TLS(常 8883),ASCII 一般看不到明文 payload——那是加密层在起作用,分层关系不变。