4. MQTT 协议详解(一):MQTT 报文结构、固定头、可变头、有效载荷

MQTT 协议,说白了就是物联网设备之间的一种“轻量级对话协议”。它的报文结构非常紧凑,特别适合带宽有限、设备资源受限的场景。我在做智能抄表项目时,一个电表终端的内存只有几十 KB,跑 MQTT 刚刚好。

今天我们先拆解 MQTT 报文的基本骨架。你只要掌握了这个骨架,后面调试报文、分析抓包数据就会轻松很多。

4.1 MQTT 报文的三层结构

每一帧 MQTT 报文,都由三个部分组成:固定头(Fixed Header)、可变头(Variable Header)、有效载荷(Payload)。其中固定头是必须的,可变头和有效载荷则根据报文类型决定要不要带。

核心记忆点:固定头决定“这是什么报文”,可变头携带“报文的附加信息”,有效载荷才是“真正要传的数据”。

我画了一张结构图,你可以直观感受一下这三层的关系:

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 报文的三层结构就像一封信——固定头是信封上的邮戳(类型和大小),可变头是收件人地址(主题、标识符),有效载荷才是信纸(真正的数据)。调试时,先看固定头确认报文类型,再看可变头确认上下文,最后解析有效载荷。这个顺序能帮你快速定位问题。