4、QoS 2详解: exactly once送达、四步握手协议、报文类型(PUBREC/PUBREL/PUBCOMP)
好,咱们终于聊到QoS 2了。
说实话,QoS 2在MQTT里是最重量的一个等级。它承诺的是“exactly once”,也就是“恰好一次”。你想想看,在网络世界里,保证消息不丢、不重,这其实挺难的。我早年做工业物联网项目时,有个设备控制指令就必须要用QoS 2,因为一旦重复执行,生产线上的机械臂就可能多打一个孔,那损失就大了。
4.1 什么是“恰好一次”?
QoS 0是“发完就不管”,QoS 1是“至少一次”(可能会重复)。而QoS 2,它要确保接收方且仅收到一次。
说白了,就是发送方和接收方之间要达成一个“共识”:这条消息你收到了,而且你只处理一次。为了实现这个目标,MQTT设计了一套四步握手机制。嗯,这里要注意,这套机制虽然可靠,但代价就是延迟高、流量大。
核心要点:QoS 2 适用于那些“绝对不能重复、也绝对不能丢失”的场景。比如:
- 支付指令(扣款一次就是一次)
- 设备固件升级指令(重复刷写可能导致变砖)
- 关键状态机切换(比如从“运行”切换到“停止”)
4.2 四步握手协议:报文交换全流程
QoS 2的交互过程,我习惯把它叫做“四次握手”。它涉及三种报文类型:PUBREC、PUBREL、PUBCOMP。咱们一步步拆解。
- 第一步:发送方 → 接收方 (PUBLISH)
发送方先发一条普通的
PUBLISH报文,但QoS字段设为2。注意,这步和QoS 1一样,但后续流程完全不同。 - 第二步:接收方 → 发送方 (PUBREC)
接收方收到后,不会直接确认。它会先存下这条消息,然后回复一个
PUBREC报文。这个报文的意思是:“我收到你的消息了,但我还没处理完,你先别发新的。”我在项目中遇到过一个问题:如果接收方在发
PUBREC之前就挂了,那发送方会超时重发PUBLISH。这没问题,因为接收方恢复后,看到重复的PUBLISH,会再次回复PUBREC。但关键是,接收方不能重复处理消息。 - 第三步:发送方 → 接收方 (PUBREL)
发送方收到
PUBREC后,就知道接收方已经存下了。于是它发一个PUBREL报文,意思是:“好了,你可以正式处理这条消息了,我这边不会再重发了。”这里有个坑:
PUBREL报文本身也需要保证送达。所以PUBREL实际上使用的是QoS 1的逻辑(需要回复)。 - 第四步:接收方 → 发送方 (PUBCOMP)
接收方收到
PUBREL后,终于可以放心地处理消息了。处理完后,它回复一个PUBCOMP报文,告诉发送方:“搞定了,这条消息彻底结束了。”
你看,整个流程就是:PUBLISH → PUBREC → PUBREL → PUBCOMP。四步,一步都不能少。
我的个人习惯:在调试QoS 2问题时,我通常会抓包看这四步报文。如果发现PUBREC之后没有PUBREL,那多半是发送方挂了。如果PUBREL之后没有PUBCOMP,那可能是接收方处理逻辑卡住了。
4.3 报文类型详解:PUBREC / PUBREL / PUBCOMP
这三种报文是QoS 2独有的。咱们分别看看它们长什么样。
| 报文类型 | 固定报头 | 可变报头 | 作用 |
|---|---|---|---|
| PUBREC | 0x50 | 报文标识符 (Packet Identifier) | 接收方确认收到,但暂不处理 |
| PUBREL | 0x62 | 报文标识符 (Packet Identifier) | 发送方授权接收方处理 |
| PUBCOMP | 0x70 | 报文标识符 (Packet Identifier) | 接收方确认处理完成 |
注意看固定报头:PUBREC是0x50,PUBREL是0x62,PUBCOMP是0x70。这三个值在抓包时一眼就能认出来。
另外,PUBREL报文的固定报头里,第2位(也就是DUP标志位)必须为0。为什么?因为PUBREL本身的重传是靠QoS 1机制来保证的,不需要DUP位来标记。我曾经在这里犯过糊涂,后来看协议规范才明白。
4.4 报文标识符(Packet Identifier)的作用
你可能注意到了,上面表格里每个报文都带一个“报文标识符”。这个ID是发送方在发PUBLISH时分配的。后续的PUBREC、PUBREL、PUBCOMP都带着同一个ID。
为什么要这样?因为MQTT允许同时有多个QoS 2的消息在传输。如果没有这个ID,发送方收到一个PUBREC,都不知道是对应哪条消息的。
避坑指南:我曾经在一个项目中,客户端同时发送了100条QoS 2的消息。结果服务端处理不过来,导致报文标识符耗尽。MQTT协议规定,报文标识符的范围是1到65535。如果短时间内并发太多,就会用完。这时候客户端必须等待,直到有标识符被释放(收到PUBCOMP后才能复用)。
所以,如果你要大量使用QoS 2,一定要评估好并发量。我个人建议,非必要不用QoS 2,能用QoS 1解决的,就别上QoS 2。
4.5 重传与去重机制
QoS 2的可靠性,靠的就是重传和去重。
- 发送方重传:如果发送方没收到
PUBREC,它会重传PUBLISH。如果没收到PUBCOMP,它会重传PUBREL。 - 接收方去重:接收方收到重复的
PUBLISH时,它不会重复处理,而是直接回复PUBREC。收到重复的PUBREL时,它直接回复PUBCOMP。
说白了,接收方维护了一个“已处理消息ID”的列表。只要看到这个ID已经处理过,就直接跳过业务逻辑,只回复确认报文。
4.6 什么时候该用QoS 2?
说实话,我在实际项目中,QoS 2的使用率不到5%。大部分场景QoS 1就够了。但有些场景,你不得不用。
推荐使用QoS 2的场景:
- 金融交易:扣款、转账指令,重复一次就是事故。
- 设备控制:比如“开启阀门”、“关闭电源”,重复执行可能造成物理损坏。
- 状态同步:比如设备从“待机”切换到“运行”,如果重复切换,状态机就乱了。
不推荐使用QoS 2的场景:
- 传感器数据上报:温度、湿度这些,丢一两个没关系,用QoS 0就行。
- 日志传输:偶尔丢一条日志不影响大局,QoS 1足够。
- 心跳包:用QoS 0,丢了就丢了,下次心跳会补上。
嗯,最后总结一下。QoS 2是MQTT里最可靠的等级,但也是最慢、最耗资源的。它通过四步握手和三种专用报文,实现了“恰好一次”的语义。你在选型时,一定要权衡好可靠性和性能。我个人习惯是:能用QoS 1解决的,绝不上QoS 2。但一旦上了QoS 2,就要做好报文标识符管理和超时重传的预案。