白话消息队列遥测传输协议(MQTT)

MQTT 是一种基于发布/订阅模式的轻量级通讯协议,该协议构建于 TCP/IP 协议上,由 IBM 在 1999 年发布,它是开放消息协议,简单容易实现,消息支持 Qos。MQTT 最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。这些特性,使其在物联网、小型设备、移动应用、车联网、电力能源等方面有着较为广泛的应用。

我是在看嵌入式资料时,了解到了 MQTT 协议。嵌入式还有一种协议叫 CoAP。今天主要讲下 MQTT。我们先看下 MQTT 协议的由来。

MQTT 最初是由 AndyStandford-Clark 博士和 Arlen Nipper 博士于 1999 年发明的通讯协议。他们当时是为了在狭窄的网络带宽和微小电力损耗的前提之下,提供石油管道传感器和人造卫星之间一个轻量、可靠的二进制通讯协议。2011 年,IBM 和 Eurotech 将 MQTT 协议捐赠给 Eclipse 基金会,并加入了 Eclipse M2M Industry 工作组织。2014 年 10 月,MQTT 正式成为一个开放的 OASIS 国际标准。

在看到这里的时候,我在想,如果 MQTT 不公布出来,不就是一个私有协议吗?前段时间,在学习 TCP/IP 时,我还在想一件事,TCP 连接之后,就可以发送数据,但是数据格式还需要自己进行定义,发送方和接收方都需要按照规定的格式进行发送和解析。还有就是 TCP 连接如何认证?除了 IP 白名单之外,如何在应用上实现认证?这些在看了 MQTT 规范后,这些问题都找到了答案,我不知道他们当时怎么想的,我真想掰开他们的脑子,他们是真的太厉害了。他们在 1999 年就已经完成了,现在 20 多年过去了。对于入门 TCP/IP 的我来说,这是一种精神食粮,能吃还很香。

这两天一直在看 MQTT 的相关资料,以及理解 MQTT 协议,不知道哪篇资料写的 MQTT 是基于 IP 协议的,后来才基于 TCP/IP 协议。我个人觉得基于 IP 协议反而更贴切当时的环境,肯定是后来协议优化之后选择的 TCP/IP 协议。在说 MQTT 协议之前,先说下我心中的疑惑?我是看嵌入式资料时,知道 MQTT 的,现在的物联网很火,完全可以做到通过私有协议来连接控制,为什么要 MQTT 协议?后来,看看身边的事物,例如 USB,是为了兼容和互通。我不知道为啥选择 MQTT?开源应该算一个原因吧,MQTT 协议设计非常好算另一个原因吧。我们来看下 MQTT 的特点吧。

MQTT 具有如下特点:

  • 轻量可靠:MQTT 报文格式精简、紧凑,可在严重受限的硬件设备和低带宽、高延迟的网络上实现稳定传输。
  • 发布订阅模式:可以使发布者与订阅者解耦,实现异步协议,即订阅者和发布者不需要建立直接连接、也不需要同时在线。
  • 为物联网而生:提供心跳机制、遗嘱消息、Qos 质量等级+离线消息、主题和安全管理,符合物联网应用特性。
  • 生态日趋完善:实现多语言平台客户端和 SDK,多家云厂商支持并提供 IOT 服务,多种嵌入式硬件支持。

先说下 MQTT 轻量可靠是怎么做到的?

MQTT 基于 TCP/IP,TCP/IP 是一种面向连接的、可靠的、基于字节流的通信协议。我这两天一直看的是 MQTT3.1.1 协议,就以它来讲。MQTT 包含 14 种类型的控制包,应用之间的交互就是通过这 14 种包协作来完成。这些控制包有固定的结构,分为 3 部分:固定包头,可变包头,载荷。

这里处处都是精华,我们来细看:

固定包头占用 2 个字节,每一个控制包都包含。第一个字节高四位,指示控制包类型。低四位是每个 MQTT 控制包类型的特殊标识。第二个字节用来指示控制包的剩余字节长度,包括可变包头的数据以及载荷。剩余长度不包含用来编码剩余长度的字节。它使用了一种可变长度结构来编码。我在此处费了一些脑细胞,我用白话来讲解下这段,我们知道一个字节一共有 8 位,可以表示十进制 256,现在,这个字节中的第 8 位用来表示是否还有下一个字节,剩下 7 位来表示长度,可以表示十进制 128。MQTT 规定,剩余长度最多可以用四个字节来表示。也就是允许这个控制包最大的长度为 256M 大小。协议给出了编码算法和解码算法,可以看 MQTT 官方协议。

