3. MQTT 报文结构:固定报头、可变报头、有效载荷、CONNECT/CONNACK 报文详解
MQTT 协议的核心在于其紧凑、高效的二进制报文结构。理解报文结构是进行数据解析、协议调试和边缘计算设备间通信的基础。一个完整的 MQTT 控制报文由三个部分组成:固定报头(Fixed Header)、可变报头(Variable Header)和有效载荷(Payload)。其中,固定报头是所有报文类型都必须包含的,而可变报头和有效载荷则根据报文类型的不同而存在差异。
3.1 固定报头(Fixed Header)
固定报头是每个 MQTT 报文的第一部分,长度至少为 2 字节。它定义了报文的基本控制信息和剩余长度。
| 字节位置 | Bit 7-4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
|---|---|---|---|---|---|
| 第 1 字节 | 报文类型(MESSAGE TYPE) | 控制标志位(DUP, QoS, RETAIN) | |||
| 第 2 字节+ | 剩余长度(Remaining Length)—— 变长编码 | ||||
- 报文类型(4 bits):用于标识报文的类型,例如 CONNECT (1)、CONNACK (2)、PUBLISH (3)、SUBSCRIBE (8) 等。
- 控制标志位(4 bits):根据报文类型不同,含义不同。例如,PUBLISH 报文中包含 DUP(重发标志)、QoS(服务质量等级)和 RETAIN(保留标志)。
- 剩余长度(Remaining Length):表示当前报文之后剩余部分的字节数(即可变报头 + 有效载荷的长度)。它采用变长编码方式,最多占用 4 个字节,每个字节的最高位(Bit 7)作为“是否还有后续字节”的指示位。
解析要点: 在边缘计算设备中解析 MQTT 报文时,首先需要读取第 1 字节的高 4 位以确定报文类型,然后解析剩余长度字段。由于剩余长度是变长编码,解析时需循环读取字节,直到当前字节的 Bit 7 为 0。
3.2 可变报头(Variable Header)
可变报头位于固定报头和有效载荷之间,其内容取决于报文类型。常见的可变报头字段包括:
- 协议名称(Protocol Name):在 CONNECT 报文中,固定为 “MQTT”。
- 协议级别(Protocol Level):MQTT 3.1.1 版本为 4,MQTT 5.0 版本为 5。
- 连接标志(Connect Flags):包含用户名、密码、遗嘱消息、清除会话等标志位。
- 保持连接(Keep Alive):以秒为单位,表示客户端发送报文的最大时间间隔。
- 报文标识符(Packet Identifier):用于 QoS 1 和 QoS 2 的报文确认流程,如 PUBLISH、PUBACK、SUBSCRIBE、SUBACK 等。
3.3 有效载荷(Payload)
有效载荷是报文的实际数据部分,其内容同样取决于报文类型。例如:
- CONNECT 报文:包含客户端标识符(Client ID)、遗嘱主题、遗嘱消息、用户名和密码。
- PUBLISH 报文:包含应用消息(即传感器数据、控制指令等)。
- SUBSCRIBE 报文:包含用户订阅的主题过滤器列表及其对应的 QoS 等级。
3.4 CONNECT 报文详解
CONNECT 报文是客户端与服务器建立连接时发送的第一个报文。其结构如下:
| 组成部分 | 字段 | 长度(字节) | 说明 |
|---|---|---|---|
| 固定报头 | 报文类型 + 标志位 | 1 | 0x10(CONNECT 报文,标志位固定为 0) |
| 剩余长度 | 1~4 | 可变报头 + 有效载荷的总长度 | |
| 可变报头 | 协议名称长度 | 2 | 固定为 0x00 0x04 |
| 协议名称 | 4 | “MQTT” 的 ASCII 码 | |
| 协议级别 | 1 | MQTT 3.1.1 为 0x04 | |
| 连接标志 | 1 | 包含 Clean Session、Will、Username、Password 等标志 | |
| 保持连接 | 2 | 单位:秒,例如 60 秒 | |
| 有效载荷 | 客户端标识符长度 | 2 | Client ID 的字节长度 |
| 客户端标识符 | 可变 | 唯一标识客户端 | |
| 遗嘱主题/消息(可选) | 可变 | 仅在连接标志中 Will 标志为 1 时存在 | |
| 用户名/密码(可选) | 可变 | 仅在连接标志中相应标志为 1 时存在 |
实战案例: 在边缘网关中,设备上电后首先构造 CONNECT 报文。假设客户端 ID 为 “edge_gw_01”,保持连接为 60 秒,无遗嘱、无用户名密码。则报文固定报头为
0x10 0x0E,可变报头为 0x00 0x04 0x4D 0x51 0x54 0x54 0x04 0x02 0x00 0x3C,有效载荷为 0x00 0x0A 0x65 0x64 0x67 0x65 0x5F 0x67 0x77 0x5F 0x30 0x31。
3.5 CONNACK 报文详解
CONNACK 报文是服务器对 CONNECT 报文的响应,用于告知客户端连接是否成功建立。其结构相对简单:
| 组成部分 | 字段 | 长度(字节) | 说明 |
|---|---|---|---|
| 固定报头 | 报文类型 + 标志位 | 1 | 0x20(CONNACK 报文,标志位固定为 0) |
| 可变报头 | 连接确认标志 + 连接返回码 | 2 | 共 2 字节 |
| 有效载荷 | 无 | 0 | CONNACK 报文不包含有效载荷 |
可变报头的 2 个字节含义如下:
- 第 1 字节(连接确认标志):当前仅使用最低位(Bit 0),即 Session Present 标志。如果服务器有之前存储的会话状态,则此位为 1。
- 第 2 字节(连接返回码):表示连接结果。常见返回码如下:
| 返回码(十进制) | 含义 |
|---|---|
| 0 | 连接已接受(Connection Accepted) |
| 1 | 连接被拒绝,不支持的协议版本 |
| 2 | 连接被拒绝,客户端标识符被拒绝 |
| 3 | 连接被拒绝,服务器不可用 |
| 4 | 连接被拒绝,用户名或密码错误 |
| 5 | 连接被拒绝,未授权 |
常见错误: 在边缘计算场景下,如果设备发送 CONNECT 报文后收到返回码为 1(不支持的协议版本),通常是因为设备端 MQTT 库版本与 Broker 不匹配。请检查协议级别字段是否为 0x04(MQTT 3.1.1)。
3.6 报文解析实战要点
在边缘计算环境中,由于设备资源受限,解析 MQTT 报文时需要特别注意以下几点:
- 逐字节解析:使用 C 或 Python 等语言时,应逐字节读取并解析固定报头,避免一次性读取大块内存。
- 变长编码处理:实现一个循环函数来解析剩余长度,注意处理多字节情况,防止死循环。
- 校验连接标志:在解析 CONNECT 报文的有效载荷时,必须根据连接标志中的位来判断后续字段是否存在,避免解析错位。
- 超时处理:发送 CONNECT 报文后,应设置超时定时器,如果在规定时间内未收到 CONNACK,则视为连接失败,进行重试或告警。
推荐实践: 在边缘网关的固件中,建议将 MQTT 报文解析封装成独立模块,并提供回调函数接口。这样当收到不同类型的报文时,主程序只需调用解析函数,即可自动分发到对应的处理逻辑,提高代码的可维护性和可移植性。