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报文里那些标志位是怎么配合工作的。记住,搞懂固定头是基础,后面的东西都是在这个框架上搭积木。