本节摘要:逐字节解剖 MQTT 控制报文——固定头里的报文类型、DUP/QoS/RETAIN 位与剩余长度编码,再讲主题命名与通配符如何组织订阅树。承接 3.1 的发布订阅模型,通往 3.3 的 QoS 状态流转。
模型讲完了,得能看懂线上的字节才行。MQTT 说到底是一套排布好的字节流,你在抓包工具里看到的一长串十六进制,其实都能按固定头、可变头、载荷三段拆开。
每条 MQTT 控制报文都由最多三段组成:
固定头第一字节是高 8 位的信息门面,按位拆开含义如下:
位 7 6 5 4 | 3 | 2 1 | 0 | 报文类型 | DUP | QoS | RETAIN 第1字节 4 bits 1 bit 2 bits 1 bit → 控制报文类型与标志 第2字节 剩余长度(1-4字节 变长编码,每字节只用低7位,最高位为续续位)
下面这张图把一条 PUBLISH 报文在字节上的排布画成更直观的字段分布,方便你对照抓包:

下表把 MQTT 3.1.1 的报文列全,标注方向与用途,方便你在抓包时查表:
| 类型值 | 报文名 | 方向 | 作用 |
|---|---|---|---|
| 1 | CONNECT | 客户端→代理 | 建立连接,带协议与会话参数 |
| 2 | CONNACK | 代理→客户端 | 连接确认,含返回码与会话标志 |
| 3 | PUBLISH | 双向 | 投递应用消息(核心) |
| 4 | PUBACK | 双向 | QoS1 确认 |
| 5 | PUBREC | 双向 | QoS2 第一步确认 |
| 6 | PUBREL | 双向 | QoS2 释放 |
| 7 | PUBCOMP | 双向 | QoS2 完成 |
| 8 | SUBSCRIBE | 客户端→代理 | 订阅主题 |
| 9 | SUBACK | 代理→客户端 | 订阅确认 |
| 10 | UNSUBSCRIBE | 客户端→代理 | 退订 |
| 11 | UNSUBACK | 代理→客户端 | 退订确认 |
| 12 | PINGREQ | 客户端→代理 | 心跳探测 |
| 13 | PINGRESP | 代理→客户端 | 心跳应答 |
| 14 | DISCONNECT | 客户端→代理 | 客户端主动断开 |
MQTT 用变长编码表示固定头之后的长度,规则是每字节取低 7 位,最高位为续续位(1 表示后面还有字节)。例如长度 321:321 换算后写进两字节——0b10100001 0b00000010(先低后高)。这个设计让超短消息只花 1 字节就能描述长度,不必为长消息预留宽位。
# 剩余长度变长编码示意 value = 321 byte0 = value & 0x7F # 低7位: 0x41 byte1 = (value >> 7) & 0x7F # 高段: 0x02 on_wire = [ 0xC1, 0x02 ] # byte0 置最高位=1 表示还有下一字节 # 解码(逆序): 0x02<<7 | 0x41 = 256+65 = 321
主题是个 UTF-8 字符串,按 / 分多级,例如 sensor/room_a/temperature。它由订阅者与发布者共同约定,Broker 按级做路由。为了灵活订阅,MQTT 提供两个通配符:
+:占位单一级,如 sensor/+/temperature 匹配 room_a、room_b 但不跨级。#:匹配后面所有层,如 sensor/# 匹配整棵子树,且常见于"全收"的看板。下面用一个主题树示意 + 与 # 的差别:
⚠️ 常见坑:主题不是目录,
#只能放在最后一层,+只能占一整层,不能写成sensor/#/temp或sensor/+a——Broker 会按规范校验,格式错直接断连。
PUBLISH 载荷可以是任意二进制数据(文本、JSON、Protobuf 都行),但负载语义由上层约定。实际工程常配一个统一 schema,例如温度消息固定 {"device":"x","metric":"temp","value":25.6,"unit":"celsius"},接收方按图索骥去解析。约定好格式,Broker 只负责"传得对",不关心"内容啥意思"。
/ 分级,+ 匹配单层,# 匹配多层且必须在末级。固定头里那块"QoS 位"是理解可靠性的钥匙——下一节就拆 QoS 0/1/2 到底怎么一路握手保证不丢不重。