文章目录
- Basics
- 一、简介
- 二、Publish & Subscribe
- 三、MQTT客户端和代理、MQTT服务器和连接建立解释
- 四、MQTT发布、订阅、取消订阅
- 五、MQTT Topics & Best Practices
- Features
- 六、Quality of Service 0,1 & 2
- 七、Persistent Session and Queuing
- 八、Retained Messages
- 九、Last Will and Testament
- 十、Keep Alive and Client Take-Over
声明:本文翻译自MQTT[官网]( Getting started (mqtt.org))Getting started板块,仅供学习使用, 由于是使用软件翻译会有翻译错误,遇到错误或者问题建议查看原文和原文博客的评论,每一章会给出原文链接。
Basics
一、简介
MQTT是客户机服务器发布/订阅消息传递传输协议。它重量轻,开放,简单,易于实现。这些特性使其非常适合在许多情况下使用,包括受限环境,如需要占用少量代码和/或网络带宽非常昂贵的机器到机器(M2M)和物联网(IoT)环境中的通信。
来自官方MQTT 规范的引用
一些历史
MQTT协议是1999年由Andy Stanford-Clark (IBM)和Arlen Nipper (Arcom,现在的Cirrus Link)发明的。在接下来的十年中,IBM内部一直使用该协议,直到2010年他们发布了MQTT 3.1作为免版税版本。
MQTT不是传统的消息队列解决方案,尽管在某些情况下可以对消息进行队列。
2019年3月,OASIS 批准了新的MQTT 5规范。这个新的MQTT版本为MQTT引入了一些新特性,这些特性是部署在云平台上的物联网应用程序所需的,以及那些需要更高的可靠性和错误处理才能实现关键任务消息传递的应用程序所需的。
参考资料
Introducing the MQTT Protocol - MQTT Essentials: Part 1 (hivemq.com)
二、Publish & Subscribe
The publish/subscribe pattern
发布/订阅模型将发送消息的客户端(发布者)与接收消息的客户端(订阅者)解耦。发布者和订阅者之间从不直接联系。事实上,他们甚至没有意识到对方的存在。它们之间的连接由第三个组件(代理)处理。
Pub / sub的最重要方面是来自收件人(订阅者)的消息的发布者的解耦。这种去耦有几个维度:
空间解耦:发布者和订阅者不需要相互了解(例如,没有IP地址和端口的交换)。
时间解耦:发布者和订阅者不需要同时运行。
同步解耦:在发布或接收期间,两个组件的操作不需要中断。
总之,发布/订阅模型消除了消息的发布者和接收方/订阅者之间的直接通信。代理的过滤活动可以控制哪个客户机/订阅者接收哪个消息。解耦具有三个维度:空间、时间和同步。
Message filtering
Broker在Pub / sub流程中发挥着关键作用。但是,Broker如何进行过滤所有消息,以便每个订户只接收感兴趣的消息?Broker有几种过滤选项:
OPTION 1: SUBJECT-BASED FILTERING
此过滤基于每个消息的课题或主题。接收客户订阅代理的兴趣主题。从该点开始,代理可确保接收客户端获取发布到订阅主题的所有消息。通常,主题是具有分层结构的字符串,其允许基于有限数量的表达式进行过滤。
OPTION 2: CONTENT-BASED FILTERING
在基于内容的滤波中,代理基于特定内容过滤器语言过滤消息。接收客户端订阅过滤器查询他们感兴趣的消息。此方法的重要缺点是必须预先知道消息的内容,并且不能加密或容易地改变。
OPTION 3: TYPE-BASED FILTERING
使用面向对象的语言时,基于消息的类型/类(事件)的筛选是常见的做法。例如,订户可以收听所有消息,这些消息是类型异常或任何子类型。
当然,发布/订阅不是每个用例的答案。在使用此模型之前,需要考虑一些事情。发布者和订阅者的解耦,这是PUB / SUB中的关键,呈现了几个挑战。例如,需要了解已发布的数据是如何事先构建的。对于基于主题的过滤,发布者和订阅者都需要知道要使用的主题。要记住的另一件事是消息传递。发布者不能假设某人正在聆听发送的消息。在某些情况下,没有用户读取特定消息。
MQTT
MQTT体现了所提到的PUB / SUB的所有方面:
-
MQTT在空间上分离发布者和订阅者。要发布或接收消息,发布者和订阅者只需要知道代理的主机名/ IP和端口。
-
MQTT在时间上解耦。虽然大多数MQTT用例近期提供消息,但如果需要,代理可以为不在线的客户端存储消息。(必须满足两个条件来存储消息:客户端已连接到持久性会话,并订阅具有大于0的服务质量的主题。
-
MQTT异步工作。由于大多数客户端库都异步工作,并且基于回调或类似的模型,因此在等待消息或发布消息时不会阻止任务。在某些用例中,在某些用例中,同步是可取的,也是可能的。要等待某个消息,某些库具有同步API。但流程通常是异步的。
MQTT在客户端尤其容易使用。
MQTT使用基于主题的消息过滤。每个消息都包含一个主题(subject),代理可以使用该主题来确定订阅客户端是否获得消息。请参阅MQTT Essentials的第5部分,以了解更多关于主题的概念。如果需要,还可以使用HiveMQ MQTT代理和我们的自定义扩展系统设置基于内容的过滤。
为了处理Pub / sub系统的挑战,MQTT有三种服务质量(QoS)水平。
Distinction from message queues
与消息队列的区别
-
消息队列存储消息,直到它们被消耗为止每个传入的消息都存储在队列中,直到客户端被客户端拾取(通常称为消费者)。如果没有客户端拾取消息,则该消息仍然存在于队列中并等待消耗。在消息队列中,消息不可能不被任何客户机处理,因为在MQTT中,如果没有人订阅某个主题。
-
消息只能由一个客户端消耗。在传统的消息队列中,消息只能由一个客户端处理。负载在队列的所有使用者之间分配。在MQTT中,行为完全相反:订阅主题的每个订阅者都得到消息。
-
队列被命名并必须显式创建。队列比主题严格得多。在使用队列之前,必须使用单独的命令显式地创建队列。只有在队列被命名和创建之后,才有可能发布或使用消息。相比之下,MQTT主题topics非常灵活,可以动态创建。
总结
-
发布/订阅解耦了接收消息的客户端(订阅者)和发送消息的客户端(发行者)。
-
MQTT使用消息的主题(topic/subject)来确定哪个消息发送到哪个客户机(订阅者)。主题是一个层次结构的字符串,可用于过滤和路由消息
参考资料
Publish & Subscribe - MQTT Essentials: Part 2 (hivemq.com)
三、MQTT客户端和代理、MQTT服务器和连接建立解释
Client
当我们谈到客户机时,我们几乎总是指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。
Broker
MQTT客户机的对应方是MQTT代理。代理是任何发布/订阅协议的核心。根据实现的不同,代理可以处理多达数百万个并发连接的MQTT客户机。
代理负责接收所有消息、过滤消息、确定谁订阅了每个消息,并将消息发送给这些订阅的客户端。代理还保存所有具有持久会话的客户机的会话数据,包括订阅和错过的消息。
代理的另一个职责是客户端的身份验证和授权。通常,代理是可扩展的,这方便了自定义身份验证、授权以及与后端系统的集成。集成特别重要,因为代理通常是直接暴露在互联网上的组件,处理大量客户端,需要将消息传递给下游的分析和处理系统。订阅所有的消息并不是一个真正的选择。简而言之,代理是每个消息都必须通过的中心枢纽。因此,重要的是代理具有高度的可伸缩性、可集成到后端系统、易于监控以及(当然)抗故障性能。HiveMQ通过使用最先进的事件驱动网络处理、开放扩展系统和标准监控提供商来满足这些需求。
MQTT Connection
MQTT协议是基于TCP/IP的。客户端和代理都需要有一个TCP/IP堆栈。
MQTT连接总是在一个客户机和代理之间。客户端之间不直接连接。要初始化连接,客户端向代理发送CONNECT消息。代理响应一个CONNACK消息和一个状态码。连接建立后,代理将保持连接打开,直到客户端发送断开连接命令或连接断开。
MQTT connection through a NAT
在许多常见的用例中,MQTT客户机位于使用网络地址转换(NAT)将私有网络地址(like 192.168.x.x, 10.0.x.x) 转换为面向公共的地址的路由器后面。
正如我们已经提到的,MQTT客户机通过向代理发送CONNECT消息来发起连接。因为代理有一个公共地址,并保持连接打开,以允许双向发送和接收消息(在初始的CONNECT之后),所以位于NAT后面的客户端完全没有问题。
Client initiates connection with the CONNECT message
现在让我们看看MQTT CONNECT命令消息。为了发起连接,客户端向代理发送一条命令消息。如果这个CONNECT消息是畸形的(根据MQTT规范),或者在打开网络套接字和发送连接消息之间花费了太多的时间,代理将关闭连接。这种行为可以阻止恶意客户机降低代理的速度。一个良好的MQTT 3客户机发送一个包含以下内容(包括其他内容)的连接消息:
ClientId
客户机标识符(ClientId)标识连接到MQTT代理的每个MQTT客户机。代理使用ClientId来标识客户端和客户端当前状态。因此,这个Id对于每个客户端和代理应该是唯一的。在MQTT 3.1.1中,如果不需要由代理持有状态,可以发送空的ClientId。空的ClientId会产生一个没有任何状态的连接。在这种情况下,clean session标志必须设置为true,否则代理将拒绝连接。
Clean Session
clean session标志告诉代理客户端是否想要建立一个持久会话。在一个持久会话(CleanSession = false)中,代理存储客户端的所有订阅以及使用服务质量(QoS)级别1或2进行订阅的客户端的所有错过的消息。如果会话不是持久的(CleanSession = true),代理不为客户端存储任何内容,并清除以前任何持久会话中的所有信息。
Username/Password
MQTT可以发送用户名和密码用于客户端身份验证和授权。但是,如果该信息没有加密或散列(通过实现或TLS),密码将以纯文本发送。我们强烈建议用户名和密码与安全传输一起使用。像HiveMQ这样的代理可以使用SSL证书验证客户端,因此不需要用户名和密码。
Will Message
最后的遗嘱消息是MQTT的最后遗嘱(last will and Testament, LWT)特性的一部分。当客户端断开连接时,此消息通知其他客户端。当客户机连接时,它可以在CONNECT消息中以MQTT消息和主题的形式向代理提供最后的意愿。如果客户端不优雅地断开连接,代理代表客户端发送LWT消息。
KeepAlive
保持活动是一个时间间隔,以秒为单位,客户端指定并在连接建立时与代理通信。这个间隔定义了代理和客户端在不发送消息的情况下可以忍受的最长时间。客户端承诺定期向代理发送PING Request消息。代理响应一个PING响应。这个方法允许双方确定另一个是否仍然可用。
基本上,这就是从MQTT 3.1.1客户机连接到MQTT代理所需的所有信息。每个库通常都有额外的选项可以配置。例如,队列消息在特定实现中存储的方式。
Broker response with a CONNACK message
当代理接收到CONNECT消息时,它有义务响应一个CONNACK消息。CONNACK消息包含两个数据项:
- The session present flag(会话当前标志)
- A connect return code(一个连接返回码)
Session Present flag
会话呈现标志告诉客户端代理是否已经有一个可用于之前与客户端的交互的持久会话。当客户端连接Clean Session设置为true时,会话呈现标志总是false,因为没有可用的会话。如果客户端连接Clean Session设置为false,有两种可能:如果客户端有会话信息。并且代理存储了会话信息,会话呈现标志为true。否则,如果代理没有任何用于clientId的会话信息,会话呈现标志为false。这个标志是在MQTT 3.1.1中添加的,以帮助客户机确定它们是否需要订阅主题,或者主题是否仍然存储在持久会话中。
Connect return code
CONNACK消息中的第二个标志是连接确认标志。这个标志包含一个返回代码,告诉客户端连接尝试是否成功。
Return Code | Return Code Response |
---|---|
0 | Connection accepted |
1 | Connection refused, unacceptable protocol version |
2 | Connection refused, identifier rejected |
3 | Connection refused, server unavailable |
4 | Connection refused, bad user name or password |
5 | Connection refused, not authorized |
参考资料
四、MQTT发布、订阅、取消订阅
Publish
MQTT客户机可以在连接到代理时立即发布消息。MQTT利用代理上基于主题的消息过滤(有关详细信息,请参阅第2部分)。每个消息都必须包含一个主题,代理可以使用该主题将消息转发给感兴趣的客户端。通常,每个消息都有一个负载,其中包含要以字节格式传输的数据。MQTT是data-agnostic。客户端的用例决定了有效负载的结构。发送客户机(发布者)决定是否要发送二进制数据、文本数据,甚至是成熟的XML或JSON。
MQTT中的PUBLISH消息有几个我们想要详细讨论的属性:
Topic Name
主题名称是一个简单的字符串,具有分层结构,前向斜杠为分隔符。例如,“myhome/livingroom/temperature”或“Germany/Munich/Octoberfest/people”。有关主题的详细信息,请参阅MQTT Essentials的第5部分。
QoS
这个数字表示消息的服务质量等级(QoS)。有三个级别:0、1和2。服务级别决定了消息到达预期接收方(客户端或代理)需要什么样的保证。关于QoS的详细信息,请参见MQTT Essentials的第6部分。
Retain Flag
此标志定义代理是否将消息保存为指定主题的最后一个已知有效值。当一个新客户端订阅一个主题时,它们会收到该主题上保留的最后一条消息。有关保留消息的详细信息,请参阅MQTT Essentials的第8部分。
Payload
有效载荷这是消息的实际内容。MQTT 是数据不可知的。以任何编码方式发送图像、文本、加密数据和几乎所有二进制数据都是可能的。
Packet Identifier
数据包标识符在消息在客户机和代理之间流动时,数据包标识符唯一地标识消息。数据包标识符仅与大于零的 QoS 级别相关。客户机库和/或代理负责设置这个内部 MQTT 标识符。
DUP flag
DUP 标志该标志表示消息是重复的,并且由于预期的收件人(客户机或代理)没有确认原始消息而重发。这只适用于 QoS 大于0的情况。通常,重发/复制机制由 MQTT 客户机库或代理作为实现细节处理。有关更多信息,请参阅 MQTT Essentials 的第6部分。
当客户机将消息发送给 MQTT 代理以供发布时,代理将读取消息、确认消息(根据 QoS 级别)并处理消息。代理的处理包括确定哪些客户机已经订阅了主题并向它们发送消息。
最初发布消息的客户机只关心将 PUBLISH 消息传递给代理。一旦代理接收到 PUBLISH 消息,代理就有责任将消息传递给所有订阅者。发布客户机不会得到任何反馈,不知道是否有人对发布的消息感兴趣,或者有多少客户机从代理收到了消息。
Subscribe
如果没有人收到消息,那么发布消息是没有意义的。换句话说,如果没有客户端订阅消息的主题。要接收关于感兴趣主题的消息,客户机向 MQTT 代理发送 SUBSCRIBE 消息。这个订阅消息非常简单,它包含一个唯一的数据包标识符和一个订阅列表。
Packet Identifier
数据包标识符在消息在客户机和代理之间流动时,数据包标识符唯一地标识消息。客户机库和/或代理负责设置这个内部 MQTT 标识符。
List of Subscriptions
订阅列表 SUBSCRIBE 消息可以包含一个客户端的多个订阅。每个订阅由一个主题和一个 QoS 级别组成。订阅消息中的主题可以包含通配符,从而可以订阅主题模式而不是特定的主题。如果一个客户机存在重叠订阅,则代理将传递该主题具有最高 QoS 级别的消息。
Suback
订阅报文确认
为了确认每个订阅,代理向客户机发送一个 SUBACK 确认消息。此消息包含原始 Subscribe 消息的包标识符(用于清楚地标识消息)和返回代码列表。
Packet Identifier
数据包标识符数据包标识符是用于识别消息的唯一标识符。它与 SUBSCRIBE 消息中的内容相同。
Return Code
代理为它在 SUBSCRIBE 消息中接收到的每个主题/qos 对发送一个返回代码。例如,如果 SUBSCRIBE 消息有五个订阅,则 SUBACK 消息包含五个返回代码。返回代码确认每个主题,并显示代理授予的 QoS 级别。如果代理拒绝订阅,则 SUBACK 消息将意味着该特定主题的返回代码失败。例如,如果客户端没有足够的权限订阅主题,或者主题格式不正确。
Return Code | Return Code Response |
---|---|
0 | Success - Maximum QoS 0 |
1 | Success - Maximum QoS 1 |
2 | Success - Maximum QoS 2 |
128 | Failure |
在客户机成功地发送 SUBSCRIBE 消息并接收到 SUBACK 消息之后,它将获取与 SUBSCRIBE 消息所包含的订阅中的某个主题匹配的每个发布消息。
Unsubscribe
与SUBSCRIBE 消息对应的是 UNSUBSCRIBE 消息。此消息删除代理上客户端的现有订阅。UNSUBSCRIBE 消息类似于 SUBSCRIBE 消息,具有数据包标识符和主题列表。
Packet Identifier
数据包标识符在消息在客户机和代理之间流动时,数据包标识符唯一地标识消息。客户机库和/或代理负责设置这个内部 MQTT 标识符。
List of Topic
主题列表主题列表可以包含多个客户端希望取消订阅的主题。只需要发送主题(没有 QoS)。代理取消订阅主题,而不考虑它最初订阅时的 QoS 级别。
Unsuback
为了确认取消订阅,代理向客户端发送一个 UNSUBACK 确认消息。此消息仅包含原始 UNSUBSCRIBE 消息的数据包标识符(用于清楚地标识消息)。
Packet Identifier
包标识符包标识符唯一地标识消息。如前所述,这与 UNSUBSCRIBE 消息中的数据包标识符相同。
从代理接收 UNSUBACK 之后,客户机可以假设删除了 UNSUBSCRIBE 消息中的订阅。
参考资料
MQTT Publish, Subscribe & Unsubscribe - MQTT Essentials: Part 4 (hivemq.com)
五、MQTT Topics & Best Practices
Topics
在 MQTT 中,单词 topic 指的是代理用来为每个连接的客户机过滤消息的 utf-8字符串。主题由一个或多个主题级别组成。每个主题级别由一个正斜杠(主题级别分隔符)分隔。
与消息队列相比,MQTT 主题非常轻量级。客户端在发布或订阅它之前不需要创建所需的主题。代理接受每个有效的主题,而不需要事先进行任何初始化。
这里有一些主题的例子:
myhome/groundfloor/livingroom/temperature
USA/California/San Francisco/Silicon Valley
5ff4a2ce-e485-40f4-826c-b1a5d81be9b6/status
Germany/Bavaria/car/2382340923453/latitude
注意,每个主题必须至少包含1个字符,并且主题字符串允许空白。主题区分大小写。例如,MyHome/Temperature 和 MyHome/Temperature 是两个不同的主题。此外,前斜杠本身就是一个有效的主题。
Wildcards(通配符)
当客户机订阅某个主题时,它可以订阅已发布消息的确切主题,也可以使用通配符同时订阅多个主题。通配符只能用于订阅主题,而不能用于发布消息。有两种不同类型的通配符:single-level and multi-level.
Single Level: +
顾名思义,单级通配符代替了一个主题级别。加号表示主题中的单级通配符。
如果主题包含任意字符串而不是通配符,则任何主题都使用单级通配符匹配主题。例如,订阅 myhome/ground/+/temperature 可以产生以下结果:
Multi Level:
多级通配符涵盖了许多主题级别。哈希符号表示主题中的多级通配符。为了让代理确定哪些主题匹配,多级通配符必须作为主题中的最后一个字符放置,并在前面加上一个正斜杠。
当客户端使用多级通配符订阅一个主题时,它会收到一个主题的所有消息,这些消息都是在通配符之前以模式开始的,不管这个主题有多长或多深。如果只将多级通配符指定为主题(#) ,则会接收发送到 MQTT 代理的所有消息。如果您期望高吞吐量,那么仅使用多级通配符的订阅就是一种反模式(请参阅下面的最佳实践)。
Topics beginning with $
通常,您可以随心所欲地命名 MQTT 主题。但是,有一个例外: 以 KaTeX parse error: Expected 'EOF', got '#' at position 28: …途。当您将多级通配符作为主题(#̲)进行订阅时,这些主题不属于订…- symbol 主题保留用于 MQTT 代理的内部统计信息。客户端不能向这些主题发布消息。目前,这些话题还没有官方的标准。通常,$SYS/用于以下所有信息,但代理实现各不相同。在 mqtt github wiki 中有一个关于 $SYS-topics 的建议。以下是一些例子:
$SYS/broker/clients/connected
$SYS/broker/clients/disconnected
$SYS/broker/clients/total
$SYS/broker/messages/sent
$SYS/broker/uptime
Summary
这些是 MQTT 消息主题的基本内容。如您所见,MQTT 主题是动态的,并且提供了很大的灵活性。在现实应用程序中使用通配符时,应该注意一些挑战。我们已经收集了在各种项目中与 MQTT 广泛合作所学到的最佳实践,并且始终愿意接受关于这些实践的建议或讨论。使用评论开始对话,让我们知道你最好的做法,或者你不同意我们的做法!
Best practices
Never use a leading forward slash
在 MQTT 中允许使用前导正斜杠。For example, /myhome/groundfloor/livingroom.。但是,前面的正斜杠引入了一个不必要的主题级别,前面的字符为零。零并没有提供任何好处,而且常常导致混乱。
Never use spaces in a topic
空格是每个程序员的天敌。当事情没有按照他们应该的方式发展时,空格使得阅读和调试主题变得更加困难。就像前面的斜杠一样,仅仅因为某些东西是允许的,并不意味着它就应该被使用。Utf-8有许多不同的空白类型,应该避免使用这些不常见的字符。
Keep the topic short and concise(简洁)
每个主题都包含在使用它的每条消息中。使你的主题尽可能简短和简洁。对于小型设备,每个字节的计数和主题的长度都有很大的影响。
Use only ASCII characters, avoid non printable characters
因为非 ascii utf-8字符经常显示不正确,所以很难找到与字符集相关的拼写错误或问题。除非绝对必要,否则我们建议避免在主题中使用非 ascii 字符。
Embed(嵌入) a unique identifier or the Client Id into the topic
在这个主题中包含出版客户端的唯一标识符/值是非常有帮助的。主题中的唯一标识符文档可以帮助您识别发送消息的人。嵌入的 ID 可用于强制授权。只有与主题中的 ID 相同的客户端才允许发布到该主题。例如,允许具有 client1 ID 的客户机发布到 client1/status,但不允许发布到 client2/status。
Don’t subscribe to
有时,需要订阅通过代理传输的所有消息。例如,将所有消息持久保存到数据库中。不要通过使用 MQTT 客户机和订阅多级通配符来订阅代理上的所有消息。通常,订阅客户端无法处理由此方法导致的消息负载(特别是在吞吐量很大的情况下)。我们的建议是在 MQTT 代理中实现一个扩展。例如,使用 HiveMQ 的插件系统,您可以挂接到 HiveMQ 的行为,并添加一个异步例程来处理每条传入的消息,并将其持久化到数据库中。
Don’t forget extensibility
主题是一个灵活的概念,没有必要以任何方式预先分配它们。但是,发布者和订阅者都需要知道主题。考虑如何扩展主题以允许新特性或产品,这一点很重要。例如,如果您的智能家居解决方案添加了新的传感器,那么应该可以在不改变整个主题层次结构的情况下将这些传感器添加到主题树中。
Use specific topics, not general ones
在命名主题时,不要像在队列中那样使用它们。尽可能多地区分你的话题。例如,如果你的客厅里有三个传感器,你可以为myhome/livingroom/temperature,myhome/livingroom/brightness and myhome/livingroom/humidity创建主题。不要把所有的值发送到我的家/客厅。对所有消息使用单一主题是一种反模式。特定的命名还可以让您使用其他MQTT特性,比如保留的消息。有关保留消息的更多信息,请参见Essentials系列的第8部分。
参考资料
MQTT Topics & Best Practices - MQTT Essentials: Part 5 (hivemq.com)
Features
六、Quality of Service 0,1 & 2
Quality of Service
What is Quality of Service?
服务质量(Quality of Service,QoS)级别是消息的发送方和消息的接收方之间的协议,它定义了对特定消息的传递的保证。MQTT 中有3个 QoS 级别:
- At most once 最多一次 (0)
- At least once 至少一次 (1)
- Exactly once 就一次 (2).
在 MQTT 中讨论 QoS 时,需要考虑消息传递的两个方面:
- Message delivery form the publishing client to the broker. 消息从发布客户端传递到代理
- Message delivery from the broker to the subscribing client. 从代理到订阅客户端的消息传递
我们将分别研究消息传递的两个方面,因为两者之间存在细微的差异。向代理发布消息的客户端在将消息发送给代理时定义了消息的QoS级别。代理使用每个订阅客户机在订阅过程中定义的QoS级别将此消息发送给订阅客户机。如果订阅客户端定义的QoS低于发布客户端,则代理以较低的服务质量传输消息。
Why is Quality of Service important?
QoS 是 MQTT 协议的一个关键特性。服务质量给予客户端选择与其可靠性(计算机网络)和应用逻辑相匹配的服务水平的权力。由于 MQTT 管理消息的重新传输并保证传递(即使底层传输不可靠) ,QoS 使得不可靠网络中的通信变得容易得多。
How does it work?
让我们仔细看看 MQTT 协议中每个 QoS 级别是如何实现的,以及它是如何工作的:
QoS 0 - at most once
最小 QoS 级别为零。此服务级别保证了尽最大努力的交付。没有交货的保证。收件人没有确认收到邮件,邮件也没有被发件人存储和重新传输。服务质量等级0通常被称为“失效和遗忘”,它提供了与底层 TCP 协议相同的保证。
QoS 1 - at least once
QoS 等级1保证消息至少传递一次给接收方。发送方将消息存储起来,直到从接收方获得一个确认收到消息的 putback 包。一条消息可以被多次发送或传递。
发送端使用报文中的报文标识符,将PUBLISH报文与相应的PUBACK报文进行匹配。如果发送方在合理的时间内没有收到PUBACK包,发送方就会重新发送PUBLISH包。当接收方收到QoS为1的消息时,它可以立即处理它。例如,如果接收方是一个代理,代理将消息发送给所有订阅的客户端,然后用一个PUBACK包进行应答。
如果发布客户端再次发送消息,它将设置一个重复(DUP)标志。在QoS 1中,这个DUP标志仅用于内部目的,不由代理或客户端处理。无论DUP标志是什么,消息的接收者都会发送一个PUBACK。
QoS 2 - exactly once
QoS 2是 MQTT 中的最高服务级别。此级别保证每个消息只被预期的收件人接收一次。QoS 2是最安全、最慢的服务质量等级。保证由发送方和接收方之间的至少两个请求/响应流(四次握手)提供。发送方和接收方使用原始 PUBLISH 消息的包标识符来协调消息的传递。
当接收方从发送方获得一个QoS 2的PUBLISH包时,它会相应地处理这个PUBLISH消息,并用一个承认这个PUBLISH包的PUBREC包回应发送方。如果发送方没有从接收方收到PUBREC包,它会再次发送带有重复(DUP)标记的PUBLISH包,直到收到确认。
一旦发送方从接收方接收到 PUBREC 数据包,发送方就可以安全地丢弃初始 PUBLISH 数据包。发送方存储来自接收方的 PUBREC 数据包,并用一个 PUBREL 数据包进行响应。
在接收方收到PUBREL数据包后,它可以丢弃所有存储的状态并使用一个PUBCOMP数据包进行应答(当发送方收到PUBCOMP数据包时也是如此)。在接收方完成处理并将PUBCOMP包发送回发送方之前,接收方存储对原始PUBLISH包的包标识符的引用。此步骤对于避免第二次处理消息非常重要。发送方收到PUBCOMP包后,发布消息的包标识符就可以重用了。
当 QoS 2流完成时,双方都确认消息已经传递,发送方已经确认传递。
如果数据包在传输过程中丢失,发送方有责任在合理的时间内重新传输消息。如果发送方是 MQTT 客户机或 MQTT 代理,也是如此。接收者有责任相应地响应每个命令消息。
Good to know
服务质量的某些方面乍一看并不明显。当你使用 QoS 时,请记住以下几点:
Downgrade of QoS
正如我们已经提到的,发送(发布)消息的客户端和接收消息的客户端之间的QoS定义和级别是两个不同的东西。这两个交互的QoS级别也可以不同。将PUBLISH消息发送给代理的客户端定义了消息的QoS。但是,当代理将消息传递给收件人(订阅者)时,代理使用收件人(订阅者)在订阅期间定义的QoS。例如,客户端A是消息的发送方。客户端B是消息的接收者。如果客户端B订阅了服务质量为1的代理,客户端A将消息发送给服务质量为2的代理,则服务质量为1的客户端B(即接收/订阅方)将消息发送给服务质量为1的代理。消息可以多次传递给客户端B,因为QoS 1保证消息至少传递一次,并且不阻止同一消息的多次传递。
Packet identifiers are unique per client
MQTT 用于 QoS 1和 QoS 2的数据包标识符在交互中的特定客户端和代理之间是唯一的。此标识符在所有客户端之间不是唯一的。一旦流完成,数据包标识符就可以重用了。这种重用是数据包标识符不需要超过65535的原因。客户端在不完成交互的情况下发送超过这个数量的消息是不现实的。
Best Practice
我们经常被问到如何选择正确的 QoS 级别。这里有一些指导方针,可以帮助你做决定的过程。适合您的 QoS 在很大程度上取决于您的用例。
Use QoS 0 when …
-
有一个完全或大部分稳定的连接。QoS 0的一个经典用例是通过有线连接将测试客户机或前端应用程序连接到 MQTT 代理
-
你不介意偶尔丢失一些信息。如果数据不是那么重要,或者数据以较短的间隔发送,则可以接受某些消息的丢失
-
您不需要消息队列。只有当客户端有QoS 1或QoS 2和持久会话时,消息才会排队。
Use QoS 1 when …
- 需要获得每条消息,并且您的用例可以处理重复消息。QoS 级别1是最常用的服务级别,因为它保证消息至少到达一次,但允许多次传送。当然,您的应用程序必须容忍重复,并能够相应地处理它们
- 你无法承受 QoS 2的开销。 QoS 1传递消息的速度比 QoS 2快得多
Use QoS 2 when …
- 对于应用程序来说,一次性接收所有消息是非常重要的。如果重复交付可能会损害应用程序用户或订阅客户机,通常就会出现这种情况。请注意开销和 QoS 2交互需要更多的时间才能完成
Queuing of QoS 1 and 2 messages
所有用 QoS 1和2发送的消息都会在离线客户端中排队,直到客户端再次可用。但是,只有在客户端具有持久会话的情况下才可能进行此队列。
参考资料
Quality of Service 0,1 & 2 - MQTT Essentials: Part 6 (hivemq.com)
七、Persistent Session and Queuing
Persistent Session
要从 MQTT 代理接收消息,客户机连接到代理并创建对其感兴趣的主题的订阅。如果客户机和代理之间的连接在非持久性会话期间中断,则这些主题将丢失,并且客户机需要在重新连接时再次订阅。对于资源有限的受限客户机来说,每次连接中断时重新订阅都是一种负担。为了避免这个问题,客户机可以在连接到代理时请求一个持久会话。持久化会话保存与代理上的客户机相关的所有信息。客户机在建立到代理的连接时提供的 clientId 标识会话。
What’s stored in a persistent session?
在持久化会话中,代理存储以下信息(即使客户端脱机)。当客户端重新连接时,信息立即可用。
- 客户端的所有订阅
- 服务质量(QoS) 1或2流中客户端尚未确认的所有消息。
- 客户端离线时丢失的所有新的 QoS 1或2消息
- 从客户端接收的所有尚未完全确认的 QoS 2消息
How do you start or end a persistent session?
当客户机连接到代理时,它可以请求一个持久会话。客户端使用一个 cleanession 标志告诉代理它需要什么样的会话:
- 当干净的会话标志设置为 true 时,客户端不想要持久的会话。如果客户端由于任何原因断开连接,则前一个持久会话中排队的所有信息和消息都将丢失
- 当 clean session 标志设置为 false 时,代理将为客户机创建一个持久化会话。保留所有信息和消息,直到客户端下次请求一个干净的会话。如果将 clean session 标志设置为 false,并且代理已经为客户端提供了一个会话,那么它将使用现有的会话并将以前排队的消息传递给客户端
有关客户机和代理之间建立连接的更多信息,请参见 mqttessentials 的第3部分。
How does the client know if a session is already stored?
由于 MQTT 3.1.1,来自代理的 CONNACK 消息包含一个 session present 标志。这个标志告诉客户机以前建立的会话在代理上是否仍然可用。有关连接建立的更多信息,请参见 MQTT Essentials 的第3部分。
Persistent session on the client side
与代理类似,每个 MQTT 客户机还必须存储一个持久性会话。当客户端请求服务器保存会话数据时,客户端负责存储以下信息:
- QoS 1或2流中尚未由代理确认的所有消息
- 从代理接收的所有尚未完全确认的 QoS 2消息
Best practices
下面是一些指导原则,可以帮助您决定何时使用持久会话或清除会话
Persistent Session
-
客户端必须从某个主题获取所有消息,即使该主题处于脱机状态。您希望代理为客户端对消息进行排队,并在客户端恢复联机后立即发送消息
-
客户资源有限。您希望代理存储客户机的订阅信息并快速恢复中断的通信
-
客户端需要在重新连接后恢复所有的 qos1和 qos2发布消息
Clean session
- 客户端只需要向主题发布消息,客户端不需要订阅主题。您不希望代理存储会话信息或重试 QoS 1和2消息的传输
- 客户机不需要获取它在离线时错过的消息
How long does the broker store messages?
人们经常问代理存储会话的时间。简单的答案是: 代理存储会话,直到客户端回到在线并接收到消息。然而,如果一个客户很长时间没有回到线上会发生什么呢?通常,操作系统的内存限制是消息存储的主要约束。对于这种情况没有标准的答案。正确的解决方案取决于您的用例。
参考资料
Persistent Session and Queuing Messages - MQTT Essentials: Part 7 (hivemq.com)
八、Retained Messages
在 MQTT 中,发布消息的客户机不能保证订阅客户机实际接收消息。发布客户机只能确保消息安全地传递到代理。基本上,订阅客户端也是如此。连接和订阅主题的客户端不能保证发布客户端何时会在其感兴趣的主题中发布消息。发布者可能需要几秒钟、几分钟或几小时以订阅的主题之一发送新消息。在下一条消息发布之前,订阅客户端对主题的当前状态一无所知。这种情况是保留的消息发挥作用的地方。
Retained Messages
保留消息是将保留标志设置为true的普通MQTT消息。代理存储最后保留的消息和该主题对应的QoS。订阅与保留消息主题匹配的主题模式的每个客户端在订阅后都会立即收到保留消息。代理对每个主题只存储一条保留的消息。
如果订阅客户端在其订阅的主题模式中包含通配符,那么它将收到一条保留的消息,即使保留消息的主题不完全匹配。下面是一个示例: 客户端 a 将保留的消息发布myhome/livingroom/temperature。过了一段时间,客户 b 订阅了 myhome/# 。客户b在订阅myhome/# 后,立即收到myhome/livingroom/temperature保留讯息。客户端 b (订阅客户端)可以看到消息是一条保留消息,因为代理发送的保留消息将保留标志设置为 true。客户端可以决定如何处理保留的消息。
保留消息有助于新订阅的客户端在订阅主题后立即获得状态更新。保留的消息消除了发布客户端发送下一个更新的等待。
换句话说,关于某个主题的保留消息是最后一个已知的好价值。保留的消息不一定是最后一个值,但必须是保留标志设置为 true 的最后一个消息。
重要的是要理解,保留的消息与持久化会话无关(这是我们上周讨论的主题)。一旦保留的消息被代理存储,只有一种方法可以删除它。继续读下去,找出原因。
Send a retained message
从开发人员的角度来看,发送保留的消息是相当简单和直接的。您只需将 MQTT 发布消息的保留标志设置为 true。通常,客户机库提供了一种简单的方法来设置此标志。
Delete a retained message
还有一种非常简单的方法可以删除主题的保留消息: 在要删除以前保留消息的主题上发送一条保留消息,其负载为零字节。代理将删除保留消息,新订阅者将不再获得该主题的保留消息。通常,甚至不需要删除,因为每个新保留的消息都会覆盖前一个消息。
Why and when should you use Retained Messages?
当您希望新连接的订阅者立即接收消息(而不必等待发布客户端发送下一条消息)时,保留消息是有意义的。
当您希望新连接的订阅者立即接收消息(而不必等待发布客户端发送下一条消息)时,保留消息是有意义的。这对于组件或设备在单个主题上的状态更新非常有帮助。例如,device1的状态是关于 myhome/devices/device1/status 的主题。使用保留消息时,主题的新订阅者在订阅后立即获得设备的状态(联机/脱机)。对于按时间间隔、温度、 GPS 坐标和其他数据发送数据的客户机也是如此。如果没有保留消息,新订阅者将在发布间隔期间处于黑暗状态。使用保留的消息有助于立即向连接的客户机提供最后的良好价值。
参考资料
Retained Messages - MQTT Essentials: Part 8 (hivemq.com)
九、Last Will and Testament
由于 MQTT 通常用于包含不可靠网络的场景,因此可以合理地假设,这些场景中的一些 MQTT 客户机偶尔会不体面地断开连接。异常的断开连接发生的原因有,失去连接,电池没电或其他原因。了解客户端是否正确地断开连接(使用 mqttdisconnect 消息)或不正确地断开连接(不使用断开连接消息) ,有助于您正确地响应。最后的遗嘱和遗嘱功能为客户提供了一种以适当的方式应对不体面的中断的方法。
Last Will and Testament
在 MQTT 中,您可以使用最后遗嘱(LWT)特性通知其他客户机关于一个不合适的断开连接的客户机。每个客户机可以在连接到代理时指定其最后的 will 消息.最后一条 will 消息是一条带有主题、保留消息标志、 QoS 和有效负载的普通 MQTT 消息.代理存储消息,直到它检测到客户端不正常地断开连接。为了响应这种不雅的断开,代理将 last-will 消息发送给 last-will 消息主题的所有订阅的客户机。如果客户机使用正确的 DISCONNECT 消息正常地断开连接,则代理将丢弃存储的 LWT 消息。
当客户端连接断开时(或者至少通知其他客户端离线状态),LWT可以帮助您实现各种策略。
How do you specify a LWT message for a client?
客户端可以在CONNECT消息中指定一个LWT消息,该消息将启动客户端和代理之间的连接。
要了解关于如何建立客户机和代理之间的连接的更多信息,请参阅MQTT Essentials的第3部分。
When does a broker send the LWT message?
根据MQTT 3.1.1规范,在以下情况下,代理必须分发客户端的LWT:
-
代理检测到I/O错误或网络故障。
-
客户端在定义的保持存活时间内通信失败。
-
客户端在关闭网络连接之前不会发送一个DISCONNECT包。
-
代理由于协议错误关闭网络连接。
我们将在下一篇文章中听到更多关于Keep Alive时间的内容。
Best Practices - When should you use LWT?
LWT是一种很好的方法,可以在另一个客户端意外失去连接时通知其他已订阅的客户端。在实际场景中,LWT经常与保留的消息相结合,以存储特定主题上的客户机状态。例如,client1首先用lastWillMessage发送一个CONNECT消息给代理,lastWillMessage以“Offline”作为有效负载,lastWillRetain标志设置为true, lastWillTopic设置为client1/status。接下来,客户端发送一个PUBLISH消息,该消息的有效负载为“Online”,保留的标志设置为true,与相同的主题(client1/status)。只要client1保持连接,新订阅到client1/status主题的客户端就会收到“Online”保留消息。如果client1意外断开连接,代理将使用有效负载“Offline”发布LWT消息作为新的保留消息。当client1脱机时,订阅主题的客户机将从代理接收LWT保留的消息(“脱机”)。这种保留消息的模式使其他客户机能够及时了解client1在特定主题上的当前状态。
参考资料
Last Will and Testament - MQTT Essentials: Part 9 (hivemq.com)
十、Keep Alive and Client Take-Over
The problem of half-open TCP connections
MQTT基于传输控制协议(TCP)。该协议确保数据包在互联网上以“可靠、有序和错误检查”的方式传输。然而,有时候,交流双方之间的传输可能会不同步。例如,如果其中一方发生故障或传输错误。在 TCP 中,这种不完全连接的状态称为半开连接。需要记住的重要一点是,沟通的一方继续运作,而不会被告知另一方的失败。仍然连接的一方继续尝试发送消息并等待确认。正如 MQTT 协议发明者安迪•斯坦福-克拉克(Andy Stanford-Clark)所指出的,移动网络中半开放连接的问题日益严重:
’‘虽然理论上 TCP/IP 会在套接字中断时通知你,但实际上,特别是在移动和卫星链路上,这些通常是通过无线方式“伪造”TCP,并在每个端口重新设置报头,TCP 会话很有可能变成“黑洞”,也就是说它看起来仍然是开着的,但实际上它只是把你写到它上的东西丢到地板上。’’
MQTT Keep Alive
MQTT 包含一个 keep alive 函数,该函数为半开连接问题提供了一个解决方案(或者至少可以评估连接是否仍然处于开放状态)。
Keep alive 确保代理和客户机之间的连接仍然是开放的,并且代理和客户机都知道已经连接。当客户机建立到代理的连接时,客户机以秒为单位与代理进行通信。此间隔定义了代理和客户机不能相互通信的最大时间长度。
MQTT 规范说明如下:
“保持活力……是客户端结束发送一个控制包到开始发送下一个控制包之间允许的最大时间间隔。客户端负责确保发送控制报文的时间间隔不超过Keep Alive值。在没有发送任何其他控制包的情况下,客户端必须发送一个PINGREQ包。
只要频繁地交换消息并且不超过保持活动间隔,就不需要发送额外的消息来确定连接是否仍然打开。
如果客户端在保持活动期间没有发送消息,它必须向代理发送一个PINGREQ包,以确认它是可用的,并确保代理也仍然可用。
代理必须在保持活动间隔的1.5倍内断开未发送消息或PINGREQ包的客户端。同样,如果客户端在合理的时间内没有收到来自代理的响应,那么它将关闭连接。
Keep Alive Flow
让我们仔细看看“保持生命”的信息。keep alive特性使用两个数据包:
PINGREQ
PINGREQ由客户端发送,并向代理表明客户端仍然存在。如果客户端不发送任何其他类型的包(例如,PUBLISH或SUBSCRIBE包),客户端必须向代理发送一个PINGREQ包。客户端可以在任何时候发送一个PINGREQ包来确认网络连接是否仍然存在。PINGREQ报文不包含有效负载。
PINGRESP
当代理接收到一个PINGREQ数据包时,代理必须用一个PINGRESP数据包来回应,以向客户端表明它仍然可用。pingrespp数据包也不包含有效负载。
Good to Know
-
如果代理没有收到来自客户端的PINGREQ或任何其他数据包,代理将关闭连接并发送最后的遗嘱和遗嘱消息(如果客户端指定了一个LWT)。
-
MQTT客户机负责设置适当的保持活动值。例如,客户端可以根据其当前的信号强度调整保持活动间隔。
-
最长存活时间是18小时12分15秒。
-
如果保持存活时间间隔为0,则关闭保持存活机制。
Client Take-Over
通常,断开连接的客户端尝试重新连接。有时,代理仍然为客户端提供一个半打开的连接。在MQTT中,如果代理检测到一个半打开的连接,它将执行“客户端接管”。代理关闭到同一客户端的前一个连接(由客户端标识符确定),并与该客户端建立一个新连接。这种行为确保半开连接不会阻止断开的客户端重新建立连接。
参考资料
Keep Alive and Client Take-Over - MQTT Essentials Part 10 (hivemq.com)