我这里拿十进制 321 来举例进行编码。你要记住,这个算法的精髓是逢 128 进位。我们将 321 整除 128 求余数,得余数 65,放在第一个字节,然后将 321 整除 128 求商,得商为 2,当为 0 时截止,因为此时商为 2,说明还有下一个字节,我们需要将第一个字节的第 8 位置 1,在计算机中,将某一位置 1,可以使用或计算。这里是将 65 和 128 的二进制进行或运算。我们来计算下一个字节,将得到的商 2,继续整除 128 求余数,得余数 2,放在第二个字节,然后将 2 整除 128 求商,得商为 0,停止计算。最后得出的结果是,第一个字节 65+128=193,第二个字节是 2。然后我们再看下上面的结果如何再解码出长度数据。我们首先对第一个字节和十进制 127(01111111)进行与运算,这一步是取出 7 位实际数据,然后乘以算法因子(算法因子是 1128128*128,即第一个字节乘 1,第二个到第四个都是乘以 128,因为是 128 进位)。得出第一个字节的值为 65,然后将第一个字节和十进制 128(10000000)进行与运算,可以算出第 8 位是否为 1,如果为 1,说明还有下一个字节。然后将第二个字节和十进制 127 进行与运算,得出 2,然后乘以 128,得出的结果 256 和第一个字节 65 相加,得出 321。然后再将第二个字节和十进制 128 进行与运算,查看第 8 位是否为 1,可以知道,这里为 0,停止运算,结果就是 321。

固定包头讲完了,我们看看可变包头,当时我在想,写到载荷中不行吗?非得加可变包头,深度思考后我个人是这样想的,可变包头是协议定义的,载荷是用户提交的数据,意义不一样。协议中定义那么在实现 SDK 或功能时,必须实现,而最终使用者可以不关心这个,这个是我的理解。可变包头不是每个控制包都包含,MQTT 定义了有哪些控制包必须包含,例如订阅,发布,取消订阅。可变包头的内容取决于包的类型。很多类型的控制包的可变包头都包含了 2 字节的唯一标识字段。它用来唯一识别这个控制包,确保服务质量可靠性。

载荷存在于一些控制包类型中。MQTT 定义了有哪些控制包必须包含载荷,载荷可以存放应用消息,是应用提交的数据。如果用有用功来表示的话,这部分数据对于应用来说才是有用功。

在前面介绍我入门 TCP 后的问题中,我提到了需要自定义数据格式,这里 MQTT 规定了数据格式,并且设计的非常精妙,完美解决数据格式问题。那么它是如何解决认证问题呢?MQTT 在 CONNECT 控制包中,可变包头中指定了连接标识,当 UserNameFlag 设置为 1 时,用户名必须出现在载荷中,当 PasswordFlag 设置为 1 时,密码必须出现在载荷中。这样在协议中就解决了认证问题,不依赖服务器 IP 白名单。

最后,再来说说质量服务 Qos,这个也是核心精髓,MQTT 根据质量服务等级来分发应用消息。Qos 分为 3 个等级:Qos0,最多分发一次,Qos1,最少分发一次,Qos2,精确一次分发。随着服务质量提高,会增加相应的开销。 在 Qos0 等级下,消息只发送一次,不管接收者是否收到。我们看下流程图:

发送者控制包接收者
PUBLISH Qos 0,DUP=0------>分发消息给合适的接收者

在 Qos1 等级下,确保消息至少一次抵达接收者。Qos1 中的 PUBLISH 包的可变包头中包含唯一标识,而且有 PUBACK 包确认。我们看下流程图:

发送者控制包接收者
存储消息,PUBLISH Qos 1,DUP 0,包唯一标识------>接收并转发消息
销毁消息<------发送 PUBACK 包唯一标识

在 Qos2 等级下,确保消息精确一次分发,不能丢失不能重复。这种服务质量会增加开销。Qos2 消息可变包头中包含唯一标识。Qos2 的 PUBLISH 包的接收者分两步确认接收。我们看下流程图:

发送者控制包接收者
存储消息,PUBLISH Qos 2,DUP 0,包唯一标识------>方法 A,存储消息
销毁消息,存储 PUBREC 包<------PUBREC 包唯一标识
PUBREL 包唯一标识------>方法 A,转发消息并销毁消息
销毁存储状态<------PUBCOMP 包唯一标识

为什么 Qos2 消息不会重复?与 Qos1 相比,Qos2 增加了 PUBREL 报文和 PUBCOMP 报文,在使用 Qos1 消息时,对接收方来说,回复完 PUBACK 报文后,Packet ID 就可以重新使用了,也不管是否确实发送到了发送方。这样,当收到相同 Packet ID 报文后,无法得知时发送方因为没有收到响应而重传,还是发送方收到了响应而重新使用 Packet ID,发送了一个全新的消息。所以,消息去重的关键就在于,通信双方如何正确的同步释放 Packet ID。在 Qos2 中增加的 PUBREL 流程,就是帮助通信双方 Packet ID 何时重用的能力。Qos2 规定,发送方只有收到 PUBREC 报文之前可以重传 PUBLISH 报文。一旦收到 PUBREC 报文并发出 PUBREL 报文,发送方就进入了 Packet ID 释放流程,不可以再使用当前 PacketID 重传 PUBLISH 报文。同时,在收到对端回复的 PUBCOMP 报文确认双方都完成 Packet ID 释放之前,也不可以使用当前 Packet ID 发送新的消息。

