3、MQTT报文结构:固定头、可变头、有效载荷、14种报文类型简介
好,咱们今天聊聊MQTT报文到底长什么样。说实话,我刚接触MQTT那会儿,看着协议文档里那一堆字节描述,头都大了。后来在SCADA项目里抓包调试多了,才发现这东西其实特别规整。你只要搞懂了它的骨架,剩下的就是往里填肉。
MQTT的报文结构,说白了就三部分:固定头、可变头、有效载荷。其中固定头是每个报文都必须有的,可变头和有效载荷则看具体报文类型。我习惯把固定头比作快递单上的收件人信息——不管包裹里装什么,这行字必须写清楚。
3.1 固定头:所有报文的身份证
固定头至少2个字节。第一个字节分成两部分:高4位是报文类型,低4位是标志位。第二个字节是剩余长度,用来告诉接收方「后面还有多少数据要读」。
这里有个坑,我当年在调试一个PLC数据采集项目时踩过——剩余长度字段不是固定1个字节。它用了一种变长编码,最大可以扩展到4个字节。如果报文长度小于128字节,那1个字节就够了;超过128字节,最高位设为1,再用下一个字节继续表示。嗯,这个设计很巧妙,但第一次见确实容易懵。
固定头结构速记:
- 字节1(高4位):报文类型,取值1~14
- 字节1(低4位):标志位,具体含义取决于报文类型
- 字节2+:剩余长度,变长编码
3.2 可变头:报文的详细参数
可变头不是所有报文都有。它跟在固定头后面,内容取决于报文类型。最常见的可变头字段包括:
- 报文标识符(Packet Identifier):用于PUBLISH(QoS>0时)、PUBACK、PUBREC、PUBREL、PUBCOMP、SUBSCRIBE、SUBACK、UNSUBSCRIBE、UNSUBACK。说白了就是给报文编个号,方便确认谁是谁。
- 协议名和协议级别:只在CONNECT报文里出现,用来声明「我是MQTT,版本是3.1.1还是5.0」。
- 连接标志:也是CONNECT报文专属,告诉你要不要用户名、密码、遗嘱消息等。
- 保持连接时间:CONNECT报文里告诉服务器,如果多久没收到客户端消息就算掉线。
我记得有一次,现场工程师反馈说设备经常掉线重连。我抓包一看,发现是保持连接时间设得太短,只有5秒。而现场网络偶尔有3~4秒的抖动,结果服务器以为设备挂了,主动断开连接。后来改成30秒,问题解决。你想想看,一个字节的配置,能折腾你一整天。
3.3 有效载荷:真正干活的数据
有效载荷就是报文里装的实际内容。哪些报文有有效载荷?我列一下:
- CONNECT:客户端ID、遗嘱主题、遗嘱消息、用户名、密码
- PUBLISH:应用消息(就是你要传的那个温度值、开关状态啥的)
- SUBSCRIBE:主题过滤器列表 + QoS列表
- SUBACK:返回的QoS列表
- UNSUBSCRIBE:要取消订阅的主题列表
- PINGREQ / PINGRESP / DISCONNECT / AUTH:这些没有有效载荷
在SCADA系统里,有效载荷通常就是传感器数据。我建议你尽量把有效载荷设计得紧凑一些。比如温度值用两个字节的整数表示,别用字符串"25.3",那样太浪费带宽了。工业现场很多还是2G/3G网络,带宽金贵着呢。
3.4 14种报文类型一览
MQTT 3.1.1定义了14种报文类型,从1到14。我按功能分了个类,方便你记忆:
| 类型编号 | 报文名称 | 方向 | 说明 |
|---|---|---|---|
| 1 | CONNECT | 客户端→服务器 | 发起连接请求 |
| 2 | CONNACK | 服务器→客户端 | 确认连接结果 |
| 3 | PUBLISH | 双向 | 发布消息 |
| 4 | PUBACK | 双向 | QoS 1的确认 |
| 5 | PUBREC | 双向 | QoS 2第一步 |
| 6 | PUBREL | 双向 | QoS 2第二步 |
| 7 | PUBCOMP | 双向 | QoS 2完成 |
| 8 | SUBSCRIBE | 客户端→服务器 | 订阅主题 |
| 9 | SUBACK | 服务器→客户端 | 确认订阅 |
| 10 | UNSUBSCRIBE | 客户端→服务器 | 取消订阅 |
| 11 | UNSUBACK | 服务器→客户端 | 确认取消 |
| 12 | PINGREQ | 客户端→服务器 | 心跳请求 |
| 13 | PINGRESP | 服务器→客户端 | 心跳响应 |
| 14 | DISCONNECT | 客户端→服务器 | 断开连接 |
你看,其实真正频繁使用的就那几种:CONNECT/CONNACK(建连)、PUBLISH及其确认系列(传数据)、SUBSCRIBE/SUBACK(订阅)、PINGREQ/PINGRESP(保活)。在SCADA系统里,我见过有些设备只用了PUBLISH和PINGREQ两种报文,照样跑得好好的。
我的小建议:刚开始学MQTT报文时,别试图一下子记住所有14种。先搞懂CONNECT、PUBLISH、SUBSCRIBE、PINGREQ这四种,剩下的遇到再查。我在项目里就是这么干的,效率高很多。
3.5 一个完整的报文示例
光说不练假把式。咱们看一个实际的PUBLISH报文,假设要发布一个温度值25.3°C:
固定头:
字节0: 0x30 (0011 0000)
- 高4位: 0011 = 3 (PUBLISH)
- 低4位: 0000 (QoS 0, 不保留, 非重复)
字节1: 0x0C (剩余长度 = 12字节)
可变头:
字节2-3: 0x00 0x05 (主题长度 = 5)
字节4-8: "temp/1" (主题名)
有效载荷:
字节9-12: "25.3" (ASCII编码, 4字节)
你看,整个报文才13个字节。这就是MQTT轻量级的精髓——协议开销极小。相比HTTP那种动不动几百字节的头部,MQTT在工业现场的优势太明显了。
注意:上面这个例子用的是QoS 0,所以没有报文标识符。如果你用QoS 1或2,可变头里还要多出2个字节的报文标识符。我曾经在项目里因为忘记加这个字段,导致服务器一直不确认消息,设备端以为网络断了,疯狂重连。排查了半天才发现是代码里QoS写死了0,但服务器配置要求至少QoS 1。
好了,报文结构这块就聊到这儿。下一节咱们会深入讲讲连接和认证的细节,到时候你会看到CONNECT报文里那些标志位是怎么配合工作的。记住,搞懂固定头是基础,后面的东西都是在这个框架上搭积木。