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种。我列个表给你看:
| 类型值 | 报文名称 | 方向 | 描述 |
|---|---|---|---|
| 0 | Reserved | 禁止 | 保留,别用 |
| 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 | 客户端→服务端 | 断开连接 |
| 15 | Reserved | 禁止 | 保留,别用 |
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时,服务端会保存这条消息,新订阅者会立即收到。
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字节)。
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报文分成三类:
- 连接类:CONNECT、CONNACK、DISCONNECT。负责建立和断开连接。
- 发布/订阅类:PUBLISH、PUBACK、PUBREC、PUBREL、PUBCOMP、SUBSCRIBE、SUBACK、UNSUBSCRIBE、UNSUBACK。负责消息的发布和订阅。
- 心跳类:PINGREQ、PINGRESP。负责保活。
你想想看,MQTT总共就14种报文,每种报文都有固定的格式。只要掌握了固定头、可变头、有效载荷这三个层次,任何报文你都能手撕出来。
- 固定头:所有报文都有,至少1字节,最多5字节
- 可变头:部分报文有,包含协议名、标识符等
- 有效载荷:部分报文有,包含实际数据
- 剩余长度用可变长编码,小数据包省空间
好了,报文结构就讲到这里。下一章咱们会拿Wireshark实际抓包,一条一条地拆解这些报文。到时候你会发现,纸上谈兵和实战还是有差距的。嗯,我等着你。