我们用白话来描述一下 Qos 的三种消息。发送方:小张,接收方:小李,中间人:小王,消息:晚上一块打球,场景:一间教室。

Qos0,小张写了一张纸条晚上一块打球,裹成了纸团,传给了小王,小王又将纸团传给了小李,小李收到后知道晚上一块打球。第二天,小张写了一张纸条晚上一块打球,裹成了纸团,传给了小王。但传的时候,纸团掉在了地上。消息丢失。

Qos1,小张写了一张纸条晚上一块打球,并写了一个编号 101,裹成了纸团,传给了小王,小王又将纸团传给了小李,小李收到后知道晚上一块打球,并回了小张一个纸条,101 消息已经收到。第二天,小张写了一张纸条晚上一块打球,并写了一个编号 101,裹成了纸团,传给了小王。但传的时候,纸团掉在了地上。小张等了约定的时间后,发现小李没有回复消息,就又写了一张编号 101 的消息传给了小李。这次很顺利,小李收到之后马上回复。第三天,小张继续传送 101 纸条,小李在回复的时候,小王又给弄掉了,小张继续发送 101 纸条,小李收到后,还是回复了对方消息已收到,只是小李不知道这个是新消息还是老消息。

Qos2,这次,小张买了信封,并和小李约定,收到信封后马上回复我收到信封,看了内容之后,再把信封还给我。所以这次小张将写的晚上一块打球的纸条添加到了信封中,当小李收到后,要回复小纸条信已收到,如果到规定时间不回复,小张继续发 101 信封,当小张收到信已收到的纸条后,将不再发送 101 信封,小张写小纸条让小李还回来 101 信封,当小张收到 101 信封后,就可以继续使用 101 信封写新的小纸条了。对于小李来说,它只收到了一条消息。这里,只是表达这个意思,在计算机中,包可以进行多次发送。

MQTT 通过 Qos 保证了消息可靠性。最后,我们来说说发布订阅,这也是一个亮点。我们假设没有发布订阅,那么发送者和接受者需要直接连接,或者通过中转器,无论哪一种,发送者个接受者都绑定到了一起。这让我想到了以前一个老总讲的医药厂家和医院的关系,当没有代理商的时候,医药厂家需要和每个医院建立关系,而医院也需要和每个医药厂家建立关系,对于医药厂家,医院都会有巨大的管理成本。如果引入代理商,将会解决这个问题,首先对于医院来说,它只需要对接这个代理商即可,对于厂家来说,也是只需要对接这个代理商即可。我不知道当时两位博士是否知道这个代理商的故事,但是我觉得应该有这方面的考量。在引入这个代理商后,当多家医院向代理商订购了某个厂家的药后,一旦该厂家生产出来。代理商会快速的分发到订购的各家医院。同理,在 MQTT 中,发布者和订阅者也是这样的关系,发布者不知道订阅者是谁?它们进行了解耦。在计算机领域,这样的模式可以使多家硬件厂商,软件厂商进行协作沟通。这应该就是 MQTT 的魅力所在吧。发布者和订阅者这里通过主题进行关联。首先,订阅者需要订阅主题,发布者会向主题上发送消息。订阅者可以订阅多个主题。我们还拿医院和厂家来举例子,如果医院要买感冒药,那么这里买感冒药可以认为是个主题,那么无论哪个厂家生产出来的感冒药都可以卖给它。如果医院要买某个厂家的感冒药,那么只有这个厂家的感冒药生产出来可以卖给它。这又带出来 MQTT 协议安全管理的话题。在这里,代理商会管理这些。比如上面说的 A 医院订购 A 厂家的药,如果是 B 厂家生产的感冒药,代理商不允许分配给 A 医院。同样的,厂家也可以指定只有某医院可以使用他们的药。例如特类品种药,代理商会管理只有某个医院可以订购。在 MQTT 协议中,这些是通过绑定主题,以及配置授权策略来实现的。

MQTT 的遗嘱消息,这里也聊一下。在 MQTT 协议中,遗嘱的建立是发布者在连接的时候就确定了的,当连接到 Broker 时,连接的遗嘱标识会设置,遗嘱信息,遗嘱主题以及遗嘱 Qos 等信息会放在载荷中。当这个客户端没有发送 DISCONNECT 控制包,就断开了连接,那么 Broker 要将遗嘱消息分发给订阅了遗嘱主题的客户端。这个遗嘱消息常用来监控客户端非正常断开连接。这里依石油管道传感器举例,在真实环境中,传感器可能突然断开连接,在非常断开的时候,Broker 会将遗嘱消息发给订阅者,这样管理人员能够知道是哪个传感器出了问题,方便下一步操作。

更多的内容,请参考官方文档。

https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html