3. MQTT控制报文详解(上):CONNECT、CONNACK、PUBLISH、SUBSCRIBE、SUBACK

各位同学,今天我们正式进入MQTT协议的核心地带——控制报文。说白了,MQTT设备之间的所有沟通,都是靠这些报文来完成的。我刚开始接触MQTT时,总觉得报文结构很抽象,后来在项目中亲手抓包分析了几次,才真正理解每个字节的意义。今天咱们就把最常用的五个报文掰开揉碎了讲清楚。

核心要点:MQTT控制报文由固定报头(Fixed Header)+ 可变报头(Variable Header)+ 有效载荷(Payload)三部分组成。这是所有报文的基础骨架。

MQTT控制报文 CONNECT / CONNACK PUBLISH SUBSCRIBE / SUBACK CONNECT 客户端ID 遗嘱消息 用户名/密码 保持连接 PUBLISH 主题名 报文标识符 QoS等级 应用消息 SUBSCRIBE 报文标识符 主题过滤器 QoS等级 SUBACK返回码 固定报头 + 可变报头 + 有效载荷

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这些。嗯,今天就到这儿,大家回去可以抓包看看实际报文长什么样。