MQTT Quality of Service(QoS,服务质量)是消息发送者和接收者之间的协议,用于定义消息的传递保证级别。

QoS具有3个级别,分别是:

  1. 最多一次(QoS 0)
  2. 至少一次(QoS 1)
  3. 确定一次(QoS 2)

其中涉及的MQTT报文有四种,分别是PUBLISH、PUBACK、PUBREC以及PUBCOMP。

消息传递原理

MQTT消息从发布者到订阅者中间有两个过程,一是发布者将消息发送给MQTT代理,二是MQTT代理将消息传递给订阅者

具体过程如下图所示:

sequenceDiagram participant 发布者 participant Broker participant 订阅者 autonumber 订阅者 ->> Broker: SUBSCRIBE(Topic, QoS) Broker ->> 订阅者: SUBACK(PacketID, ReturnCode) 发布者 ->> Broker: PUBLISH(Payload, Topic, QoS) Broker ->> 订阅者: PUBLISH(Payload, Topic, QoS)

发布者在发送消息给MQTT代理时会指定消息主题以及QoS级别。

订阅者在订阅主题时也会指定QoS级别。

如果订阅者订阅主题时指定的QoS级别小于发布者发布消息时指定的QoS级别,那么MQTT代理在传递消息给订阅者时会使用订阅者订阅主题时指定的QoS级别

如果发布者发布消息时指定的QoS级别小于订阅者订阅主题时指定的QoS级别,那么MQTT代理在传递消息给订阅者时会使用发布者发布消息时指定的QoS级别

一句话总结,MQTT代理传递消息给订阅者的QoS级别取决于订阅者订阅主题时指定的QoS级别与发布者发布消息时指定的QoS级别,从其中取较小值

QoS的工作方式

服务质量(QoS)在MQTT中扮演者重要角色,其主要作用就是能够让客户端根据网络可靠性和应用程序要求来选择具体的QoS级别。

下面是3种不同QoS级别的工作原理

QoS0

MQTT QoS的最低级别,表示消息最多发送一次

MQTT代理在接收到该QoS级别的消息之后不会对其进行确认,发布者也不会多次发布。

在MQTT报文交互过程中仅有一个PUBLISH报文。

sequenceDiagram participant Sender participant Receiver autonumber Sender ->> Receiver: PUBLISH(Payload, Topic, QoS=0)

QoS1

MQTT QoS1级别,表示消息至少发送一次

当发送者使用QoS1发布消息时,发送者会保留消息副本,直到接收到来自接收者返回的PUBACK报文,以确认接收者成功接收为止,否则会不断重复发布消息

当接受者收到消息后可以立即进行处理,接收者可以是订阅者或者MQTT代理

如果接收者为订阅者则需要向MQTT代理返回PUBACK消息,如果接受者为MQTT代理则需要将消息传递给已订阅对应主题的订阅者,并返回PUBACK给发送者。

在这个过程中,会在发送者与接收者之间交互两个报文,分别是PUBLISH与PUBACK。

sequenceDiagram participant Sender participant Receiver autonumber Sender ->> Sender: Store PUBLISH loop Send Intervall Sender ->> Receiver: PUBLISH(Payload, DUP, PacketId=100, QoS=1) end Receiver ->> Sender: PUBACK(PacketId=100) Sender ->> Sender: Delete PUBLISH Sender ->> Sender: Release PacketId

PUBACK报文内容如下图:

消息重复原因

一般有两种情况导致发送者收不到PUBACK报文:

  1. PUBLISH报文没有到达接收者。
  2. PUBLISH报文到达接收者,接受者的PUBACK报文未到达发送者。

对于第1种情况,虽然发送者重传了PUBLISH报文(重传时会设置DUP=1),但实际上接收者仅接收到一次,不存在消息重复

sequenceDiagram participant Sender participant Receiver autonumber Sender ->> Sender: Store PUBLISH Sender ->> Receiver: PUBLISH(Payload, PacketId=100, DUP=0, QoS=1) note right of Sender: Transport failure Sender ->>+ Receiver: PUBLISH(Payload, PacketId=100, DUP=1, QoS=1) Receiver ->>- Sender: PUBACK(PacketId=100) Sender ->> Sender: Delete PUBLISH Sender ->> Sender: Release PacketIds

对于第2种情况,在发送者重传PUBLISH报文(重传时会设置DUP=1)时,接收者已经接收到PUBLISH报文了,这就会导致消息重复

sequenceDiagram participant Sender participant Receiver autonumber Sender ->> Sender: Store PUBLISH Sender ->> Receiver: PUBLISH(Payload, PacketId=100, DUP=0, QoS=1) Receiver ->> Sender: PUBACK(PacketId=100) note left of Receiver: Transport failure Sender ->>+ Receiver: PUBLISH(Payload, PacketId=100, DUP=1, QoS=1) Receiver ->>- Sender: PUBACK(PacketId=100) Sender ->> Sender: Delete PUBLISH Sender ->> Sender: Release PacketId

