3. MQTT控制报文详解(上):CONNECT、CONNACK、PUBLISH、SUBSCRIBE、SUBACK
各位同学,今天我们正式进入MQTT协议的核心地带——控制报文。说白了,MQTT设备之间的所有沟通,都是靠这些报文来完成的。我刚开始接触MQTT时,总觉得报文结构很抽象,后来在项目中亲手抓包分析了几次,才真正理解每个字节的意义。今天咱们就把最常用的五个报文掰开揉碎了讲清楚。
核心要点:MQTT控制报文由固定报头(Fixed Header)+ 可变报头(Variable Header)+ 有效载荷(Payload)三部分组成。这是所有报文的基础骨架。
3.1 CONNECT报文——设备的第一声招呼
当一个MQTT客户端想要连接服务器时,它必须发送CONNECT报文。这是整个会话的起点。我记得第一次调试MQTT设备时,客户端怎么都连不上服务器,抓包一看,原来是CONNECT报文里的协议版本号写错了。嗯,这种低级错误,谁还没犯过呢?
固定报头
CONNECT报文的固定报头很简单:
- 报文类型:0x10(二进制0001 0000),高4位表示CONNECT,低4位保留为0
- 剩余长度:可变报头 + 有效载荷的总字节数
可变报头
这里包含四个关键字段:
| 字段 | 长度 | 说明 |
|---|---|---|
| 协议名 | 4字节 | 固定为"MQTT"(0x4D 0x51 0x54 0x54) |
| 协议级别 | 1字节 | MQTT 3.1.1为0x04,MQTT 5.0为0x05 |
| 连接标志 | 1字节 | 包含清洁会话、遗嘱、用户名/密码等标志位 |
| 保持连接 | 2字节 | 单位秒,建议值60-300秒 |
我的经验:保持连接(Keep Alive)这个值别设太大。我曾经在项目中设了3600秒(1小时),结果设备断网后服务器要等1小时才能发现。后来统一改成60秒,配合遗嘱消息,断连检测快多了。
有效载荷
这部分包含客户端标识符、遗嘱主题、遗嘱消息、用户名和密码。其中只有客户端标识符是必须的,其他都是可选的。
// CONNECT报文示例(十六进制)
10 1E 00 04 4D 51 54 54 04 02 00 3C 00 0C 6D 79
63 6C 69 65 6E 74 5F 69 64 5F 30 30 31
// 解读:
// 0x10 - 固定报头,CONNECT
// 0x1E - 剩余长度30字节
// 00 04 4D 51 54 54 - 协议名"MQTT"
// 0x04 - 协议级别3.1.1
// 0x02 - 连接标志(清洁会话=1)
// 00 3C - 保持连接60秒
// 00 0C - 客户端ID长度12
// 6D 79... - "myclient_id_001"
3.2 CONNACK报文——服务器的回应
服务器收到CONNECT后,会回复CONNACK报文。说白了,就是告诉客户端:「行,你连上了」或者「不行,你哪儿错了」。
CONNACK报文结构更简单:
- 固定报头:0x20
- 可变报头:2字节
- 第1字节:连接确认标志(当前协议版本中保留为0)
- 第2字节:连接返回码
- 无有效载荷
| 返回码 | 含义 | 常见原因 |
|---|---|---|
| 0x00 | 连接已接受 | 一切正常 |
| 0x01 | 协议版本错误 | 客户端和服务器MQTT版本不匹配 |
| 0x02 | 标识符被拒绝 | 客户端ID无效或已被占用 |
| 0x03 | 服务器不可用 | 服务器过载或维护中 |
| 0x04 | 用户名或密码错误 | 认证失败 |
| 0x05 | 未授权 | 没有访问权限 |
避坑指南:我曾经遇到一个设备,每次连接都返回0x04(用户名密码错误)。查了半天,发现是设备固件里把密码字段的编码搞错了——服务器要求UTF-8,设备发的是ASCII。这种编码问题,抓包一看就明白了。
3.3 PUBLISH报文——消息的载体
PUBLISH报文是MQTT中最常用的报文。客户端发布消息、服务器转发消息,都用它。你想想看,一个物联网系统里,99%的流量都是PUBLISH报文。
固定报头
PUBLISH的固定报头比较特殊,低4位有实际含义:
- 报文类型:0x30(二进制0011 0000)
- DUP标志:重复分发标志,QoS > 0时使用
- QoS等级:0、1、2
- RETAIN标志:保留消息标志
可变报头
包含主题名和报文标识符:
- 主题名:UTF-8编码字符串,不能包含通配符
- 报文标识符:仅QoS 1和QoS 2需要,QoS 0没有此字段
有效载荷
就是实际要发送的应用数据。可以是任何二进制数据,长度由剩余长度决定。
// PUBLISH报文示例(QoS 0,主题"temp/sensor1")
30 0D 00 0C 74 65 6D 70 2F 73 65 6E 73 6F 72 31
32 2E 35
// 解读:
// 0x30 - PUBLISH,QoS=0,DUP=0,RETAIN=0
// 0x0D - 剩余长度13字节
// 00 0C - 主题名长度12
// 74 65... - "temp/sensor1"
// 32 2E 35 - 有效载荷"25.5"
我的习惯:QoS 0的PUBLISH报文没有报文标识符,所以服务器无法确认是否收到。我在做传感器数据上报时,对于温度、湿度这种允许丢失的数据,就用QoS 0。但控制指令(比如开关灯)必须用QoS 1,确保执行。
3.4 SUBSCRIBE报文——订阅感兴趣的主题
客户端想要接收某个主题的消息,就得发送SUBSCRIBE报文。这里有个关键点:SUBSCRIBE报文只能由客户端发送,服务器不能主动订阅。
固定报头
报文类型为0x82(二进制1000 0010)。注意,SUBSCRIBE的固定报头低4位固定为0010,这是协议规定的。
可变报头
只有一个字段:报文标识符(2字节)。客户端需要自己管理这个标识符,每次订阅递增。
有效载荷
包含一个或多个主题过滤器及其对应的QoS等级:
- 每个主题过滤器以UTF-8字符串形式存在
- 后面紧跟1字节的QoS等级(0x00、0x01、0x02)
// SUBSCRIBE报文示例(订阅两个主题)
82 0E 00 01 00 0A 68 6F 6D 65 2F 2B 2F 74 65 6D 70 00 01
// 解读:
// 0x82 - SUBSCRIBE
// 0x0E - 剩余长度14字节
// 00 01 - 报文标识符=1
// 00 0A - 第一个主题过滤器长度10
// 68 6F... - "home/+/temp"(+是单级通配符)
// 00 01 - QoS=1
注意:SUBSCRIBE报文的有效载荷中,主题过滤器可以包含通配符。但PUBLISH报文中的主题名不能包含通配符。这个区别很多新手会搞混。我见过有人试图向"home/+/temp"发布消息,结果服务器直接拒绝。
3.5 SUBACK报文——订阅确认
服务器收到SUBSCRIBE后,必须回复SUBACK报文。这个报文告诉客户端:你订阅的每个主题,我都处理好了,结果如下。
结构
- 固定报头:0x90
- 可变报头:报文标识符(与对应的SUBSCRIBE报文相同)
- 有效载荷:返回码列表,每个订阅主题对应一个返回码
| 返回码 | 含义 |
|---|---|
| 0x00 | 订阅成功,最大QoS 0 |
| 0x01 | 订阅成功,最大QoS 1 |
| 0x02 | 订阅成功,最大QoS 2 |
| 0x80 | 订阅失败 |
// SUBACK报文示例(对应上面的SUBSCRIBE)
90 03 00 01 01
// 解读:
// 0x90 - SUBACK
// 0x03 - 剩余长度3字节
// 00 01 - 报文标识符=1(与SUBSCRIBE对应)
// 01 - 返回码:订阅成功,最大QoS 1
关键点:SUBACK的返回码表示服务器允许的最大QoS等级,不一定等于客户端请求的QoS。比如客户端请求QoS 2,但服务器只支持QoS 1,返回码就是0x01。客户端收到后,应该按返回码的QoS等级来接收消息。
好了,这五个报文是MQTT通信的基础。我个人觉得,理解了它们,MQTT协议就算入门了。下次咱们再聊剩下的报文——UNSUBSCRIBE、PINGREQ、DISCONNECT这些。嗯,今天就到这儿,大家回去可以抓包看看实际报文长什么样。