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的交互过程,我习惯把它叫做“四次握手”。它涉及三种报文类型:PUBRECPUBRELPUBCOMP。咱们一步步拆解。

  1. 第一步:发送方 → 接收方 (PUBLISH)

    发送方先发一条普通的PUBLISH报文,但QoS字段设为2。注意,这步和QoS 1一样,但后续流程完全不同。

  2. 第二步:接收方 → 发送方 (PUBREC)

    接收方收到后,不会直接确认。它会先存下这条消息,然后回复一个PUBREC报文。这个报文的意思是:“我收到你的消息了,但我还没处理完,你先别发新的。”

    我在项目中遇到过一个问题:如果接收方在发PUBREC之前就挂了,那发送方会超时重发PUBLISH。这没问题,因为接收方恢复后,看到重复的PUBLISH,会再次回复PUBREC。但关键是,接收方不能重复处理消息。

  3. 第三步:发送方 → 接收方 (PUBREL)

    发送方收到PUBREC后,就知道接收方已经存下了。于是它发一个PUBREL报文,意思是:“好了,你可以正式处理这条消息了,我这边不会再重发了。”

    这里有个坑:PUBREL报文本身也需要保证送达。所以PUBREL实际上使用的是QoS 1的逻辑(需要回复)。

  4. 第四步:接收方 → 发送方 (PUBCOMP)

    接收方收到PUBREL后,终于可以放心地处理消息了。处理完后,它回复一个PUBCOMP报文,告诉发送方:“搞定了,这条消息彻底结束了。”

你看,整个流程就是:PUBLISHPUBRECPUBRELPUBCOMP。四步,一步都不能少。

我的个人习惯:在调试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时分配的。后续的PUBRECPUBRELPUBCOMP都带着同一个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,就要做好报文标识符管理和超时重传的预案。