表1:QoS(摘自协议文档)
QoS value | bit 2 | bit 1 | Description |
---|---|---|---|
0 | 0 | 0 | At most once / Fire and Forget / <=1 |
1 | 0 | 1 | At least once / Acknowledged delivery / >=1 |
2 | 1 | 0 | Exactly once / Assured delivery / =1 |
3 | 1 | 1 | Reserved |
QoS是Quality of Service的缩写,表示服务质量,MQTT把QoS划分为三级,对应协议值为0、1、2,服务质量依次变高,2代表的服务质量是最好的。MQTT的QoS应用在发送消息的过程中。
QoS0表示消息最多发送一次,发送后消息就被丢弃。
QoS1表示消息至少发送一次,可以保证消息被接收方收到,但是会存在接收方收到重复消息的情况。
QoS2表示消息发送成功且只发送一次,这是一个有且只有的概念,可以保证消息发送成功,且接收方只消费一次消息。
具体到程序实现上,QoS发生于传输通道的建立,作用于通道数据的传输。
这个过程中会涉及几个概念,对其进行命名会便于讲解:
表二:概念命名
概念 | 命名 |
---|---|
消息 | M |
消息发送方 | S |
消息接收方 | R |
要弄明白这一过程,还要知道消息的类型:
表三:消息类型(摘自协议文档)
Mnemonic | Enumeration | Description |
---|---|---|
Reserved | 0 | Reserved |
CONNECT | 1 | Client request to connect to Server |
CONNACK | 2 | Connect Acknowledgment |
PUBLISH | 3 | Publish message |
PUBACK | 4 | Publish Acknowledgment |
PUBREC | 5 | Publish Received (assured delivery part 1) |
PUBREL | 6 | Publish Release (assured delivery part 2) |
PUBCOMP | 7 | Publish Complete (assured delivery part 3) |
SUBSCRIBE | 8 | Client Subscribe request |
SUBACK | 9 | Subscribe Acknowledgment |
UNSUBSCRIBE | 10 | Client Unsubscribe request |
UNSUBACK | 11 | Unsubscribe Acknowledgment |
PINGREQ | 12 | PING Request |
PINGRESP | 13 | PING Response |
DISCONNECT | 14 | Client is Disconnecting |
Reserved | 15 | Reserved |
消息的类型不同,用处是不一样的。CONNECT/CONNACK是publisher/subscriber和broker建立连接是用到的消息的类型,PUBLISH/PUBACK/PUBREC/PUBREL/PUBCOMP是发送消息时用到的类型,SUBSCRIBE/SUBACK是订阅消息用到的类型,等等。
发送消息这一过程存在两种情况:
- publisher向broker发送消息
- broker转发消息给subscriber
所以消息发送方可能是publisher也可能是broker,消息接收方可能是subscriber也可能是broker。以上两种情况只是发送双发的差异,消息的传输过程是完全一样的。
QoS0
“最多发送一次消息”。这种情况最为简单,消息发送方把消息丢出去,过程就结束了。整个过程只用到PUBLISH类型的消息。
S ---M(PUBLISH)---> R
可能的结果:
- 消息接收方收到了消息
- 消息接收方没有收到消息
消息发送方只负责发消息,并且只发一次,不关心消息有没有被收到。
消息接收方只负责收消息,其它也不关心。
QoS1
“至少发送一次消息”。这个过程用到两种消息类型,分别是PUBLISH和PUBACK。
首先我们假设网络传输是可靠的,程序是没有BUG的,也就是理想状态下的传输过程:
S ---M(PUBLISH)---> R
S <---M(PUBACK)--- R
现实中,网络传输是不可靠的,程序是有BUG的,所以,上面的传输过程会存在下面的问题:
- M(PUBLISH)发送失败
- M(PUBACK)发送失败
对消息发送方的影响: 无论是哪个发送失败,都会导致S重复发送M消息。
对消息接收方的影响: M(PUBLISH)发送失败对消息接收方没有影响,反正S会保证给“我”消息。 M(PUBACK)发送失败会导致消息接收方收到重复的消息。
相较于QoS0增加的开销:
- M(PUBLISH)重复发送,会占用带宽
- M(PUBACK)消息确认机制,会占用带宽
- 消息发送方需要增加消息超时重发机制,增加程序复杂度,同时带来存储资源开销
QoS2
“有且只有一次,确保消息发送成功且只被接收方处理一次”。首先还是假设在完美情况下,看一下协议定义的交互过程,一共涉及四种类型的消息。
S ---M(PUBLISH)---> R
S <---M(PUBREC)--- R
S ---M(PUBREL)---> R
S <---M(PUBCOMP)--- R
基于网络传输不确定和程序总会存在BUG的事实,以上过程存在的问题:
- M(PUBLISH)消息发送失败
- M(PUBREC)消息发送失败
- M(PUBREL)消息发送失败
- M(PUBCOMP)消息发送失败
问题1和问题2 会导致S不停的向R发送重复消息。为了满足有且只有一次的需求,需要设计一个去重机制:
R创建两个队列,队列A用于保存收到的M(PUBLISH),队列B用于保存收到的M消息的ID(可以在M(PUBLISH)获取)。当R收到M(PUBLISH)时,首先去队列A检查是否存在这个消息,如果不存在则保存,否则直接丢弃,然后向S发送M(PUBREC)消息。当S收到PUBREC消息后,则停止发送M(PUBLISH)消息,并在发送消息队里删除消息M。接下来S会向R发送M(PUBREL)消息。
问题3和问题4 会导致S不停的向R发送M(PUBREL)消息
R收到M(PUBREL)消息后,会取出ID,并在队列B中进行检索,如果存在,则从队列A中取出对应的消息M进行消费,并从队列A和B中删除;如果不存在,则直接丢弃M(PUBREL)消息。接着,向S发送M(PUBCOMP)消息。到这里消息M在R中的处理就完成了,还剩下一种情况会导致S对消息M的处理没有完全完成,这种情况就是:M(PUBCOMP)消息发送失败了。
接下来的逻辑是这样:S没有收到M(PUBCOMP)消息,会继续发送M(PUBREL)消息。R收到M(PUBREL)消息后,在队列B找不到消息ID,会直接发送M(PUBCOMP)消息。总有一次M(PUBCOMP)消息可以发送成功。这样S对消息M的处理也完成了。
至此,消息M处理完成,并且协议上层应用收到且只收到一条M消息 。
重传机制和去重机制:
上面过程中的重传机制只发生在消息发送方,消息接收方只是被动接收消息,然后给与回应。
上面过程中的去重机制只发生在消息接收方。
相较于QoS1增加的开销:
- 一次消息链路变长了,M(PUBREL)重复发送、M(PUBREC)和M(PUBCOMP)占用带宽
- 消息接收方的去重机制增加代码复杂度,同时带来存储资源开销
后记
QoS1的配置能不能实现“有且只有一次,保证发送成功且只消费一次”?
相比较之下,QoS1存在的问题是消息接收方会收到重复的消息,可以给其增加去重机制,就可以在QoS1的配置下做到“有且只有一次”。但是,考虑到计算机的资源(存储和算力)是有限的,程序实现起来会有一些问题。
去重机制需要有一个队列A来记录已经处理过的消息,在QoS1只有两种消息类型的配置下,消息接收方不知道消息发送方什么是否不在发送消息M,所以队里A无法执行删除操作,这样一来,队列A的内容越来越多,会导致程序效率降低,甚至好近存储和算力。