4、固定报头详解:报文类型、标志位、剩余长度(Remaining Length)的编码与解码

MQTT 控制报文(Control Packet)的结构分为三部分:固定报头(Fixed Header)、可变报头(Variable Header) 和 有效载荷(Payload)。其中固定报头是所有报文类型共有的、必须存在的部分,它承载了报文类型、标志位和剩余长度三个关键信息。理解固定报头是解析 MQTT 数据流的起点,也是抓包分析、协议调试和客户端/服务端实现的基础。

4.1 固定报头的字节布局

固定报头至少包含 2 个字节,但剩余长度字段采用变长编码,因此固定报头总长度可能超过 2 字节。其结构如下:

Bit 76543210
Byte 1 报文类型(4 bit) 标志位 DUP/QoS/Retain(4 bit)
Byte 2… 剩余长度(Remaining Length)变长编码,1~4 字节

第一个字节的高 4 位表示报文类型,取值范围 1~14(0 和 15 为保留)。低 4 位是标志位,不同类型的报文对标志位有固定要求。从第二个字节开始是剩余长度,它表示当前报文后续所有字节的总数(即可变报头 + 有效载荷的长度),不包含固定报头自身。

报文类型速查表(部分常用)

  • 1:CONNECT——客户端请求连接
  • 2:CONNACK——连接确认
  • 3:PUBLISH——发布消息
  • 4:PUBACK——发布确认(QoS 1)
  • 8:SUBSCRIBE——订阅主题
  • 9:SUBACK——订阅确认
  • 12:PINGREQ——心跳请求
  • 13:PINGRESP——心跳响应
  • 14:DISCONNECT——断开连接

4.2 标志位(Flags)的规则

固定报头第一个字节的低 4 位并非所有报文都相同。MQTT 协议规定:除 PUBLISH 报文外,其余所有报文的标志位均为协议固定的值,如果收到不符合规定的标志位,接收方必须关闭网络连接(Malformed Packet 错误)。

PUBLISH 报文的标志位含义如下:

  • Bit 3(DUP):重复投递标志。值为 1 表示这是因未收到确认而重发的报文。
  • Bit 2~1(QoS):服务质量等级。00 表示 QoS 0(最多一次),01 表示 QoS 1(至少一次),10 表示 QoS 2(恰好一次)。11 为非法值。
  • Bit 0(RETAIN):保留标志。值为 1 表示服务端需保留该消息作为新订阅者的遗嘱消息。

其他常见报文的固定标志位值如下:

报文类型 固定标志位(二进制) 说明
CONNECT 0000 保留,不可修改
CONNACK 0000 保留
PUBACK 0000 保留
SUBSCRIBE 0010 QoS 位固定为 01
SUBACK 0000 保留
PINGREQ / PINGRESP 0000 保留
DISCONNECT 0000 保留

常见误区:初学者容易将 SUBSCRIBE 报文的标志位误写为 0000。实际上 SUBSCRIBE 的固定标志位必须是 0010(即 QoS 位为 01),否则协议解析会失败。同理,PUBLISH 的 QoS 位为 11 时属于协议违规,应断开连接。

4.3 剩余长度的变长编码原理

剩余长度(Remaining Length)采用 Base-128 变长编码,每个字节只用低 7 位存储数据,最高位(Bit 7)作为连续标志:

  • 若最高位为 1,表示后续还有字节继续表示长度;
  • 若最高位为 0,表示这是最后一个长度字节。

编码规则:将长度值按 128 进制拆分,从低位到高位依次放入每个字节的低 7 位。MQTT 协议规定剩余长度最多使用 4 个字节,因此可表示的最大值为 256 MB(即 268,435,455 字节)。

编码示例:

长度值 0        → 0x00
长度值 127      → 0x7F
长度值 128      → 0x80 0x01
长度值 16383    → 0xFF 0x7F
长度值 16384    → 0x80 0x80 0x01
长度值 2097151  → 0xFF 0xFF 0x7F
长度值 268435455→ 0xFF 0xFF 0xFF 0x7F

解码时,接收方从第一个长度字节开始,依次读取每个字节的低 7 位,乘以 128 的幂次后累加,直到遇到最高位为 0 的字节为止。

解码算法伪代码:

multiplier = 1
value = 0
loop:
    encodedByte = 读取下一个字节
    value += (encodedByte & 0x7F) * multiplier
    if (encodedByte & 0x80) == 0:
        break
    multiplier *= 128
    if multiplier > 128 * 128 * 128:
        报错:剩余长度超过 4 字节

4.4 完整报文解析案例

