【从零学习 MQTT】发布/订阅、主题、QoS 与报文解析

从零学习 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 通信就会清楚很多。

相关推荐
天国梦1 小时前
2026高中生英语词汇学习APP测评:天学网等4款产品实测对比
学习
我爱cope2 小时前
【计算机网络 | 物理层3:数字编码与调制:比特如何变成可传输的信号?】
网络·学习·计算机网络
一只小菜鸡..2 小时前
南京大学 操作系统 (JYY) 学习笔记:最终章——操作系统的宇宙,与我们的星辰大海
笔记·学习
0566465 小时前
Python高级——迭代器
开发语言·python·学习
math_hongfan5 小时前
鸿蒙Agent高级技能自定义开发:自定义意图服务/能力注册/Agent技能编排/服务插件开发实战
android·学习·华为·harmonyos·鸿蒙
duhong20266 小时前
Linux刷题记录第一套
学习
AI 小老六6 小时前
Agent Runtime 如何用 Session、Memory、User Profile 和 Skill 实现外部学习
服务器·人工智能·学习·ai·架构·自动化
又折桃枝换酒钱6 小时前
CycleChart:一个统一的基于一致性学习的双向图表理解与生成框架(翻译与解读)
人工智能·深度学习·学习