值得注意的是,MQTT发送者在重传时会设置PUBLISH报文中的DUP标志为1,表示是一个重复的报文。

DUP参数的作用

虽然发送者在重传PUBLISH报文时,会将报文中的DUP标志设置为1(表示该报文是重复的),但是接收者并不能因此就假定曾经接收过该报文。从这儿看出DUP标志的意义不大

这是因为对于接收者来说存在以下三种情况:

第一种情况如下图所示:

sequenceDiagram participant Sender participant Receiver autonumber Sender ->> Sender: Store PUBLISH Sender ->> Receiver: PUBLISH(Payload, PacketId=100, DUP=0, QoS=1) note right of Sender: Transport failure Sender ->>+ Receiver: PUBLISH(Payload, PacketId=100, DUP=1, QoS=1) Receiver ->>- Sender: PUBACK(PacketId=100) Sender ->> Sender: Delete PUBLISH Sender ->> Sender: Release PacketId

从图中可以看出,虽然发送者发送的第2个报文中的DUP标志为1,但是接收者事实上只收到1个报文,因此必须当作新消息来进行处理。

第二种情况如下图所示:

sequenceDiagram participant Sender participant Receiver autonumber Sender ->> Sender: Store PUBLISH Sender ->> Receiver: PUBLISH(Payload, PacketId=100, DUP=0, QoS=1) Receiver ->> Sender: PUBACK(PacketIds=100) note left of Receiver: Transport failure Sender ->>+ Receiver: PUBLISH(Payload, PacketId=100, DUP=1, QoS=1) Receiver ->>- Sender: PUBACK(PacketId=100) Sender ->> Sender: Delete PUBLISH Sender ->> Sender: Release PacketId

从图中可以看出,接收者分别在步骤2、4处接收到两个报文,报文PacketId相同并且第2个报文DUP为1,这可以确定为重复的报文。

第三种情况如下图所示:

sequenceDiagram participant Sender participant Receiver autonumber rect rgb(200, 150, 255) Sender ->> Sender: Store PUBLISH Sender ->>+ Receiver: PUBLISH(Payload, PacketId=100, DUP=0, QoS=1) Receiver ->>- Sender: PUBACK(packetId=100) Sender ->> Sender: Delete PUBLISH Sender ->> Sender: Release packetId=100 end rect rgb(191, 223, 255) Sender ->> Sender: Store PUBLISH Sender ->> Receiver: PUBLISH(Payload, PacketId=100, DUP=0, QoS=1) note right of Sender: Transport failure Sender ->>+ Receiver: PUBLISH(Payload, PacketId=100, DUP=1, QoS=1) Receiver ->>- Sender: PUBACK(packetId=100) Sender ->> Sender: Delete PUBLISH Sender ->> Sender: Release packetId=100 end

从图中可以看出,第1次PUBLISH/PUBACK报文交互已经完成,PacketId=100的标识符变为可用状态。

当发送者再次使用该PacketId=100发送全新的PUBLISH报文且第1次发送失败,第2次重传该PUBLISH报文,接收者接收到的报文和上次的PacketId相同并且DUP为1,但是是全新的报文

总结

在第1、3种情况中,接收者收到的都是新报文,在第2种情况中,接收者收到的是重复报文。

对于接收者来说,收到PUBLISH报文并响应PUBACK报文之后,PacketId就可以重新使用。但是接收者并不能根据相同的PacketId来判断当前报文是因为上一次PUBACK报文没有响应成功导致发送者重传的,还是发送者使用该PacketId发送的新PUBLISH报文。

换句话说,由于无法区分相同PacketId的报文是否属于重复报文,因此都必须当作全新的报文来进行处理。因此在使用QoS1时,消息的重复在协议层面是无法避免的

那么为了避免报文重复,接收者就需要使用某种确定的方式,来判断收到相同PacketId的报文是新报文还是重复报文,QoS2提供了解决方案(即增加PacketId释放流程)。

QoS2

MQTT消息传递最高级别,确保仅传递一次

为了实现在这个目的,QoS2就需要解决QoS1中相同PacketId是否属于重复报文的问题

为了解决这个问题,QoS新增了一次PUBREL/PUBCOMP报文的交互流程,用于收发双方确认可以释放并重新使用PacketId,在这个交互流程完成之前,接收者收到相同的PacketId报文均为重复报文。

具体过程如下图所示:

