从零学习 MQTT:发布/订阅、主题、QoS 与报文解析
最近开始系统学习 MQTT 协议,整理一篇入门笔记。目标不是只记概念,而是能理解设备如何上报数据到云平台、能看懂 MQTT 报文,也能在实际项目中判断"数据为什么没传上来"。
1. MQTT 是什么
MQTT 是物联网领域非常常见的一种消息协议,常用于设备上报数据、云端下发指令,比如储能 EMS 控制器、智能电表、温控器、传感器等。
MQTT 采用发布/订阅(Publish/Subscribe)模式,核心是三个角色:
text
代理(Broker) = 邮局,负责中转消息
发布者(Publish) = 寄信的人
订阅者(Subscribe)= 收信的人
消息不是直接发给某个人,而是发到一个主题(Topic),可以理解成"信箱编号":
text
EMS 往主题 ems/001/power 发布一条消息(功率 50kW)
云端订阅了 ems/001/power
代理就把消息转给云端
和 Modbus 对比着记:
text
Modbus:主站问一句,从站答一句,从站不能主动说话
MQTT:设备想发就发,不用等人来问,谁订阅谁收
Modbus = 打电话(一对一,一问一答)
MQTT = 广播电台(发出去,订阅的人自己听)
2. 核心角色:代理和客户端
MQTT 通信只涉及两类角色:
text
代理(Broker):消息中转站,负责接收、过滤、转发消息
客户端(Client):设备或软件,既能发布也能订阅
一个客户端可以同时订阅很多主题,也可以往很多主题发消息,身份是灵活的。
3. 主题(Topic)和通配符
主题是消息的路由地址,像文件夹路径一样用 / 分层:
text
ems/001/power 1 号柜功率
ems/001/soc 1 号柜电池电量
ems/002/power 2 号柜功率
3.1 通配符
订阅主题时可以用通配符,一次订阅多个主题:
text
+ 匹配"一层"
# 匹配"剩余所有层"
text
订阅 ems/+/power
→ 能收到 ems/001/power 和 ems/002/power
→ 收不到 ems/001/soc(这一层不匹配)
→ 收不到 ems/001/power/extra(层数多了)
订阅 ems/#
→ ems/ 下面所有主题都能收到,不管几层
两条规则的差别:
text
ems/+/power → 只要功率,精确
ems/# → 什么都收,省事但消息多
实际项目里需要什么就订阅多精确的主题,不要一上来就 #,否则流量和干扰消息会很多。
3.2 主题规则
text
- 通配符只能在"订阅"时用,发布消息时不能用
- + 只匹配一层,# 必须放在主题最后
- 主题区分大小写:Power 和 power 是两个不同主题
- 不要以 / 开头,不要包含空格
- 长度不能超过 65535 字节
4. 服务质量(QoS)
QoS 表示"这条消息到底要可靠到什么程度",取值 0、1、2:
| QoS | 含义 | 说明 |
|---|---|---|
| 0 | 最多一次 | 发出去就不管了,可能丢,最快 |
| 1 | 至少一次 | 保证能到,但可能重复,中等 |
| 2 | 恰好一次 | 保证到且不重复,最可靠,最慢 |
打个比方:
text
QoS 0 = 寄平信,丢了就丢了
QoS 1 = 寄挂号信,丢了重寄,可能寄两封(重复)
QoS 2 = 专人送达并签收,绝不重复
实际选择(储能场景):
text
实时功率、频率 → QoS 0(晚一秒就没意义,丢一条无所谓)
电表读数、状态量 → QoS 1(不能丢,重复了用最新值覆盖)
计费、关键指令 → QoS 2(绝对不能丢也不能重复)
注意一个容易踩的坑:
text
发布端 QoS 1 + 订阅端 QoS 0 = 实际按 QoS 0 投递
(两边都要求了才算数,取两者中较小的)
5. 保留消息(Retain)
云端平台刚开机、刚订阅某个主题时,设备可能还要等 60 秒才发下一条数据。保留消息解决的就是"新订阅者一上来就能拿到当前状态"的问题:
text
发布时勾选"保留"(Retain)
→ 代理把这条消息存下来,作为该主题的"最后一条状态"
→ 之后任何人新订阅这个主题,立刻收到这条保留消息
text
EMS 每 60 秒发布一次功率,每次都勾选保留
云端早上重启,订阅 ems/001/power
→ 不用等 60 秒,立刻收到 EMS 最后发布的那个功率值
和 QoS 的区别:
text
QoS = 消息送得可不可靠(会不会丢/重复)
Retain = 消息要不要"存档",让后来订阅的人也能拿到
清理方法:往同一个主题发布一条空消息并勾选保留,代理就会删掉之前存的保留消息。
判断标准:状态类数据(在线/离线、当前电量)适合用保留消息;纯波形、高频数据不适合。
6. 遗嘱消息(Will)
设备突然断电时,来不及通知云端"我下线了"。遗嘱消息让代理替设备说这句话:
text
1. 设备连接代理时,提前设置好遗嘱主题和遗嘱内容
2. 设备正常断开(发 DISCONNECT)→ 遗嘱不触发
3. 设备异常掉线(断电、断网、超时)→ 代理立刻发布遗嘱消息
text
EMS 连接云平台时设置:
遗嘱主题:ems/001/status
遗嘱内容:"offline"
EMS 突然断电
代理检测到连接断了
→ 自动向 ems/001/status 发布 "offline"
云端订阅了这个主题 → 立刻知道 1 号柜掉线了
和保留消息搭配使用效果最好:
text
ems/001/status 用保留消息存 "online"
设备掉线时遗嘱发一条 "offline"
→ 任何人订阅 ems/001/status,立刻看到当前真实状态
一句话记忆:遗嘱 = 设备没来得及说再见,代理替它说。
7. 心跳与会话
7.1 心跳(Keep Alive)
设备连接代理后,两边怎么判断对方还活着?靠心跳:
text
连接时告诉代理一个时间,比如 60 秒
设备每隔一段时间发一个 PINGREQ(心跳请求)
代理回一个 PINGRESP(心跳响应)
超过约定时间没收到任何报文 → 代理认为掉线 → 断开并触发遗嘱
text
心跳太短:网络稍微一卡就被误判掉线
心跳太长:设备真死了,要好几分钟才发现
常见设置:30~120 秒
7.2 会话(Session)
设备掉线后,代理要不要"记账":
text
清理会话(Clean Session = 1):
断开后代理立刻清掉订阅和排队消息
离线期间的指令直接丢掉,重连后收不到
持久会话(Clean Session = 0):
断开后代理保留订阅
离线期间的消息先存着,重连后补发
7.3 离线期间的消息怎么处理(重点)
text
多条消息:QoS 1/2 的消息全部排队,一条不丢,重连后按顺序补发
QoS 0 的消息不排队,离线期间直接丢
同一个主题多次不同值:MQTT 不合并、不只留最新,按顺序全发
例:离线期间发了 50 → 60 → 40,重连后按顺序收到三次
想要"只留最新一条"怎么办:
text
1. 保留消息(Retain):新订阅者只拿到最后一条
注意:保留消息 ≠ 离线排队,两者是独立机制
2. 应用层自己处理:收到同主题多条数据时,只认最新的时间戳或序号
(实际项目里最常用)
3. MQTT 5.0 消息过期:给消息设过期时间,过期的自动丢弃
8. MQTT 报文结构
MQTT 报文最前面是固定报头(Fixed Header),所有报文都有:
text
第 1 字节:报文类型(高 4 位)+ 标志位(低 4 位)
第 2 字节起:剩余长度(表示后面还有多少字节)
常用报文类型:
text
1 CONNECT 连接请求
2 CONNACK 连接确认
3 PUBLISH 发布消息
8 SUBSCRIBE 订阅请求
9 SUBACK 订阅确认
12 PINGREQ 心跳请求
13 PINGRESP 心跳响应
14 DISCONNECT 断开连接
心跳报文最短,只有 2 个字节:
text
C0 00 心跳请求(类型 12 = 0xC0,剩余长度 0)
D0 00 心跳响应(类型 13 = 0xD0,剩余长度 0)
8.1 拆一个 PUBLISH 报文
往主题 test/topic 发消息 hello(QoS 0):
text
30 11 00 0A 74 65 73 74 2F 74 6F 70 69 63 68 65 6C 6C 6F
text
30 第 1 字节:类型 3(PUBLISH),QoS 0,不保留
11 剩余长度 = 0x11 = 17(后面还有 17 字节)
00 0A 主题长度 = 10
74 65 73 74 2F 74 6F 70 69 63 主题 "test/topic"(10 字节)
68 65 6C 6C 6F 载荷 "hello"(5 字节)
剩余长度验证:主题长度字段(2)+ 主题内容(10)+ 载荷(5)= 17 ✓
8.2 拆一个 CONNECT 报文
客户端连代理时发的第一帧:
text
10 0D 00 04 4D 51 54 54 04 02 00 3C 00 01 61
text
10 类型 1(CONNECT)
0D 剩余长度 = 13
00 04 协议名长度 = 4
4D 51 54 54 "MQTT"
04 协议版本 3.1.1
02 连接标志:清理会话
00 3C 保活时间 = 60 秒
00 01 客户端标识符长度 = 1
61 客户端标识符 "a"
其他常见报文:
text
订阅 test 主题(QoS 1):
82 09 00 01 00 04 74 65 73 74 01
代理回复订阅成功:
90 03 00 01 01
正常断开:
E0 00 (DISCONNECT)
看报文的通用三步:
text
第 1 步:看第 1 字节高 4 位 → 是什么报文
第 2 步:看剩余长度 → 后面多少内容
第 3 步:按报文类型各自的结构 → 逐字段拆
9. 与 Modbus 的区别和配合
| 项目 | Modbus | MQTT |
|---|---|---|
| 通信模式 | 主从问答 | 发布/订阅 |
| 主动上报 | 从站不能主动上报 | 设备可以随时上报 |
| 数据模型 | 寄存器、线圈 | 主题 + 消息内容 |
| 传输方式 | RS-485 串口 / 以太网 | 以太网 TCP |
| 额外校验 | RTU 带 CRC16 | 靠 TCP,无额外校验 |
| 最大连接数 | 一条总线设备有限 | 代理可挂成千上万设备 |
| 典型场景 | 现场设备采集 | 设备上云、远程监控 |
以储能系统为例,两个协议各管一段:
text
电表 / PCS / BMS <--Modbus RTU--> EMS 控制器 <--MQTT--> 云平台
text
EMS 用 Modbus 从现场设备读数据(一次一问一答)
EMS 把整理好的数据用 MQTT 发布到云平台(主动上报)
云平台用 MQTT 向 EMS 下发指令(远程调度)
学 Modbus 解决的是"怎么和设备说话",学 MQTT 解决的是"怎么把数据送上天"。
10. 端口与安全
常用端口:
text
1883 MQTT 明文
8883 MQTT over TLS(加密)
8083 MQTT over WebSocket(明文)
8084 MQTT over WebSocket(加密)
443 常见 WebSocket 转发端口
安全注意:
text
- 明文 1883 不要直接暴露到公网
- 公网连接优先用 8883 加密
- 设备接入一般要配用户名和密码
- 客户端标识符(Client ID)不能重复,重复会把对方踢下线
11. MQTT 5.0 简单了解
text
- 增加属性(Properties):可携带更多元数据
- 返回码更详细:能区分更多失败原因
- 主题别名:缩短报文长度
- 会话过期时间:替代原来的 Clean Session
- 兼容性:5.0 客户端可以和 3.1.1 代理通信(向下兼容)
目前大多数设备和云平台还是用 3.1.1,先掌握 3.1.1 就能覆盖大部分场景。
12. 实操:用 MQTTX 跑通
12.1 连接公共代理
不用自己搭服务器,用公共代理即可:
text
1. 下载安装 MQTTX(免费,https://mqttx.app)
2. 新建连接:
名称(Name):test
主机(Host):broker.emqx.io
端口(Port):1883
3. 点连接,右上角变绿 = 已连接
12.2 第一个发布/订阅
text
1. 左边「订阅」区:主题填 test/topic,点订阅
2. 右边「发布」区:主题填 test/topic,内容填 hello mqtt,点发送
3. 左边 test/topic 下面立刻收到消息 → 通了
注意:订阅时主题不能为空,否则提示 Topic is required。
12.3 实验 A:验证保留消息
text
1. 发布区勾选「保留(Retain)」,内容改 last status,发送
2. 断开连接,重新连接,重新订阅 test/topic
3. 一订阅就立刻收到 last status → 保留消息生效
12.4 实验 B:验证通配符
text
1. 新建订阅 test/#,发布到 test/a 和 test/b/c → 两条都收到
2. 新建订阅 test/+,同样发布 → 只收到 test/a,收不到 test/b/c
12.5 实验 C:验证 QoS
text
把发布区 QoS 分别改成 1、2 各发一条
(连公共代理时一般都能收到,重点是熟悉选项位置)
13. 学习总结
学习 MQTT 可以按这个顺序来:
text
1. 理解代理、发布、订阅
2. 掌握主题和通配符
3. 掌握 QoS 0/1/2
4. 理解保留消息和遗嘱消息
5. 学会拆报文(固定报头)
6. 用 MQTTX 发布/订阅
7. 结合项目:设备上报 + 云端下发
最重要的一句话:
text
MQTT 的核心就是:客户端把消息发布到某个主题,代理转发给所有订阅该主题的客户端。
真正上手时,要特别注意:
- 主题是否拼写一致(大小写敏感,拼错就收不到)
- QoS 是否选对(越高越可靠,但越慢、流量越大)
- 保留消息不清理会一直存在,可能误导新订阅者
- 遗嘱主题和内容要提前配好
- 心跳(Keep Alive)时间要合理,太短容易误判掉线,太长掉线发现慢
- 客户端标识符不能重复
- 明文 1883 别直接上公网,用 8883 加密
- 离线补发的消息不会"只留最新",要按顺序处理或应用层自己过滤
掌握这些之后,看设备的上报流程和调试 MQTT 通信就会清楚很多。