下面通过一个真实的 CONNECT 报文(十六进制)来演示固定报头的解析过程:

10 0E 00 04 4D 51 54 54 04 02 00 3C 00 01 61 62

字节 0:0x10 → 高 4 位 = 0001(CONNECT),低 4 位 = 0000(标志位正确)
字节 1:0x0E → 剩余长度 = 14(十进制),表示后续还有 14 个字节
字节 2~3:0x00 0x04 → 可变报头中的协议名长度 = 4
字节 4~7:0x4D 0x51 0x54 0x54 → "MQTT" 协议名
字节 8:0x04 → 协议级别 = MQTT 3.1.1
字节 9:0x02 → 连接标志(Clean Session = 1)
字节 10~11:0x00 0x3C → Keep Alive = 60 秒
字节 12~13:0x00 0x01 → 客户端标识符长度 = 1
字节 14:0x61 → 客户端 ID = "a"

验证:从字节 2 到字节 14 共 13 个字节,但剩余长度字段值为 14?这里需要仔细核对——实际上 0x0E = 14,而后续字节从索引 2 开始到索引 15(最后一个字节 0x62)才是完整的 14 字节。上面的示例中客户端 ID 应为 "ab"(0x61 0x62),因此总长度正确。

再来看一个 PUBLISH 报文的例子:

31 0C 00 05 74 6F 70 69 63 68 65 6C 6C 6F

字节 0:0x31 → 高 4 位 = 0011(PUBLISH),低 4 位 = 0001
       → DUP=0, QoS=00, RETAIN=1(保留消息)
字节 1:0x0C → 剩余长度 = 12
字节 2~3:0x00 0x05 → 主题名长度 = 5
字节 4~8:"topic" → 主题名
字节 9~13:"hello" → 消息内容(5 字节)

验证:主题名 5 字节 + 消息 5 字节 + 主题长度字段 2 字节 = 12 字节,与剩余长度一致。

4.5 剩余长度编码的边界与陷阱

在实际开发中,剩余长度的编解码是最容易出错的环节之一。以下是常见的误区和注意事项:

  • 误区一:将剩余长度当作普通整数直接发送。例如发送长度 200 时直接写入 0xC8,这是错误的。200 需要编码为 0x48 0x01(因为 200 = 128 + 72,低字节为 72|0x80 = 0xC8?不对,正确编码是 200 % 128 = 72,加上连续位 0x80 得 0xC8,然后 200 / 128 = 1,所以第二字节为 0x01,最终为 0xC8 0x01)。
  • 误区二:解码时忘记检查连续位。如果只读取一个字节就停止,遇到多字节长度时会导致报文解析错位。
  • 误区三:忽略最大长度限制。MQTT 3.1.1 规定剩余长度最多 4 字节,超过 268,435,455 字节的报文无法表示,需要分片或使用其他机制。
  • 误区四:在 SUBSCRIBE 报文中错误设置标志位。SUBSCRIBE 的固定标志位必须是 0x02(二进制 0010),如果写成 0x00 则协议违规。

调试技巧:使用 Wireshark 抓取 MQTT 报文时,展开固定报头即可看到协议自动解析出的报文类型、标志位和剩余长度。若发现剩余长度与实际内容不符,优先检查是否使用了正确的变长编码。

4.6 固定报头可视化解析

为了更直观地理解固定报头的字节布局和剩余长度的变长编码过程,下面给出一个综合示意图:

MQTT 固定报头结构解析 Byte 1 报文类型 + 标志位 高4位:类型 低4位:标志 Byte 2 ~ Byte 5 剩余长度(变长编码 1~4 字节) 后续字节 可变报头 + 有效载荷 剩余长度编码示例(十进制 → 十六进制字节流) 十进制长度 编码后字节(十六进制) 说明 0 0x00 单字节,最高位为 0 127 0x7F 单字节最大值 128 0x80 0x01 低7位=0,连续位=1;第二字节=1 16383 0xFF 0x7F 两字节最大值 解码流程(以 0x80 0x01 为例) 读取 0x80 低7位 = 0 连续位 = 1 → 继续 读取 0x01 低7位 = 1 连续位 = 0 → 结束 结果:value = 0×1 + 1×128 = 128 最大 4 字节可表示 268,435,455(256 MB)

上图左侧展示了固定报头在报文中的位置关系,右侧通过具体数值演示了剩余长度从十进制到十六进制字节流的编码过程,底部则展示了解码时如何根据连续位判断是否继续读取下一字节。掌握这一编解码规则后,无论是手动构造报文还是编写协议解析代码,都能准确处理任意长度的 MQTT 消息。