参考文档:https://www.hivemq.com/mqtt-essentials/
MQTT是物联网(IoT)最常用的消息传输协议。MQTT是MQ遥测传输的缩写。该协议是一套规则,定义了物联网设备如何通过互联网发布和订阅数据。MQTT用于物联网和工业物联网(IIoT)设备之间的信息传递和数据交换,如嵌入式设备、传感器、工业PLC等。该协议是事件驱动的,使用发布/订阅(Pub/Sub)模式连接设备。发送方(Publisher)和接收方(Subscriber)通过主题(Topic)进行通信,彼此之间是解耦的。它们之间的连接是由MQTT代理处理的。MQTT代理过滤所有传入的消息,并将它们正确地分发给订阅者。
“MQTT是一个客户端服务器发布/订阅消息的传输协议。它是轻量级的、开放的、简单的,而且设计得很容易实现。这些特点使它在许多情况下都是理想的选择,包括受限制的环境,如机器对机器(M2M)和物联网(IoT)背景下的通信,其中需要较小的代码足迹和/或网络带宽是有优势的。”—— MQTT 3.1.1规范
MQTT规范的摘要很好地描述了MQTT的内容。它是一个非常轻量级的二进制协议,由于其最小的数据包开销,与HTTP等协议相比,MQTT在电线上传输数据时表现出色。该协议的另一个重要方面是,MQTT在客户端非常容易实现。易用性是MQTT发展过程中的一个关键问题,并使其成为当今资源有限的受限设备的完美选择。
MQTT协议是由Andy Stanford-Clark(IBM)和Arlen Nipper(Arcom,现在的Cirrus Link)于1999年发明的。他们需要一个最小电池损耗和最小带宽的协议,以便通过卫星与石油管道连接。这两位发明家为未来的协议指定了几个要求:
这些目标仍然是MQTT的核心。然而,该协议的主要焦点已经从专有的嵌入式系统转变为开放的物联网(IoT)用例。这种焦点的转移造成了很多关于MQTT的缩写代表什么的混乱。简短的回答是,MQTT不再被认为是一个首字母缩写。MQTT只是该协议的名称。
较长的答案是,前者的缩写代表MQ遥测传输。
"MQ "指的是MQ系列,这是IBM开发的一个支持MQ遥测传输的产品。当Andy和Arlen在1999年创建他们的协议时,他们用IBM的产品来命名它。许多消息来源将MQTT错误地标记为一个消息队列协议。这根本不是事实。MQTT不是一个传统的消息队列解决方案(尽管在某些情况下有可能对消息进行排队,这一事实我们将在接下来的文章中详细讨论)。在接下来的十年里,IBM在内部使用该协议,直到他们在2010年将MQTT 3.1作为免版税版本发布。从那时起,欢迎每个人都来实现和使用该协议。
我们在2012年开始认识了MQTT,并在同年建立了第一个版本的HiveMQ。2013年,我们向公众发布了HiveMQ。随着协议规范的发布,IBM为Eclipse基金会新成立的Paho项目贡献了MQTT客户端的实现。这些事件对该协议来说绝对是一件大事,因为如果没有一个支持性的生态系统,几乎没有机会广泛采用。
在最初发布约3年后,宣布MQTT将在OASIS(一个以推进标准为目的的开放组织)的羽翼下实现标准化。AMQP、SAML和DocBook只是之前发布的OASIS标准中的几个。标准化过程花了大约1年时间。2014年10月29日,MQTT成为正式批准的OASIS标准。从3.1到3.1.1的小版本变化表明,对前一个版本的修改很少。有关这些变化的详细信息,请参见我们关于3.1.1的优势的博文。
2019年3月,OASIS批准了新的MQTT 5规范。这个新的MQTT版本为MQTT引入了部署在云平台上的物联网应用所需的新功能,以及那些需要更多的可靠性和错误处理来实现关键任务的信息传递。要了解更多关于MQTT 5的信息,请查看我们的MQTT 5精华系列博客。
我们强烈建议使用MQTT 5。
MQTT发布/订阅模式(也称为pub/sub)为传统的客户端-服务器架构提供了一个替代方案。在客户端-服务器模式中,客户端直接与端点进行通信。pub/sub模式将发送消息的客户端(发布者)与接收消息的客户端(订阅者)解耦。发布者和订阅者从不直接联系对方。事实上,他们甚至不知道对方的存在。他们之间的联系由第三个组件(经纪人)处理。经纪人的工作是过滤所有传入的消息,并将它们正确地分发给订阅者。因此,让我们深入了解一下发布/订阅的一些一般方面(我们将在一分钟内谈论MQTT的具体内容)。
pub/sub最重要的方面是消息的发布者与接收者(订阅者)的解耦。这种解耦有几个方面:
总之,MQTT的pub/sub模型取消了消息的发布者和接收者/订阅者之间的直接通信。代理商的过滤活动使得控制哪个客户/用户收到哪个消息成为可能。解耦有三个层面:空间、时间和同步。
** MQTT Pub/Sub的扩展性比传统的客户-服务器方法好**。这是因为代理上的操作可以高度并行化,消息可以以事件驱动的方式处理。消息缓存和消息的智能路由往往是提高可扩展性的决定性因素。尽管如此,扩展到数百万的连接是一个挑战。这样高的连接量可以通过集群的经纪人节点来实现,使用负载均衡器将负载分配到更多的单个服务器上。(这个话题超出了当前文章的范围,我们将在另一篇文章中介绍)。
很明显,MQTT代理在发布/订阅过程中起着关键的作用。但是,经纪人是如何过滤所有的消息,使每个订阅者只收到感兴趣的消息的呢?正如你将看到的,代理有几个过滤方案:
当然,发布/订阅不是每个用例的答案。在你使用这种模式之前,有几件事你需要考虑。发布者和订阅者的解耦是pub/sub的关键,它本身也带来一些挑战。例如,你需要事先了解发布的数据是如何结构化的。对于基于主题的过滤,发布者和订阅者都需要知道要使用哪些主题。另一件需要注意的事情是信息传递。发布者不能假设有人在收听所发送的消息。在某些情况下,有可能没有订阅者阅读某条消息。
现在我们已经探讨了一般的发布/订阅模型,让我们具体关注一下MQTT。根据你想要实现的目标,MQTT体现了我们所提到的发布/订阅的所有方面:
另一件应该提到的事情是,MQTT在客户端特别容易使用。大多数pub/sub系统的逻辑都在代理方,但MQTT在使用客户端库时确实是pub/sub的精髓,这使得它成为小型和受限设备的轻量级协议。
MQTT使用基于主题的消息过滤。每条消息都包含一个主题(topic),经纪人可以用它来决定订阅的客户端是否能得到该消息。请参阅MQTT要点的第5部分,了解更多关于主题的概念。如果需要,你也可以用HiveMQ MQTT代理和我们的自定义扩展系统来设置基于内容的过滤。
为了应对pub/sub系统的挑战,MQTT有三个服务质量(QoS)级别。你可以很容易地指定一个消息被成功地从客户端传递给代理,或从代理传递给客户端。然而,有可能没有人订阅这个特定的主题。如果这是一个问题,经纪人必须知道如何处理这种情况。例如,HiveMQ MQTT代理有一个插件系统,可以解决这种情况。你可以让经纪人采取行动,或者简单地将每条消息记录到数据库中进行历史分析。为了保持分层主题树的灵活性,必须非常仔细地设计主题树,并为未来的用例留下空间。如果你遵循这些策略,MQTT是生产设置的完美选择。
对于MQTT这个名字,以及该协议是否以消息队列的形式实现,有很多人感到困惑。我们将尝试对这个话题进行一些说明,并解释其中的区别。在上一篇文章中,我们提到MQTT指的是IBM的MQseries产品,与 "消息队列 "没有关系。不管这个名字来自哪里,了解MQTT和传统的消息队列之间的区别是非常有用的。
当你使用一个消息队列时,每个传入的消息都被储存在队列中,直到它被一个客户(通常称为消费者)取走。如果没有客户端拿起该消息,该消息仍停留在队列中,等待被消费。在消息队列中,消息不可能不被任何客户端处理,就像在MQTT中,如果没有人订阅一个主题一样。
一个消息只被一个客户消费。另一个很大的区别是,在传统的消息队列中,一个消息只能被一个消费者处理。负载是在一个队列的所有消费者之间分配的。在MQTT中,行为则完全相反:每个订阅主题的订阅者都能得到消息。
队列是命名的,必须明确创建队列 队列比主题要严格得多。在使用队列之前,必须用一个单独的命令明确地创建队列。只有在队列被命名和创建之后,才有可能发布或消费消息。相比之下,MQTT主题非常灵活,可以即时创建。
下面是对核心概念的快速回顾:
我们的上一篇文章给了你一个关于发布/订阅模型的高层观点,以及它与传统的消息队列有什么不同。这篇文章采取了一种实用的方法,并塞满了关于MQTT的基本知识:MQTT客户端和代理的术语定义,MQTT连接的基本知识,带有参数的连接消息,以及通过代理的确认建立连接。
因为MQTT将发布者和订阅者解耦,所以客户端连接总是由一个经纪人处理。在我们讨论这些连接的细节之前,让我们清楚地了解一下我们所说的客户端和经纪人是什么意思。
当我们谈论一个客户端时,我们几乎总是指MQTT客户端。发布者和订阅者都是MQTT客户端。发布者和订阅者的标签指的是客户端当前是发布消息还是订阅接收消息(发布和订阅功能也可以在同一个MQTT客户端中实现)。MQTT客户端是任何运行MQTT库并通过网络连接到MQTT代理的设备(从一个微型控制器到一个成熟的服务器)。例如,MQTT客户端可以是一个非常小的、资源有限的设备,通过无线网络连接,并拥有一个最低限度的库。MQTT客户端也可以是一台运行图形化MQTT客户端的典型计算机,用于测试目的。基本上,任何通过TCP/IP协议栈讲MQTT的设备都可以被称为MQTT客户端。MQTT协议的客户端实现是非常直接和简化的。实施的简易性是MQTT非常适合于小型设备的原因之一。MQTT客户端库可用于大量的编程语言。例如,Android、Arduino、C、C++、C#、Go、iOS、Java、JavaScript和.NET。你可以在MQTT的维基上看到一个完整的列表。
与MQTT客户端相对应的是MQTT代理。代理商是任何发布/订阅协议的核心。根据不同的实现方式,经纪人可以处理多达数百万个并发连接的MQTT客户端。
代理人负责接收所有的消息,过滤消息,确定谁订阅了每条消息,并将消息发送给这些订阅的客户。代理人还持有所有拥有持久会话的客户端的会话数据,包括订阅和错过的消息(更多细节)。代理人的另一个责任是对客户的认证和授权。通常情况下,经纪人是可扩展的,这有利于自定义认证、授权和与后端系统的集成。集成尤其重要,因为经纪人经常是直接暴露在互联网上的组件,处理大量的客户,并需要将消息传递给下游的分析和处理系统。正如在以前的文章中所讨论的,订阅所有的消息并不是一个真正的选择。简而言之,经纪人是中央枢纽,每条消息都必须通过它。因此,重要的是你的代理服务器要有高度的可扩展性,可以集成到后端系统中,易于监控,并且(当然)能抗故障。HiveMQ通过使用最先进的事件驱动的网络处理、开放的扩展系统和标准的监控提供者来满足这些要求。
MQTT协议是基于TCP/IP的。客户端和经纪人都需要有一个TCP/IP协议栈。
MQTT连接总是在一个客户和经纪人之间。客户端永远不会直接连接到对方。为了启动一个连接,客户端向经纪人发送一个CONNECT消息。代理商用一个CONNACK消息和一个状态代码来响应。一旦连接建立,经纪人就会保持开放,直到客户发送断开命令或连接中断。
在许多常见的用例中,MQTT客户端位于一个路由器后面,该路由器使用网络地址转换(NAT)将私有网络地址(如192.168.x.x,10.0.x.x)转换为公共地址。正如我们已经提到的,MQTT客户端通过向经纪人发送CONNECT消息来启动连接。因为经纪人有一个公共地址,并保持连接开放,允许双向发送和接收消息(在最初的CONNECT之后),所以对于位于NAT后面的客户端来说,根本就没有问题。
现在让我们来看看MQTT CONNECT命令消息。为了启动一个连接,客户端向经纪人发送一个命令消息。如果这个CONNECT消息是畸形的(根据MQTT规范),或者在打开网络套接字和发送连接消息之间经过了太多的时间,经纪人会关闭连接。这种行为可以阻止恶意的客户端,因为它们会拖慢经纪人的工作。一个善良的MQTT 3客户端发送一个连接消息,内容如下(除其他外):
CONNECT消息中包含的一些信息可能对MQTT库的实现者而不是该库的用户更感兴趣。关于所有的细节,请看MQTT 3.1.1规范。
我们将重点讨论以下选项。
3.2.2.1、ClientId
客户端标识符 (ClientId) 用于识别连接到MQTT代理的每个MQTT客户端。代理商使用 ClientId 来识别客户端和客户端的当前状态。因此,这个Id应该是每个客户端和代理机构唯一的。在MQTT 3.1.1中,如果你不需要经纪人持有的状态,你可以发送一个空的ClientId。空ClientId的结果是一个没有任何状态的连接。在这种情况下,干净的会话标志必须被设置为 "true",否则经纪商将拒绝该连接。
3.2.2.2、Clean Session
清洁会话标志告诉经纪人,客户是否想建立一个持久的会话。在一个持久的会话中(CleanSession = false),经纪人为客户存储所有的订阅和所有错过的信息,这些信息是以服务质量(QoS)级别1或2订阅的客户。如果会话不是持久的(CleanSession = true),经纪人不为客户存储任何东西,并清除任何以前的持久会话的所有信息。
3.2.2.3、用户名/密码
MQTT可以为客户端的认证和授权发送一个用户名和密码。然而,如果这些信息没有被加密或散列(无论是通过实施还是TLS),密码就会以纯文本形式发送。我们强烈建议将用户名和密码与安全传输一起使用。像HiveMQ这样的经纪公司可以通过SSL证书来验证客户,所以不需要用户名和密码。
3.2.2.4、遗嘱消息
最后遗嘱消息是MQTT的最后遗嘱(LWT)功能的一部分。当一个客户端不体面地断开连接时,这个消息会通知其他客户端。当一个客户端连接时,它可以以MQTT消息和CONNECT消息中的主题的形式向经纪人提供一个最后的遗嘱。如果客户端不顾一切地断开连接,经纪人会代表客户端发送LWT消息。你可以在本系列的第9部分了解更多关于LWT的信息。
3.2.2.5、Keep Alive
Keep Alive 是一个时间间隔,以秒为单位,由客户指定并在连接建立时传达给经纪人。这个时间间隔定义了经纪商和客户可以忍受的不发送消息的最长时间段。客户端承诺定期向经纪人发送PING请求消息。经纪人以PING响应来回应。这种方法允许双方确定另一方是否仍然可用。关于MQTT保活功能的详细信息,请参见本系列的第10部分。
基本上,这就是你从MQTT 3.1.1客户端连接到MQTT代理的所有信息。个别库通常有额外的选项,你可以进行配置。例如,在一个特定的实现中,排队的消息的存储方式。
当一个经纪人收到CONNECT消息时,它有义务用CONNACK消息来响应。
CONNACK消息包含两个数据项:
3.2.3.1、会话存在标志
会话存在标志告诉客户,经纪商是否已经在与客户的先前交互中获得了一个持久的会话。当一个客户在清洁会话设置为 "true "的情况下进行连接时,会话存在标志总是false,因为没有可用的会话。如果客户在连接时将Clean Session设置为false,有两种可能。如果客户端的会话信息是可用的,并且经纪人已经存储了会话信息,则会话存在标志为真。否则,如果经纪商没有该客户的任何会话信息,那么会话存在标志为false。这个标志是在MQTT 3.1.1中添加的,以帮助客户端确定他们是否需要订阅主题,或者主题是否仍然存储在一个持久的会话中。
3.2.3.2、连接返回代码
CONNACK消息中的第二个标志是连接确认标志。这个标志包含一个返回代码,告诉客户端连接尝试是否成功。
下面是返回代码,一目了然:
返回代码 | 返回代码响应 |
0 | 连接接受 |
1 | 连接被拒绝,协议版本不可接受 |
2 | 连接被拒绝,标识符被拒绝 |
3 | 连接被拒绝,服务器不可用 |
4 | 连接被拒绝,用户名或密码错误 |
5 | 连接被拒绝,未授权 |
关于这些代码的更详细的解释,请参阅MQTT规范。
一个MQTT客户端可以在连接到一个代理机构后立即发布消息。MQTT利用基于主题的过滤方式对代理上的消息进行过滤(详见第二部分)。每条消息都必须包含一个主题,经纪人可以用这个主题将消息转发给感兴趣的客户。通常情况下,每个消息都有一个有效载荷,其中包含以字节格式传输的数据。MQTT是数据无关的。客户端的使用情况决定了有效载荷的结构方式。发送客户端(发布者)决定它是否要发送二进制数据、文本数据,甚至是成熟的XML或JSON。
MQTT中的PUBLISH消息有几个属性,我们要详细讨论一下。
如果没有人收到消息,发布消息就没有意义。换句话说,如果没有客户端来订阅消息的主题。为了接收感兴趣的主题的消息,客户端向MQTT代理发送一个SUBSCRIBE消息。这个订阅消息非常简单,它包含一个唯一的数据包标识符和一个订阅列表。
为了确认每次订阅,经纪人向客户发送一个SUBACK确认消息。该消息包含原始订阅消息的数据包标识符(以清楚地识别该消息)和一个返回代码列表。
数据包标识符:数据包标识符是一个唯一的标识符,用于识别一个消息。它与SUBSCRIBE消息中的相同。
返回代码:代理商为它在SUBSCRIBE消息中收到的每个主题/QoS-对发送一个返回代码。例如,如果SUBSCRIBE消息有五个订阅,SUBACK消息就包含五个返回代码。返回代码确认每个主题,并显示由经纪人授予的QoS级别。如果经纪人拒绝订阅,SUBACK消息包含该特定主题的失败返回码。例如,如果客户端没有足够的权限来订阅该主题或该主题是畸形的。
返回代码 | 返回代码响应 |
0 | 成功 - 最大 QoS 0 |
1 | 成功 - 最大 QoS 1 |
2 | 成功 - 最大 QoS 2 |
128 | 失败 |
在客户端成功发送SUBSCRIBE消息并收到SUBACK消息后,它将获得每一个与SUBSCRIBE消息所包含的订阅中的主题相匹配的已发布消息。
与SUBSCRIBE消息相对应的是UNSUBSCRIBE消息。该消息删除经纪人上客户的现有订阅。UNSUBSCRIBE消息与SUBSCRIBE消息类似,有一个数据包标识符和一个主题列表。
为了确认取消订阅,经纪人向客户发送一个UNSUBACK确认消息。该消息只包含原始UNSUBSCRIBE消息的数据包标识符(以明确识别该消息)。
数据包标识符:数据包标识符是对消息的唯一标识。如前所述,这与UNSUBSCRIBE消息中的数据包标识符相同。
在收到经纪人的UNSUBACK后,客户端可以认为UNSUBSCRIBE消息中的订阅已经删除。
在MQTT中,主题这个词指的是一个UTF-8字符串,经纪人用它来为每个连接的客户端过滤消息。主题由一个或多个主题级别组成。每个主题级别由一个正斜杠(主题级别分隔符)分开。
与消息队列相比,MQTT主题是非常轻量级的。客户端不需要在发布或订阅之前创建所需的主题。代理商接受每个有效的主题,而不需要事先进行任何初始化。
下面是一些主题的例子:
请注意,每个主题必须至少包含1个字符,而且主题字符串允许有空位。主题是区分大小写的。例如,myhome/temperature和MyHome/Temperature是两个不同的主题。此外,单单是正斜杠就是一个有效的主题。
当客户端订阅一个主题时,它可以订阅已发布消息的确切主题,也可以使用通配符来同时订阅多个主题。通配符只能用于订阅主题,不能用于发布消息。有两种不同的通配符:单级和多级。
顾名思义,单级通配符取代了一个主题级别。加号代表一个主题中的单级通配符。
如果任何主题包含一个任意的字符串而不是通配符,则该主题与带有单级通配符的主题匹配。例如,对myhome/groundfloor/+/temperature的订阅可以产生以下结果。
多级通配符涵盖许多主题级别。哈希符号代表主题中的多级通配符。为了让经纪人确定哪些主题是匹配的,多级通配符必须放在主题中的最后一个字符,并且前面有一个正斜杠。
当客户端订阅一个带有多级通配符的主题时,它就会收到以通配符前的模式开始的主题的所有消息,无论该主题有多长或多深。如果你只指定多级通配符作为一个主题(#),你会收到所有发送到MQTT代理的消息。如果你期望有高的吞吐量,单独用多级通配符订阅是一种反模式(见下面的最佳实践)。
一般来说,你可以按照自己的意愿来命名你的MQTT主题。然而,有一个例外。以$符号开头的主题有一个不同的目的。当你把多级通配符作为一个主题(#)来订阅时,这些主题不是订阅的一部分。$符号的主题是为MQTT代理的内部统计保留的。客户端不能向这些主题发布消息。目前,这类主题还没有正式的标准化。通常情况下,$SYS/被用于以下所有信息,但经纪人的实现是不同的。关于$SYS-topics的一个建议是在MQTT GitHub的维基中。这里有一些例子:
这些是MQTT消息主题的基础知识。正如你所看到的,MQTT主题是动态的,提供了极大的灵活性。当你在现实世界的应用中使用通配符时,有一些挑战是你应该注意的。我们已经收集了我们在不同项目中广泛使用MQTT的最佳实践,并随时欢迎对这些实践提出建议或进行讨论。使用评论来开始对话,让我们知道你的最佳实践,或者如果你不同意我们的某个做法
在MQTT中允许使用前导斜杠。例如,/myhome/groundfloor/livingroom。然而,前面的正斜杠引入了一个不必要的主题级别,在前面有一个零字符。这个零并没有提供任何好处,而且经常导致混淆。
空格是每个程序员的天敌。当事情进展不顺时,空格会使阅读和调试主题变得更加困难。就像前导斜杠一样,允许使用的东西并不意味着应该使用它。UTF-8有许多不同的空白类型,这种不常见的字符应该被避免。
每个主题都包含在每个使用它的消息中。尽可能使你的主题短而精。当涉及到小型设备时,每一个字节都很重要,主题的长度有很大影响。
由于非ASCII UTF-8字符经常显示错误,所以很难发现与字符集有关的错字或问题。除非是绝对必要,我们建议避免在主题中使用非ASCII字符。
在主题中包含发布客户端的唯一标识符可能非常有帮助。主题中的唯一标识符可以帮助你识别谁发送了信息。嵌入的ID可以用来执行授权。只有拥有与主题中的ID相同的客户ID的客户才被允许发布到该主题。例如,一个拥有client1 ID的客户端被允许发布到client1/status,但不允许发布到client2/status。
有时,有必要订阅所有通过中介传输的消息。例如,要把所有消息持久化到数据库中。不要通过使用MQTT客户端和订阅多级通配符来订阅经纪人上的所有消息。通常情况下,订阅的客户端无法处理这种方法产生的消息负载(特别是当你有大量的吞吐量时)。我们的建议是在MQTT代理中实现一个扩展。例如,通过HiveMQ的插件系统,你可以钩住HiveMQ的行为,添加一个异步例程来处理每个传入的消息并将其持久化到数据库中。
主题是一个灵活的概念,没有必要以任何方式预先分配它们。然而,发布者和订阅者都需要意识到主题的存在。重要的是,要考虑如何对主题进行扩展,以实现新的功能或产品。例如,如果你的智能家居解决方案增加了新的传感器,应该可以在不改变整个主题层次结构的情况下将这些传感器添加到你的主题树中。
当你命名主题时,不要用与队列中相同的方式来使用它们。尽可能地将你的主题区分开来。例如,如果你在客厅有三个传感器,为myhome/livingroom/temperature、myhome/livingroom/brightness和myhome/livingroom/humidity创建主题。不要通过myhome/livingroom发送所有数值。对所有信息使用一个主题是一种反模式。特定的命名也使你有可能使用其他MQTT功能,如保留消息。关于保留消息的更多信息,请看精华系列的第8部分。
服务质量(QoS)级别是消息的发送方和消息的接收方之间的协议,它定义了特定消息的交付保证。在MQTT中,有3个QoS级别:
当你谈论MQTT中的QoS时,你需要考虑消息传递的两个方面:
我们将分别研究消息传递的两个方面,因为两者之间存在着微妙的差异。向经纪人发布消息的客户在向经纪人发送消息时定义了消息的QoS级别。代理商使用每个订阅客户在订阅过程中定义的QoS级别,将该消息传送给订阅客户。如果订阅客户定义了比发布客户更低的QoS,经纪人就用较低的服务质量传送消息。
QoS是MQTT协议的一个关键特征。QoS使客户端有能力选择与其网络可靠性和应用逻辑相匹配的服务水平。由于MQTT管理消息的重新传输并保证交付(即使在底层传输不可靠的情况下),QoS使不可靠的网络中的通信变得容易得多。
让我们仔细看看每个QoS级别是如何在MQTT协议中实现的,以及它是如何发挥作用的。
6.1.3.1、QoS 0 - 最多一次
最小的QoS水平是零。这个服务水平保证了尽力而为的交付。没有交付的保证。收件人不确认收到消息,消息也不被发送者储存和重新发送。QoS 0级通常被称为 "发射和遗忘",并提供与基础TCP协议相同的保证。
6.1.3.2、QoS 1 - 至少一次
QoS级别1保证了消息至少被传递给接收者一次。发送方存储消息,直到它从接收方收到确认收到消息的PUBACK包。一个消息有可能被多次发送或交付。
发送方使用每个数据包中的数据包标识符来匹配PUBLISH数据包和相应的PUBACK数据包。如果发送方在合理的时间内没有收到PUBACK包,发送方会重新发送PUBLISH包。当一个接收者得到一个具有QoS 1的消息时,它可以立即处理它。例如,如果接收者是一个经纪人,经纪人将消息发送给所有订阅的客户,然后用一个PUBACK包来回复。
如果发布客户端再次发送消息,它会设置一个重复(DUP)标志。在QoS 1中,这个DUP标志只用于内部目的,不被经纪人或客户端处理。消息的接收者会发送一个PUBACK,不管DUP标志是什么。
6.1.3.3、QoS 2 - 正好一次
QoS 2是MQTT中最高级别的服务。这个级别保证每个消息只被预定的接收者收到一次。QoS 2是最安全和最慢的服务质量水平。该保证由发送方和接收方之间的至少两个请求/响应流(一个四部分的握手)提供。发送方和接收方使用原始PUBLISH消息的数据包标识符来协调消息的交付。
当接收方从发送方得到QoS 2 PUBLISH包时,它相应地处理发布消息,并以确认PUBLISH包的PUBREC包回复发送方。如果发送方没有收到接收方的PUBREC数据包,它就会再次发送带有重复(DUP)标志的PUBLISH数据包,直到收到确认。
一旦发送方收到接收方的PUBREC数据包,发送方就可以安全地丢弃最初的PUBLISH数据包。发送方存储来自接收方的PUBREC数据包,并以PUBREL数据包作为回应。
在接收方得到PUBREL数据包后,它可以丢弃所有存储的状态,用PUBCOMP数据包来回答(发送方收到PUBCOMP时也是如此)。在接收方完成处理并将PUBCOMP包发回给发送方之前,接收方会存储对原始PUBLISH包的包标识的引用。这一步很重要,可以避免第二次处理该消息。在发送方收到PUBCOMP数据包后,已发布消息的数据包标识符就可以重新使用了。
当QoS 2流程完成后,双方都确信消息已被送达,而且发送方也得到了送达的确认。
如果一个数据包在途中丢失,发送方有责任在合理的时间内重新发送消息。如果发送方是MQTT客户端或MQTT代理,这也同样适用。收件人有责任对每个命令消息作出相应的回应。
QoS的某些方面乍一看不是很明显。以下是使用QoS时需要记住的几件事。
正如我们已经提到的,发送(发布)消息的客户端和接收消息的客户端之间的QoS定义和水平是两回事。这两种交互的QoS水平也可以是不同的。向经纪人发送PUBLISH消息的客户定义了该消息的QoS。然而,当经纪人将消息传递给接收者(订阅者)时,经纪人使用接收者(订阅者)在订阅时定义的QoS。例如,客户A是消息的发送者。客户端B是消息的接收者。如果客户B以QoS 1订阅经纪商,客户A以QoS 2向经纪商发送消息,经纪商以QoS 1向客户B(接收者/订户)发送消息。该消息可以多次向客户B发送,因为QoS 1保证至少有一次消息的发送,并不阻止同一消息的多次发送。
MQTT用于QoS 1和QoS 2的数据包标识符在一个互动中的特定客户和经纪人之间是唯一的。这个标识符在所有客户之间不是唯一的。一旦流量完成,数据包标识符就可以重新使用。这种重复使用是数据包标识符不需要超过65535的原因。一个客户端可以在不完成交互的情况下发送超过这个数量的消息是不现实的。
我们经常被问及如何选择正确的QoS水平的建议。这里有一些准则,可以帮助你在决策过程中。适合你的QoS在很大程度上取决于你的使用情况。
6.2.3.1、使用 QoS 0 的情况
6.2.3.2、使用 QoS 1 的情况
你需要得到每一条消息,而且你的用例可以处理重复的消息。QoS 级别1 是最经常使用的服务级别,因为它保证消息至少到达一次,但允许多次交付。当然,你的应用程序必须容忍重复,并且能够相应地处理它们。
你无法承受 QoS 2 的开销。QoS 1 传递信息的速度比 QoS 2 快得多。
6.2.3.3、使用 QoS 2 的情况
对你的应用程序来说,准确接收一次所有的消息是至关重要的。如果重复传递会损害应用程序用户或订阅客户,通常是这种情况。要注意开销以及QoS 2 的交互需要更多时间来完成。
所有用 QoS 1 和 2 发送的消息都为离线客户端排队,直到客户端再次可用。然而,这种排队只有在客户端有持久会话的情况下才有可能。
为了接收来自MQTT代理的消息,客户端连接到代理,并创建它感兴趣的主题的订阅。如果在非持久会话中,客户端和代理之间的连接被中断,这些主题就会丢失,客户端需要在重新连接时再次订阅。每次连接中断时,重新订阅对于资源有限的客户来说是一个负担。为了避免这个问题,客户端可以在连接到经纪人时请求一个持久化会话。持久会话在经纪人上保存所有与客户相关的信息。客户端在与经纪人建立连接时提供的clientId可以识别该会话(更多细节)。
在一个持久的会话中,经纪人会存储以下信息(即使客户端处于离线状态)。当客户端重新连接时,这些信息立即可用:
当客户端连接到代理服务器时,它可以请求一个持久化会话。客户端使用 cleanSession 标志来告诉经纪人它需要什么样的会话。
从MQTT 3.1.1开始,来自经纪人的CONNACK消息包含一个会话存在的标志。这个标志告诉客户端,以前建立的会话在代理上是否仍然可用。关于连接建立的更多信息,请看MQTT Essentials的第3部分。
与经纪人类似,每个MQTT客户端也必须存储一个持久的会话。当客户端请求服务器持有会话数据时,客户端负责存储以下信息:
这里有一些准则,可以帮助你决定何时使用持久化会话或清理会话。
人们经常问,经纪人把会话存储多长时间。简单的答案是。经纪人存储会话,直到客户重新上线并收到消息。然而,如果一个客户很长时间不回来在线,会发生什么?通常情况下,操作系统的内存限制是消息存储的主要限制。对于这种情况,没有标准答案。正确的解决方案取决于你的使用情况。
在MQTT中,发布消息的客户端不能保证订阅的客户端真的收到该消息。发布的客户端只能确保消息被安全地传递给代理。基本上,订阅的客户端也是如此。连接和订阅主题的客户端不能保证发布客户端何时在他们感兴趣的一个主题中发布消息。发布者可能需要几秒钟、几分钟或几个小时才能在某个订阅的主题中发送一条新消息。在下一条消息被发布之前,订阅的客户端完全不知道该主题的当前状态。这种情况就是保留消息发挥作用的地方。
保留的消息是一个正常的MQTT消息,保留的标志被设置为真。代理人存储最后一个保留消息和该主题的相应 QoS。每个订阅了与保留消息的主题相匹配的主题模式的客户,在他们订阅后立即收到保留消息。代理商对每个主题只存储一个保留信息。
如果订阅的客户在他们订阅的主题模式中包括通配符,即使保留信息的主题不是完全匹配的,它也会收到保留信息。下面是一个例子。客户端A发布了一条保留信息到myhome/livingroom/temperature。过了一段时间,客户B订阅了myhome/#。客户端B在订阅myhome/#后直接收到myhome/livingroom/temperature的保留信息。客户端B(订阅的客户端)可以看到该消息是一个保留的消息,因为经纪人在发送保留的消息时,保留的标志设置为真。客户端可以决定它要如何处理保留的消息。
保留消息有助于新订阅的客户在订阅一个主题后立即得到一个状态更新。保留的消息消除了对发布客户端发送下一次更新的等待。
换句话说,一个主题上的保留消息是最后一个已知的好值。保留的消息不一定是最后一个值,但它必须是保留标志设置为真的最后一个消息。
重要的是要理解,保留的消息与持久性会话无关(我们上周已经讨论过这个问题)。一旦经纪人存储了保留的信息,就只有一种方法可以删除它。继续阅读以了解如何。
从开发者的角度来看,发送保留信息是非常简单和直接的。你只需将MQTT发布消息的保留标志设置为真。一般来说,你的客户端库提供了一个简单的方法来设置这个标志。
还有一个非常简单的方法来删除一个主题的保留消息:在你想删除之前的保留消息的主题上,发送一个有效载荷为零字节的保留消息。经纪人删除保留的信息,新的订阅者不再得到该主题的保留信息。通常情况下,甚至没有必要删除,因为每条新的保留信息都会覆盖以前的信息。
当你想让新连接的订阅者立即收到消息时,保留消息是有意义的(不用等到发布客户端发送下一条消息)。这对于组件或设备在单个主题上的状态更新是非常有帮助的。例如,设备1的状态是在主题myhome/devices/device1/status上。当使用保留消息时,主题的新订阅者在订阅后立即得到设备的状态(在线/离线)。对于以间隔、温度、GPS坐标和其他数据发送数据的客户端也是如此。如果没有保留信息,新的订阅者在发布间隔期间就会被蒙在鼓里。使用保留信息有助于立即向连接的客户端提供最后的好值。
因为MQTT经常被用于包括不可靠的网络的场景中,所以我们有理由认为,这些场景中的一些MQTT客户端偶尔会不体面地断开连接。非优雅地断开连接可能是由于失去连接、空电池或许多其他原因而发生。知道客户端是优雅地断开(有MQTT DISCONNECT消息)还是不优雅地断开(没有断开消息),有助于你做出正确的反应。最后的遗嘱功能为客户端提供了一种方法,以适当的方式回应非优雅的断开连接。
在MQTT中,你可以使用 "最后的遗嘱"(LWT)功能来通知其他客户关于一个不体面地断开连接的客户。每个客户端在连接到代理服务器时都可以指定它的最后遗嘱信息。最后的遗嘱消息是一个正常的MQTT消息,有一个主题、保留的消息标志、QoS和有效载荷。代理商存储该消息,直到它检测到客户端不体面地断开连接。作为对非优雅断开连接的回应,经纪人向所有订阅了最后一次消息主题的客户发送最后一次消息。如果客户以正确的DISCONNECT消息优雅地断开连接,经纪人会丢弃存储的LWT消息。
LWT帮助你在客户端的连接断开时实现各种策略(或者至少通知其他客户端离线状态)。
客户端可以在启动客户端和代理之间的连接的CONNECT消息中指定一个LWT消息。
根据MQTT 3.1.1规范,经纪人必须在以下情况下分发客户端的LWT:
LWT是一个很好的方法来通知其他订阅的客户端关于另一个客户端意外失去连接的情况。在现实世界的场景中,LWT经常与保留消息相结合,以存储一个客户端在特定主题上的状态。例如,client1首先向经纪商发送一个CONNECT消息,其lastWillMessage的有效载荷为 "Offline",lastWillRetain标志设置为true,lastWillTopic设置为client1/status。接下来,客户端向同一主题(client1/status)发送一个PUBLISH消息,其有效载荷为 "Online",保留标志设置为true。只要client1保持连接,新订阅client1/status主题的客户就会收到 "在线 "的保留消息。如果client1意外断开连接,经纪人就会发布有效载荷为 "离线 "的LWT消息作为新的保留消息。当client1离线时,订阅该主题的客户会收到经纪商的LWT保留消息("离线")。这种保留消息的模式使其他客户能够及时了解客户1在特定主题上的当前状态。
MQTT是基于传输控制协议(TCP)的。该协议确保数据包在互联网上以 "可靠、有序和检查错误 "的方式传输。尽管如此,通信各方之间的传输不时地会出现不同步的情况。例如,如果其中一方崩溃或出现传输错误。在TCP中,这种不完全连接的状态被称为半开放连接。需要记住的一点是,通信的一方继续运作,不会被通知另一方的故障。仍在连接的一方继续尝试发送消息,并等待确认。
正如Andy Stanford-Clark(MQTT协议的发明者)所指出的,半开放连接的问题在移动网络中会增加:
"虽然理论上TCP/IP会在套接字断开时通知你,但在实践中,特别是在像移动和卫星链路上,它们经常在空中 "伪造 "TCP,并在每一端重新加上报头,TCP会话很有可能出现 "黑洞",即它看起来仍然是开放的,但实际上只是把你写给它的东西倾倒在地上。"
MQTT包括一个 Keep Alive 功能,为半开放连接的问题提供了一个解决方法(或者至少使评估连接是否仍然开放成为可能)。
Keep Alive 确保经纪人和客户之间的连接仍然是开放的,并且经纪人和客户都知道正在连接。当客户机与经纪人建立连接时,客户机向经纪人传达一个以秒为单位的时间间隔。这个时间间隔定义了经纪人和客户可能不互相通信的最大时间长度。
MQTT 规范中说了以下内容:
"Keep Alive......是指从客户完成一个控制包的发送到开始发送下一个控制包之间所允许的最大时间间隔。客户端有责任确保发送控制包的时间间隔不超过Keep Alive值。在没有发送任何其他控制包的情况下,客户端必须发送一个PINGREQ包。"
只要消息被频繁地交换,并且不超过保持间隔,就没有必要发送额外的消息来得知连接是否仍然开放。
如果客户端在保持通话期间没有发送消息,它必须向经纪人发送一个PINGREQ包,以确认它是可用的,并确保经纪人也仍然可用。
如果客户在保持生存间隔的1.5倍内没有发送消息或PINGREQ包,经纪商必须断开其连接。同样地,如果客户在合理的时间内没有收到经纪商的响应,客户也应该关闭连接。
让我们仔细看看 Keep Alive 信息。 Keep Alive 功能使用两个数据包。
10.1.1.1、PINGREQ
PINGREQ是由客户机发送的,它向经纪人表明客户机仍然活着。如果客户没有发送任何其他类型的数据包(例如,PUBLISH或SUBSCRIBE数据包),客户必须向经纪人发送一个PINGREQ数据包。客户端可以在任何时候发送PINGREQ包,以确认网络连接仍然有效。PINGREQ数据包不包含有效载荷。
10.1.1.2、PINGRESP
当经纪人收到PINGREQ数据包时,经纪人必须用PINGRESP数据包来回复,以显示客户机它仍然可用。PINGRESP数据包也不包含有效载荷。
通常情况下,一个断开连接的客户试图重新连接。有时,经纪人对客户端仍有一个半开放的连接。当半开放连接发生时,而MQTT代理仍将其视为在线的客户端,重新连接并执行客户端接管。经纪人然后关闭之前与同一个客户端的连接(由客户端标识符决定),并与该客户端建立一个新的连接。这种行为保证了半开放的连接不会阻止断开连接的客户重新建立连接。
MQTT是一个发布/订阅协议,它是轻量级的,需要最小的足迹和带宽来连接一个物联网设备。与HTTP的请求/响应模式不同,MQTT是事件驱动的,能够将消息推送给客户端。这种类型的架构将客户端相互解耦,以实现一个高度可扩展的解决方案,在数据生产者和数据消费者之间没有依赖性。
MQTT的主要优点是:
MQTT被用于许多行业和应用中。HiveMQ已经发布了许多行业的案例,如汽车(宝马)、电信(Liberty Global)、能源(Fortum)、公共安全(Hytera)、互联产品(Awair、Matternet)。在这里阅读我们客户的所有故事。
MQTT的核心是MQTT代理和MQTT客户端。代理商负责在发送方和合法的接收方之间调度消息。一个MQTT客户端向经纪人发布消息,其他客户端可以订阅经纪人以接收消息。每个MQTT消息都包括一个主题。客户端向一个特定的主题发布消息,而MQTT客户端订阅他们想要接收的主题。MQTT代理使用主题和订阅者列表将消息分派给适当的客户。
MQTT代理能够缓冲那些不能被派发到没有连接的MQTT客户端的消息。这在网络连接不可靠的情况下变得非常有用。为了支持可靠的消息传递,该协议支持3种不同类型的服务质量消息。0--最多一次,1--至少一次,以及2--正好一次。
该规范有两个版本。MQTT 3.1.1和MQTT 5。大多数商业MQTT经纪商现在支持MQTT 5,但许多物联网管理云服务只支持MQTT 3.1.1。我们强烈建议新的物联网部署使用版本5,因为新的功能侧重于更强大的系统和云原生可扩展性。
对于协议的更深入的描述,我们建议你阅读MQTT Essentials系列文章或回顾配套的视频系列。MQTT 5精华系列文章还对MQTT 5的具体功能进行了深入的介绍。
有许多开源的客户端可以用各种编程语言来使用。HiveMQ提供了用Java开发的HiveMQ MQTT客户端。Eclipse Paho也提供了C/C++、Python和其他各种实现方式。客户端的详细列表可以在mqtt.org找到。
MQTT经纪商提供开源、商业实现和管理云服务。HiveMQ提供了两个商业版本。HiveMQ专业版和HiveMQ企业版,一个可管理的云MQTT服务。HiveMQ云,以及一个开源版本。HiveMQ社区版。详细的经纪商列表可以在mqtt.org找到。
为了更好地说明MQTT的工作原理,我们介绍了一个使用HiveMQ Cloud的简单实施案例。要在一个实际的集群上测试这个实现,你首先需要注册HiveMQ Cloud免费计划,它允许你免费连接100个物联网设备。此外,注册时不需要信用卡。一旦你注册并登录到HiveMQ Cloud,你的免费HiveMQ Cloud Free集群就会运行并准备好。如果您是第一次使用HiveMQ Cloud,HiveMQ Cloud会自动将您重定向到集群的管理视图中的入门部分。下面的例子是在Java 11上测试的。
在这个例子中,我们将有一个温度和亮度传感器连接到Raspberry Pi上,它将把传感器数据发送到一个MQTT代理。另一个设备将运行一个控制中心,接收MQTT数据。
第一步是创建发布传感器数据的MQTT客户端。在这个例子中,我们将使用一个温度计和亮度传感器。我们还将使用HiveMQ Cloud作为MQTT代理。在你注册了HiveMQ Cloud之后,在你的集群的 "概述 "选项卡上导航到 "详细信息 "部分。在那里你会发现你的主机名。复制这个主机名,在下面的示例代码段中替换它。
为了将MQTT客户端安全地连接到你的集群,你必须为集群创建MQTT凭证。切换到HiveMQ Cloud Basic集群的访问管理选项卡,定义你的MQTT凭证并选择+添加。这些也是你必须在代码中替换为"
public class Sensor {
public static void main(String[] args) throws InterruptedException {
final String host = ""; // use your host-name, it should look like '.s2.eu.hivemq.cloud'
final String username = ""; // your credentials
final String password = "";
// 1. create the client
final Mqtt5Client client = Mqtt5Client.builder()
.identifier("sensor-" + getMacAddress()) // use a unique identifier
.serverHost(host)
.automaticReconnectWithDefaultConfig() // the client automatically reconnects
.serverPort(8883) // this is the port of your cluster, for mqtt it is the default port 8883
.sslWithDefaultConfig() // establish a secured connection to HiveMQ Cloud using TLS
.build();
// 2. connect the client
client.toBlocking().connectWith()
.simpleAuth() // using authentication, which is required for a secure connection
.username(username) // use the username and password you just created
.password(password.getBytes(StandardCharsets.UTF_8))
.applySimpleAuth()
.willPublish() // the last message, before the client disconnects
.topic("home/will")
.payload("sensor gone".getBytes())
.applyWillPublish()
.send();
// 3. simulate periodic publishing of sensor data
while (true) {
client.toBlocking().publishWith()
.topic("home/brightness")
.payload(getBrightness())
.send();
TimeUnit.MILLISECONDS.sleep(500);
client.toBlocking().publishWith()
.topic("home/temperature")
.payload(getTemperature())
.send();
TimeUnit.MILLISECONDS.sleep(500);
}
}
private static byte[] getBrightness() {
// simulate a brightness sensor with values between 1000lux and 10000lux
final int brightness = ThreadLocalRandom.current().nextInt(1_000, 10_000);
return (brightness + "lux").getBytes(StandardCharsets.UTF_8);
}
private static byte[] getTemperature() {
// simulate a temperature sensor with values between 20°C and 30°C
final int temperature = ThreadLocalRandom.current().nextInt(20, 30);
return (temperature + "°C").getBytes(StandardCharsets.UTF_8);
}
}
上面的代码片段做了以下工作:
下一步是实现订阅客户端,它消耗主题home/temperature和home/brightness上的值。
public class ControlCenter {
public static void main(String[] args) {
final String host = ""; // use your host-name, it should look like '.s2.eu.hivemq.cloud'
final String username = ""; // your credentials
final String password = "";
// 1. create the client
final Mqtt5Client client = Mqtt5Client.builder()
.identifier("controlcenter-" + getMacAddress()) // use a unique identifier
.serverHost(host)
.automaticReconnectWithDefaultConfig() // the client automatically reconnects
.serverPort(8883) // this is the port of your cluster, for mqtt it is the default port 8883
.sslWithDefaultConfig() // establish a secured connection to HiveMQ Cloud using TLS
.build();
// 2. connect the client
client.toBlocking().connectWith()
.simpleAuth() // using authentication, which is required for a secure connection
.username(username) // use the username and password you just created
.password(password.getBytes(StandardCharsets.UTF_8))
.applySimpleAuth()
.cleanStart(false)
.sessionExpiryInterval(TimeUnit.HOURS.toSeconds(1)) // buffer messages
.send();
// 3. subscribe and consume messages
client.toAsync().subscribeWith()
.topicFilter("home/#")
.callback(publish -> {
System.out.println("Received message on topic " + publish.getTopic() + ": " +
new String(publish.getPayloadAsBytes(), StandardCharsets.UTF_8));
})
.send();
}
}
上面的代码片段做了以下工作:
现在你对MQTT有了一个很好的介绍,我们建议如下: