5月31日 00:26

MQTT 有哪些控制报文?连接、发布和订阅流程怎么串起来?

MQTT 控制报文不是一张要死背的清单,而是一套围绕连接、发布、订阅、心跳和断开的状态机。理解它们的最好方式,是先把流程跑通:客户端用 CONNECT 建立连接,Broker 用 CONNACK 回应;发布消息用 PUBLISH,不同 QoS 会带出 PUBACK、PUBREC、PUBREL、PUBCOMP;订阅主题用 SUBSCRIBE/SUBACK,取消订阅用 UNSUBSCRIBE/UNSUBACK;空闲时靠 PINGREQ/PINGRESP 保活,正常退出用 DISCONNECT。

报文类型怎么分组?

连接类只有 CONNECT 和 CONNACK。CONNECT 里会带 Client ID、用户名密码、Keep Alive、Clean Session 或 MQTT 5.0 的会话过期设置,还可以带遗嘱消息。CONNACK 告诉客户端是否连接成功,失败时会返回原因码或返回码。

发布类以 PUBLISH 为中心。QoS 0 只发 PUBLISH,不等确认;QoS 1 是 PUBLISH 加 PUBACK,保证至少一次;QoS 2 是 PUBLISH、PUBREC、PUBREL、PUBCOMP 四步,目标是恰好一次。这里最容易混淆的是 QoS 2 的“恰好一次”只针对 MQTT 投递流程,不代表业务处理绝对只发生一次。

订阅类包括 SUBSCRIBE、SUBACK、UNSUBSCRIBE、UNSUBACK。SUBSCRIBE 可以一次带多个 Topic Filter,例如 home/+/temperaturefactory/#。SUBACK 会逐个返回订阅结果,别只看报文到了没到,还要检查每个主题是否被授权。

心跳和断开类包括 PINGREQ、PINGRESP、DISCONNECT。Keep Alive 到期前客户端需要发心跳;如果 Broker 在 1.5 倍 Keep Alive 时间内没收到任何控制报文,通常会认为连接失效。正常 DISCONNECT 不会触发遗嘱消息,异常断线才会触发,这个细节在告警系统里很重要。

固定头部怎么看?

所有 MQTT 控制报文都有固定头部,第一字节高 4 位是报文类型,低 4 位是标志位,后面是 Remaining Length。PUBLISH 的 flags 会携带 DUP、QoS、Retain,PUBREL、SUBSCRIBE、UNSUBSCRIBE 的固定标志位也有固定要求,写客户端或排查抓包时不能乱填。

text
Byte 1: Message Type(4 bits) + Flags(4 bits) Byte 2+: Remaining Length(variable byte integer) Next: Variable Header + Payload
bash
# 用 mosquitto 快速观察订阅和发布流程 mosquitto_sub -d -h test.mosquitto.org -t 'demo/packet' mosquitto_pub -d -h test.mosquitto.org -t 'demo/packet' -q 1 -m 'hello'

一条消息会经过哪些报文?

以设备上报一条 QoS 1 温度消息为例,客户端先 CONNECT,Broker 返回 CONNACK 后才算连接建立。订阅端发送 SUBSCRIBE,Broker 用 SUBACK 确认订阅结果;发布端发送 PUBLISH,Broker 收到后返回 PUBACK,同时按主题匹配把消息转发给订阅端。连接空闲期间客户端继续用 PINGREQ 保活,Broker 回 PINGRESP;设备正常下线时发送 DISCONNECT,Broker 就不会发布遗嘱消息。

如果换成 QoS 2,发布链路会多出 PUBREC、PUBREL、PUBCOMP。这个流程看起来啰嗦,但它把“收到消息”和“释放消息”拆成两个阶段,避免网络抖动时双方状态不一致。代价也很明显:Broker 和客户端都要保存更多中间状态,吞吐下降,排查时也要关注 Packet Identifier 是否被复用或卡住。

MQTT 5.0 里的变化

MQTT 3.1.1 常说 14 种控制报文,MQTT 5.0 增加了 AUTH,并且给很多报文加了属性和原因码。AUTH 用于增强认证,适合需要多轮认证或重新认证的场景。原因码让 CONNACK、PUBACK、SUBACK、DISCONNECT 等报文能表达更细的失败原因,这对线上排障很有价值。

追问

QoS 0、1、2 分别会触发哪些确认报文?

QoS 0 没有确认报文,消息发出后协议层就不再追踪。QoS 1 需要 PUBACK,所以断线或超时后可能重发,接收方要能处理重复消息。QoS 2 要经过 PUBREC、PUBREL、PUBCOMP,流程最完整但延迟和状态存储也最多。取舍很直接:越可靠,报文越多,吞吐和实现复杂度越受影响。

CONNECT 里最容易配错哪些字段?

Client ID、Clean Session、Keep Alive 和遗嘱消息最容易出问题。多个设备复用同一个 Client ID 会互相踢下线,看起来像网络不稳定。Keep Alive 设置太短会制造无意义心跳,太长又会拖慢离线检测。遗嘱消息要配合异常断线理解,主动 DISCONNECT 不会触发它。

SUBACK 收到了就代表订阅成功吗?

不一定。SUBACK 只是 Broker 回应了订阅请求,真正要看每个返回码或原因码。某些主题可能因为 ACL 被拒绝,客户端如果只判断“收到 SUBACK”就会误以为订阅成功。项目里建议把订阅结果写入日志,并在关键主题失败时直接告警。边界是:协议完成和业务可用不是同一件事。

Remaining Length 为什么是可变长度编码?

MQTT 面向小设备和小消息,固定用 4 字节表示长度会浪费空间。可变长度编码让小报文只用 1 个字节表示长度,大报文再逐步扩展。踩坑点是它最多占 4 字节,而且每个字节只有 7 位表示数值,最高位表示是否还有后续字节。自己写解析器时如果没处理非法超长编码,可能被恶意报文拖住。

抓包时如何快速判断 MQTT 流程卡在哪里?

先看 CONNECT 后有没有 CONNACK,没有就查网络、TLS、认证和协议版本。发布卡住时看 QoS:QoS 1 缺 PUBACK,QoS 2 缺哪一步就查对应的会话状态。订阅收不到消息时别只盯 PUBLISH,要检查 SUBACK 返回码、Topic Filter 是否匹配、Retain 和 ACL。排查边界是先确认协议层报文完整,再看业务载荷是否符合预期。

标签:MQTT