3. MQTT控制报文结构:固定头、可变头、有效载荷的格式与字段解析

好,咱们今天来啃一块硬骨头——MQTT的控制报文结构。说实话,我刚接触MQTT那会儿,看着协议规范里那一堆二进制位,头都大了。但后来我发现,只要你理解了它的设计思路,这东西其实特别精巧。

MQTT的报文,说白了就三部分:固定头可变头有效载荷。嗯,就像寄快递——快递单(固定头)是必须的,包装盒(可变头)看情况,里面的东西(有效载荷)才是核心。

3.1 固定头:每个报文都有的“身份证”

固定头是所有MQTT报文都必须携带的。它至少占1个字节,最多可以到5个字节。我习惯把它拆成两个字段来看:

Bit 7-4 3 2 1 0
Byte 1 报文类型 标志位(DUP/QoS/Retain)
Byte 2~ 剩余长度(可变长编码)

3.1.1 报文类型(4位)

这4个bit决定了这个报文是干嘛的。从0到15,一共16种,但实际只用了14种。我列个表给你看:

类型值 报文名称 方向 描述
0Reserved禁止保留,别用
1CONNECT客户端→服务端客户端请求连接
2CONNACK服务端→客户端连接确认
3PUBLISH双向发布消息
4PUBACK双向QoS 1 确认
5PUBREC双向QoS 2 第一步
6PUBREL双向QoS 2 第二步
7PUBCOMP双向QoS 2 完成
8SUBSCRIBE客户端→服务端订阅主题
9SUBACK服务端→客户端订阅确认
10UNSUBSCRIBE客户端→服务端取消订阅
11UNSUBACK服务端→客户端取消订阅确认
12PINGREQ客户端→服务端心跳请求
13PINGRESP服务端→客户端心跳响应
14DISCONNECT客户端→服务端断开连接
15Reserved禁止保留,别用
我的小技巧:抓包时看到报文类型值是0或15,基本可以断定是非法报文。我在调试一个国产模组时遇到过,对方发了个类型0的包,直接导致服务端断开连接。排查了半天才发现是固件bug。

3.1.2 标志位(4位)

这4个bit,不同报文类型含义不同。但有个通用规则:CONNECT、SUBSCRIBE、UNSUBSCRIBE、DISCONNECT 这四种报文的标志位必须为0,否则就是非法报文。

咱们重点说说PUBLISH报文的标志位:

  • DUP位(Bit 3):重复分发标志。值为1表示这条消息可能是重复的。我遇到过一种情况:客户端QoS 1发消息,没收到PUBACK就重发,DUP位就会置1。抓包时看到DUP=1,基本可以判断网络不太稳定。
  • QoS位(Bit 2-1):服务质量。00=QoS 0,01=QoS 1,10=QoS 2。11是保留值,别用。
  • Retain位(Bit 0):保留消息标志。值为1时,服务端会保存这条消息,新订阅者会立即收到。
注意:我曾经在项目中看到有人把QoS位设为11(即3),结果服务端直接断开连接。MQTT规范明确说了,QoS=3是非法值。别踩这个坑。

3.1.3 剩余长度:可变长编码的艺术

剩余长度表示可变头+有效载荷的总字节数。它用的是可变长编码,每个字节只用低7位表示数据,最高位是延续位。

举个例子,如果剩余长度是0,那编码就是 0x00。如果是127,编码是 0x7F。如果是128呢?

128 = 0x80
编码过程:
  128 % 128 = 0 → 低7位是0,延续位=1 → 0x80
  128 / 128 = 1 → 低7位是1,延续位=0 → 0x01
最终编码:0x80 0x01

嗯,说白了就是:每个字节的低7位存数据,最高位告诉解析器“后面还有没有字节”。这种设计的好处是,小数据包只占1个字节,大数据包最多占4个字节(最大268435455字节)。

实战经验:我在调试一个传感器网关时,发现它发的PUBLISH报文剩余长度总是算错。后来抓包一看,它把有效载荷的UTF-8字符串长度算成了字节数,但MQTT要求的是字节数。差了一个字节,整个报文就解析错了。所以,算剩余长度时,一定要用字节数,不是字符数

3.2 可变头:报文的“个性化信息”

可变头不是所有报文都有。比如PINGREQ就没有可变头,固定头后面直接就是有效载荷(其实PINGREQ连有效载荷都没有)。

常见的可变头字段有:

  • 协议名和协议级别:CONNECT报文特有。协议名是"MQTT"(4个字节),协议级别是0x04(MQTT 3.1.1)或0x05(MQTT 5.0)。
  • 连接标志:CONNECT报文特有。1个字节,包含用户名、密码、遗嘱、清理会话等标志位。
  • Keep Alive:CONNECT报文特有。2个字节,单位是秒。我一般设60秒,太短了浪费带宽,太长了服务端可能误判离线。
  • 连接返回码:CONNACK报文特有。1个字节,0表示成功,其他值表示各种错误。
  • 主题名:PUBLISH报文特有。UTF-8编码的字符串,前面2个字节是长度。
  • 报文标识符:用于QoS 1和QoS 2的报文。2个字节,从1开始递增。
我的习惯:抓包时,我第一眼先看报文标识符。如果发现同一个标识符重复出现,那基本可以断定是重传。这在网络不稳定的场景下特别常见。

3.3 有效载荷:真正的“数据内容”

有效载荷就是实际传输的数据。不同报文的有效载荷格式不同:

报文类型 有效载荷内容
CONNECT客户端ID(必选)、遗嘱主题、遗嘱消息、用户名、密码
PUBLISH应用消息(二进制数据)
SUBSCRIBE主题过滤器列表(每个过滤器后面跟一个QoS字节)
SUBACK返回码列表(每个订阅对应一个字节)
UNSUBSCRIBE主题过滤器列表
其他无有效载荷

这里有个细节:字符串都是UTF-8编码的,前面2个字节是长度(大端序)。我见过有人直接传GBK编码的字符串,结果服务端解析出来全是乱码。嗯,这个坑我踩过。

3.4 报文类型分类:从功能角度理解

我个人习惯把MQTT报文分成三类:

  1. 连接类:CONNECT、CONNACK、DISCONNECT。负责建立和断开连接。
  2. 发布/订阅类:PUBLISH、PUBACK、PUBREC、PUBREL、PUBCOMP、SUBSCRIBE、SUBACK、UNSUBSCRIBE、UNSUBACK。负责消息的发布和订阅。
  3. 心跳类:PINGREQ、PINGRESP。负责保活。

你想想看,MQTT总共就14种报文,每种报文都有固定的格式。只要掌握了固定头、可变头、有效载荷这三个层次,任何报文你都能手撕出来。

总结一下:
  • 固定头:所有报文都有,至少1字节,最多5字节
  • 可变头:部分报文有,包含协议名、标识符等
  • 有效载荷:部分报文有,包含实际数据
  • 剩余长度用可变长编码,小数据包省空间

好了,报文结构就讲到这里。下一章咱们会拿Wireshark实际抓包,一条一条地拆解这些报文。到时候你会发现,纸上谈兵和实战还是有差距的。嗯,我等着你。