sequenceDiagram participant Sender participant Receiver autonumber Sender ->> Sender: Store PUBLISH loop Send Interval Sender ->> Receiver: PUBLISH(Payload, PacketId=100, QoS=2) end Receiver ->> Receiver: Store PUBLISH Receiver ->> Sender: PUBREC(packetId=100) Sender ->> Sender: Delete PUBLISH Sender ->> Sender: Store PUBREL loop Send Interval Sender ->> Receiver: PUBREL(PacketId=100) end Receiver ->> Receiver: Delete PUBREL Receiver ->> Receiver: Relese packetId Receiver ->> Sender: PUBCOMP(packetId=100) Sender ->> Sender: Delete PUBREL Sender ->> Sender: Relese packetId
  1. 当发送者发送第一笔PUBLISH之后,在收到接收者反馈的PUBREC报文之前,会再次重复发送DUP=1的PUBLISH报文。
  2. 当接收者收到QoS2的PUBLISH报文后,会向发送者返回PUBREC报文用于确认接收成功,并转发报文给订阅者。

PUBREC报文示例如下图所示:

  1. 当发送者收到接收者返回的PUBREC确认报文之后,就可以丢弃原始的PUBLISH报文。
  2. 此时发送者缓存PUBREL报文(用于通知接收者可以释放PacketId),并向接收者发送第一笔PUBREL报文。
  3. 发送者在收到接收者对于PUBREL报文的确认(PUBCOMP)之前,会再次重复发送PUBREL报文。

PUBREL报文示例如下图所示:

  1. 当接收者收到PUBREL报文后,就可以释放PacketId,即对接收者来说该PacketId可以用于标识新报文了
  2. 接收者随后返回PUBCOMP报文用于通知发送者已收到PUBREL报文。
  3. 发送者收到PUBCOMP报文后,释放PacketId,表示这一次报文传递结束。其后PacketId可以用于表示新报文

PUBCOMP报文示例如下图所示:

注意事项

为什么QoS2能够保证消息不重复?

QoS2保证消息不丢失的原理和QoS1相同,即在收到确认(PUBREC)之前不停的发送消息。

对于QoS1来说,消息重复的根本原因在于接收者无法确认收到相同PacketId的报文是全新报文还是没有收到响应重传的(即使是DUP标志也无法保证这一点)。

因此消息去重的关键就在于通信双方如果正确同步释放PacketId,即无论是重传消息还是发布新消息,一定要和对端先达成共识

相比QoS1,QoS2增加了PUBREL/PUBCOMP报文流程,该流程保证了消息不会重复。

QoS2使用以下三个约束保证了消息不重复:

  1. 发送者在收到PUBREC报文之前传输的PUBLISH报文(除了第一笔)都属于重复报文
  2. 发送者收到PUBREC报文并发出PUBREL报文之后,就进入了PacketId释放流程,不能再使用当前的PacketId重传PUBLISH报文。
  3. 同时发送者在收到接收者返回的PUBCOMP报文确认接收者完成释放PacketId之前,也不能使用当前PacketId发布新消息。

MQTT如何处理发布者与订阅者之间的QoS级别差异?

当发布者向MQTT代理发送消息时,它为消息指定一个QoS级别。代理将根据发布者指定的QoS级别确认和处理消息。

当MQTT代理向订阅者发送消息时,消息传递的QoS级别是发布者发布消息时的QoS级别和订阅者订阅主题时的QoS级别的较低级别

例如,如果发布者使用QoS2级别发送消息,而订阅者订阅主题时使用QoS1,此时MQTT代理收到消息之后会将该消息降级为QoS1发送给订阅者。如果发布者以QoS0级别发布消息,订阅者订阅主题时使用QoS2,那么MQTT代理收到该消息后会将消息以原QoS级别(QoS0)发送给订阅者。

换句话说,订阅者接收消息的QoS级别不仅取决于订阅主题时设置的QoS级别也取决于发布者发布消息的QoS级别

因此仅有发布者指定QoS2级别并不能保证消息到达订阅者时不会重复。

如果多个客户端使用不同级别的QoS订阅相同的主题会发生什么?

如果多个客户端订阅了相同的主题,但具有不同的服务质量(QoS)级别,代理将根据客户机订阅的QoS级别而不是发布消息的QoS级别向每个客户端发送消息。

如果保留消息与客户端订阅该主题时的QoS不同会怎么处理?

MQTT代理会将比较发布者发送该保留消息与订阅者订阅该主题时的QoS级别,然后以其中较低的QoS级别将该保留消息发布给MQTT客户端

QoS1和QoS2的消息存储

当MQTT客户端启用持久会话并之前使用QoS1或QoS2订阅过主题,那么在客户端脱机之后,代理仍会为该客户端存储该订阅主题上的消息,直到客户端再次可用。

参考文档

MQTT QoS 0, 1, 2 介绍 | EMQ (emqx.com)

What is MQTT Quality of Service (QoS) 0,1, & 2? – MQTT Essentials: Part 6 (hivemq.com)