4. MQTT 协议详解(一):MQTT 报文结构、固定头、可变头、有效载荷
MQTT 协议,说白了就是物联网设备之间的一种“轻量级对话协议”。它的报文结构非常紧凑,特别适合带宽有限、设备资源受限的场景。我在做智能抄表项目时,一个电表终端的内存只有几十 KB,跑 MQTT 刚刚好。
今天我们先拆解 MQTT 报文的基本骨架。你只要掌握了这个骨架,后面调试报文、分析抓包数据就会轻松很多。
4.1 MQTT 报文的三层结构
每一帧 MQTT 报文,都由三个部分组成:固定头(Fixed Header)、可变头(Variable Header)、有效载荷(Payload)。其中固定头是必须的,可变头和有效载荷则根据报文类型决定要不要带。
核心记忆点:固定头决定“这是什么报文”,可变头携带“报文的附加信息”,有效载荷才是“真正要传的数据”。
我画了一张结构图,你可以直观感受一下这三层的关系:
4.2 固定头(Fixed Header)—— 报文的“身份证”
固定头只有两个字节,但信息量很大。第一个字节是控制字段,第二个字节是剩余长度字段。
4.2.1 第一个字节:控制字段
这个字节被拆成了四个部分:
| Bit 7-4 | Bit 3 | Bit 2-1 |
|---|---|---|
| 报文类型(4 bit) | DUP 标志 | QoS 等级(2 bit) |
报文类型有 14 种,常用的就几个:CONNECT(1)、CONNACK(2)、PUBLISH(3)、SUBSCRIBE(8)、SUBACK(9)、PINGREQ(12)、PINGRESP(13)、DISCONNECT(14)。
DUP 标志表示这条报文是不是重发的。我在调试一个网关时,发现设备老是发重复的 PUBLISH 报文,一查 DUP 标志全是 1,原来是 QoS 1 的确认包丢了,设备在重传。
QoS 等级是 MQTT 的核心概念:
- QoS 0:最多发一次,丢了不补。适合传感器定时上报,丢了等下次。
- QoS 1:至少发一次,可能会重复。适合控制指令,但要做好去重。
- QoS 2:恰好发一次,最可靠但开销最大。适合计费、告警等关键数据。
我的经验:大部分物联网场景用 QoS 1 就够了。QoS 2 的握手流程多了一倍,设备功耗和网络延迟都会增加。除非是金融级数据,否则别轻易上 QoS 2。
4.2.2 第二个字节及后续:剩余长度
剩余长度表示“可变头 + 有效载荷”的总字节数。它采用了一种变长编码方式:每个字节只用低 7 位存数据,最高位是“是否还有后续字节”的标志。
举个例子,如果剩余长度是 200:
200 的二进制是 11001000
取低 7 位:1001000(0x48)
最高位置 1:11001000(0xC8)
所以第一个字节是 0xC8
200 减去 128 得 72,小于 128,直接放第二个字节:0x48
最终编码:0xC8 0x48
嗯,这里要注意:剩余长度最多可以占 4 个字节,最大能表示 268435455 字节。不过我在实际项目中,很少见到超过 1MB 的 MQTT 报文。
4.3 可变头(Variable Header)—— 报文的“附加信息”
可变头紧跟在固定头后面,内容取决于报文类型。我挑几个最常用的说说:
4.3.1 CONNECT 报文的可变头
CONNECT 报文的可变头包含一个协议名字段、协议级别、连接标志和保活时间。协议名固定是“MQTT”,协议级别对于 MQTT 3.1.1 是 4,对于 MQTT 5.0 是 5。
连接标志是一个字节,里面塞了 8 个开关:
- Bit 0:Clean Session(清除会话)
- Bit 1:Will Flag(遗嘱标志)
- Bit 2:Will QoS(遗嘱 QoS)
- Bit 3:Will Retain(遗嘱保留)
- Bit 4:Password Flag(密码标志)
- Bit 5:Username Flag(用户名标志)
- Bit 6-7:保留位
我曾经遇到一个设备死活连不上服务器,抓包一看,连接标志的 Clean Session 位被置成了 0,而服务器端要求必须为 1。这种问题不看报文根本想不到。
4.3.2 PUBLISH 报文的可变头
PUBLISH 报文的可变头包含主题名和报文标识符(Packet Identifier)。主题名是 UTF-8 编码的字符串,报文标识符只在 QoS 1 和 QoS 2 时才出现。
避坑指南:我曾经调试过一个设备,它发的 PUBLISH 报文没有报文标识符,但 QoS 设置的是 1。服务器直接断开连接了。记住:QoS > 0 时,报文标识符必须存在,且不能为 0。
4.4 有效载荷(Payload)—— 报文的“真正数据”
有效载荷就是实际要传输的内容。不同的报文类型,载荷格式不一样:
| 报文类型 | 有效载荷内容 |
|---|---|
| CONNECT | 客户端标识符、遗嘱主题、遗嘱消息、用户名、密码 |
| PUBLISH | 应用消息(二进制数据) |
| SUBSCRIBE | 主题过滤器列表 + 每个主题对应的 QoS |
| SUBACK | 返回码列表(每个主题一个返回码) |
| UNSUBSCRIBE | 主题过滤器列表 |
| UNSUBACK | 无有效载荷 |
| PINGREQ / PINGRESP | 无有效载荷 |
| DISCONNECT | 无有效载荷 |
你看,PINGREQ 和 PINGRESP 连可变头都没有,只有固定头。这就是 MQTT 轻量化的体现——心跳包只有 2 个字节。
4.5 报文结构实战:抓包分析
我建议你打开 Wireshark,过滤 mqtt,随便抓一个 CONNECT 报文看看。你会看到:
固定头:
0x10 (报文类型 CONNECT, DUP=0, QoS=0, Retain=0)
0x0E (剩余长度 = 14)
可变头:
0x00 0x04 'M' 'Q' 'T' 'T' (协议名长度 + 协议名)
0x04 (协议级别)
0x02 (连接标志:Clean Session=1)
0x00 0x3C (保活时间 60 秒)
有效载荷:
0x00 0x04 'c' 'l' 'i' '1' (客户端标识符长度 + 标识符)
嗯,看到没?整个报文只有 18 个字节。这就是为什么 MQTT 在窄带物联网中这么受欢迎。
总结一下:MQTT 报文的三层结构就像一封信——固定头是信封上的邮戳(类型和大小),可变头是收件人地址(主题、标识符),有效载荷才是信纸(真正的数据)。调试时,先看固定头确认报文类型,再看可变头确认上下文,最后解析有效载荷。这个顺序能帮你快速定位问题。