0x00 写在前面
本文仅介绍MQTT与ROS两种协议的基本概念并做横向对比,不会涉及各个协议的高级特性,两种协议的本地搭建方式也会在下一篇文章中予以阐述,以下高级特性若读者有兴趣可以自行收集资料了解。(也不排除我后面会发相关文章,老鸽子了)
MQTT高级特性:保留消息(Retained Messages
)、遗嘱消息(Last Will and Testament
)、会话保持(Keep Alive
)、客户端托管(Client Take-over
)、链路保密(TLS
)、访问控制(ACL
)。
ROS高级特性:参数服务器(Parameter Server
)、服务端-客户端方式。
0x01 概述
MQTT是基于发布-订阅模式的C/S架构消息传输协议,它轻量、开放、简单且易于实施。这些特性使其非常适合在特殊的受限情况下使用,例如用于机器对机器(M2M)和物联网(IoT)中的通信,这些环境中由于存储空间和网络带宽非常宝贵因此需要代码体积尽量小,网络协议尽量简单,MQTT正好满足这两点要求。
ROS是用于编写机器人软件的灵活框架,它是工具、库和约定的集合,旨在简化跨各种机器人平台创建复杂而强大的机器人行为的任务。
0x02 关于订阅者-发布者模式
观察者模式(Observer Pattern)
在继续说订阅者-发布者模式之前,有必要提出观察者模式的概念,观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。
观察者模式有一个别名叫“发布-订阅模式”,或者说是“订阅-发布模式”,订阅者和订阅目标是联系在一起的,当订阅目标发生改变时,逐个通知订阅者。我们可以用报纸期刊的订阅来形象的说明,当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报纸,报社和订报纸的客户就是上面文章开头所说的“一对多”的依赖关系。
发布-订阅模式(Pub-Sub Pattern)
简介
上面提到发布-订阅模式是观察者模式的一个别称,但是经过时间的沉淀,这个模式已经慢慢独立于观察者模式,成为另外一种不同的设计模式。
在现在的发布-订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为消息代理或调度中心或中间件,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。
举一个例子,你在微博上关注了A,同时其他很多人也关注了A,那么当A发布动态的时候,微博就会为你们推送这条动态。A就是发布者,你是订阅者,微博就是调度中心,你和A是没有直接的消息往来的,全是通过微博来协调的(你的关注,A的发布动态)。
特点
使用发布订阅模式可以对发布者和订阅者进行解耦,主要表现为:
- 空间解耦:对于任意的发布者与订阅者,他们事先不需要知道彼此的存在,甚至不需要知道彼此的IP和端口。
- 时间解耦:对于任意的发布者与订阅者,他们不需要同时运行。
- 同步解耦:在通信发生时,他们彼此正在进行的任务不需要进行中断。
0x03 关于MQTT协议
协议特点
MQTT协议在实际实现中就整体使用了发布者-订阅者模式,那么依据发布者-订阅者模式的特点,MQTT同样实现了这三个特点:
- 空间解耦:对于使用了MQTT协议的系统,每一个发布者或订阅者只需要知道消息代理(或称为MQTT主服务器)的IP/端口就可以进行消息的发布与接收。
- 时间解耦:尽管大多数情况下使用MQTT协议的系统在进行消息传递时都是实时的,但是在必要的情况下消息代理可以为离线的客户端储存消息。必要的情况应当满足以下两个条件:
- 客户端曾经连入过消息代理并建立过持久化会话
- 需要储存的消息的QoS(服务质量)应当大于0
- 同步解耦:MQTT协议的系统的绝大多数客户端库都是异步的,他们的函数模式基本都是基于callback模式的,因此在其发布消息或等待消息时不会发生流程的阻塞。但是,MQTT提供了部分同步方法,如果某些特定消息必须进行同步,那么可以使用这些方法来达到理想的同步效果。
PS:当客户端与消息代理连接时,可以额外指定持久化会话标识,消息代理接收到后会与此客户端建立持久化会话而非临时会话,与临时会话的区别只要在于,在持久化会话中,消息代理会帮客户端保存特定的信息。
话题-Topic
基本概念
根据上面的描述,我们可以很轻易地画出在MQTT中消息传递的拓扑图:
可以看到,我们在拓扑中引入了话题(Topic
)的概念,对于MQTT系统,其通常采用基于话题的消息过滤机制,即消息代理会依据话题名来进行消息的分发。
编写规范
在MQTT中,话题名是由一串UTF-8
编码的字符串组成的,例如:
kingdom/phylum/class/order/family/genus/species
(界门纲目科属种)
这同时也是一个多级主题的示例,在MQTT中,允许存在多级话题,可以向任何一级发布消息,每一个话题级别由/
分割。
对于主题名,有如下规则:
- 每个主题至少包含一个字符。
- 主题允许包含空格而不会被截断。
- 大小写敏感。
- 单个
/
是一个有效的话题。 - 发布消息时不允许包含通配符作为话题名。
- 发布消息时不允许使用
$
开头的主题名。
对于这些规则,有以下几点补充说明:
- 不要使用前导正斜杠。例如
/kingdom/phylum
,尽管这也是合法的主题名,但是此时kingdom
的主题层级不再是顶级主题,通常这会导致混乱的发生。 - 不要使用空格作为主题名的一部分。例如
shui guo/xi gua
,尽管这也是合法的主题名,但是通常空格会导致可读性下降,进而导致混乱的发生。 - 在主题名中加入UID。例如
0df8827c-2af2-4710-84f3-b35f30f177f5/data
,尽管这并非强制需求,但是配合ACL规则,这将可以保证良好的可读性,并且避免敏感信息的泄露。 - 非必要不要订阅
#
。这将导致本地的消息负载过大进而导致宕机,如确有需要记录所有消息(例如实现了一个logger
),可以考虑基于MQTT做功能扩展实现负载均衡。 - 不要操作
$
开头的主题。这部分主题由消息代理保留,一般用于消息代理保存相关统计信息。例如:$SYS/broker/clients/connected $SYS/broker/clients/disconnected $SYS/broker/clients/total $SYS/broker/messages/sent $SYS/broker/uptime
通配符
订阅者在订阅话题时,除了可以使用完整的话题名之外,还可以使用通配符进行话题的订阅。
通配符分为两种,分别是单层通配符(+
)和多层通配符(#
):
- 单层通配符(
+
):单层通配符可以替代主题名中的任意一个层级用来指代本层级的所有话题层级名,单层通配符前后必须是正斜杠/
(单层通配符处于主题名末尾时除外)。例如使用kingdom/phylum/+/order
作为主题名订阅时,匹配结果如下| Topic | Status |
| :—————————————————: | :——: |
| kingdom/phylum/class/order | Yes |
| kingdom/phylum/aaaaa/order | Yes |
| kingdom/phylum/aaaaa/bbbbb | No |
| kingdom/bbbbbb/aaaaa/order | No |
| kingdom/phylum/aaaaa/bbbbb/order | No | - 全局通配符(
#
):全局通配符必须是主题名的最后一个字符,且全局通配符之前必须是正斜杠/
(主题名只有#
的情形除外)例如使用kingdom/phylum/#
作为主题名订阅时,匹配结果如下| Topic | Status |
| :———————————————: | :——: |
| kingdom/phylum/class/order | Yes |
| kingdom/phylum/aaaaa/order | Yes |
| kingdom/phylum/aaaaa/bbbbb | Yes |
| kingdom/bbbbbb/aaaaa/order | No |
| kingdom/phylum/aaaaa/bbbbb/order | Yes |特别的,当使用
#
作为整个主题名时,此订阅者将会收到所有发给消息代理的消息。
与消息队列的区别
- 消息队列机制中,消息被消息队列保存,直到使用者将其接收使用(消耗)为止,这点MQTT与之相同,就像没有订阅者的话题一样。
- 消息队列机制中,每一条消息都仅由一个客户端使用并处理,这将把负载均衡到每个客户端,而在MQTT系统中,所有订阅了此Topic的订阅者都将获取此消息。
- 队列是命名的,必须首先显式创建后才能对其写入以及读取,而MQTT拥有隐式创建Topic的能力,即,当我们向MQTT的某个Topic发布消息时,若此时Topic不存在,消息代理会自动的创建此Topic。
服务质量-QoS
MQTT中有三个服务质量级别,服务质量将直接影响消息传递的可靠性以及资源开销。
QoS 0 – 尽最大努力交付
这个级别是最低的服务质量级别,只保证尽最大努力,此消息在发送方端不做存储,且不要求接收方发送确认,不会进行任何重发操作,可靠性与TCP协议相同,资源开销最低。
QoS 1 – 确保交付
这个级别是中等的服务质量级别,只保证消息能被接收方能接收到消息而不管接收方接收到几次消息,资源开销中等,发送方应当保存一份消息至本地,直到接收到接收方发送来的PUBACK
消息,当等待时间超过阈值,即重发消息。一般来说,若消息接受方是客户端(例如,订阅者),它应当立即处理此消息并回复PUBACK
消息;若消息接收方是消息代理,它应当立即依据话题名或其他消息过滤机制进行下一步分发并回复PUBACK
消息。
此外,发送方发送的消息包中有DUP
标志位,当这个消息包是重发的消息包时将会把这个标志位置位,但是,无论此标志位是否置位,接收方在收到后都会发起确认,此标志位只用于内部的目的,对于消息代理和客户端都是透明的。
QoS 2 – 仅一次交付
这个服务级别是最高服务级别,这是最安全的级别但同时也是最慢的服务级别,在此种模式下可以确保接受方接收且仅接受一次目标消息。发送方和接收方之间至少有两个请求/响应流(四部分握手)来提供保证,发送者和接收者使用原始发布消息的数据包标识符来协调消息的传递。
当接收方从发送方获得QoS 2
级别的数据包时,它会相应地处理发布消息,并向发送方回复PUBREC数据包,如果发送方直到阈值时间结束也没有从接收方收到PUBREC
数据包,它将再次发送带有DUP
标志的数据包,直到收到确认为止。
一旦发送方从接收方接收到PUBREC
数据包,发送方就可以安全地丢弃初始的数据包。发送方存储来自接收方的PUBREC
数据包,并以PUBREL数据包作为响应 。
接收者获得PUBREL
数据包后,它可以丢弃所有存储的数据包并用PUBCOMP数据包应答(发送者接收到PUBCOMP
时也是如此)。在接收方完成处理并将PUBCOMP
数据包发送回发送方之前,接收方将存储对原始数据包的标识符并将其锁死。此步骤很重要,可以避免再次处理该消息。发送方收到PUBCOMP
数据包后,将对之前锁死的数据包进行解锁操作,此时已发布消息的数据包标识符将变为可用。
此时,QoS 2级别的消息交付流程结束,如果任何一个数据包在途中丢失,则发件人有责任在合理的阈值时间内进行消息的重传。
局限性
降级攻击
对于MQTT系统中最重要的发布者和订阅者而言,实际在进行订阅动作与发布动作时的QoS级别可能是不同的。例如,客户端A是消息的发布者,客户端B是消息的订阅者,如果客户端B以QoS 1
订阅Topic
,而客户端A以QoS 2
发布信息到Topic
,那么代理最终会以QoS 1
的服务质量将消息传送到客户端B处,而且客户端B将有可能会收到多次消息。
数据标识符并不唯一
正像上文所提到的,数据传输时的数据标识符并不是唯一的,它只会在数据传输时被消息代理临时锁死(QoS 0
级别时除外),一旦整个流程结束此标识符将会被释放,因此官方给定的数据标识符范围是0~65535
,官方认为在不进行客户端的交互时,发送超过65535
条消息是不现实的。
0x04 关于ROS”协议”
系统特点
与MQTT协议不同,ROS 是一个适用于机器人的开源的元操作系统。它提供了操作系统应有的服务,包括硬件抽象,底层设备控制,常用函数的实现,进程间消息传递,以及包管理。它也提供用于获取、编译、编写、和跨计算机运行代码所需的工具和库函数。在某些方面ROS相当于一种“机器人框架(robot frameworks)。ROS实现了几种不同的通信方式,包括基于同步RPC样式通信的服务(services)机制,基于异步流媒体数据的话题(topics)机制以及用于数据存储的参数服务器(Parameter Server)。
- 小型化:ROS尽可能设计的很小 — 我们不封装您的 main() 函数 — 所以为ROS编写的代码可以轻松的在其它机器人软件平台上使用。 由此得出的必然结论是ROS可以轻松集成在其它机器人软件平台:ROS已经可以与OpenRAVE,Orocos和Player集成。
- ROS不敏感库:ROS的首选开发模型都是用不依赖ROS的干净的库函数编写而成。
- 语言独立:ROS框架可以简单地使用任何的现代编程语言实现。我们已经实现了Python版本,C++版本和 Lisp版本。同时,我们也拥有Java 和 Lua版本的实验库。
- 方便测试:ROS内建一个了叫做rostest的单元/集成测试框架,可以轻松安装或卸载测试模块。
- 可扩展:ROS可以适用于大型运行时系统和大型开发进程。
话题-Topic
- ROS的话题与MQTT的话题不同,它并不是一个单纯的存储位置,它拥有实际的消息处理能力,我们每一个接入
ROS master server
(此处担任消息代理)的PC都是一个节点,而每一个节点都拥有若干个Topic
,产生订阅关系时,订阅者与发布者都是话题。 - 由于ROS并未实现标准的订阅者-发布者模式,因此在ROS系统中的订阅者与发布者事实上没有进行解耦,所有的节点都可以向任意话题发布消息,同时,所有的节点可以订阅任意的话题。
- ROS的话题机制不支持通配符机制,它的话题名不应包含任何特殊字符。
0x05 二者对比
- 首先,ROS是一个成型的系统,而MQTT只是一种通信的协议。
- ROS除了支持
Pub-Sub
通信外,还支持C-S
通信。 - MQTT协议的所有消息都经过消息代理的转发,只需要一个单一出口,代理管理所有的消息处理与分发。而ROS的消息由话题自己处理,其主服务器虽然担任消息代理,但是其仅管理所有Topic的IP与端口,实际的消息处理由Topic自行进行。
- 对于消息传输,MQTT协议更加健全,包括引入了服务质量以及访问控制等安全性概念,相比之下,ROS系统的鉴权以及链路保护均不完善,但是由于其比MQTT更轻量,二次开发难度更低,也有不少厂商开始青睐此系统,应当引起重视。(事实上,ROS安全性已经在ROS2中有了很大的改善,但是由于迭代难度较大,部分API的约定与ROS不一致导致许多产商并不愿意去进行版本的迭代)