WebSocket的话题
撰写于:2016年10月24日
作者:silex Wi-Fi专家
“物联网”简称 IoT(Internet of Things)成为流行语已经过去好几年了。在这个博客中,我一直有意与 IoT 保持一定距离,但现在似乎已经不能再这样了。这次我要讲的是作为 IoT 协议标准候选之一的 MQTT 相关内容。
什么是MQTT
MQTT 是一种代理型的消息传递与再分配协议,其起源相当古老,据说它是在 1999 年由 IBM 和 Eurotech 公司开发的。由于它是以 IBM 的 MQ 协议为基础进行开发的,所以也有一种说法是,它是 “MQ Telemetry Transport” 的缩写,故而被命名为 MQTT,但实际上 MQTT 这个名称本身并没有什么特别的含义。从根本上来说,MQTT 没有存储(Queue)消息的功能,MQTT 这个名字属于那种 “名不副实” 的奇妙情况。
在传统的客户端 - 服务器型系统中,数据的发送方和接收方是直接相连的。当从网络浏览器连接到 Amazon 或 Google 时,这不会产生问题,但在像传感器网络那样存在成百上千个数据源的情况下,这就成了一个问题。例如 “管理着 2 楼第 3 会议室南侧窗户开关的传感器的节点的 IP 地址是什么呢?”,客户端必须检测并管理与传感器节点数量相同的 IP 地址。另一方面,在服务器端,当连接数量增多时,处理和内存消耗的负荷会增加,而且 “当发生了什么情况时该告知谁呢?” 这样的数据分发条件设定也往往会变得复杂。

客户端 - 服务器模型
当服务器数量较少时,情况较为简单

客户端 - 服务器模型
当服务器的数量增加时,其复杂程度会呈几何级数增长
另一方面,所谓 “代理型”,指的是数据的发送方(发布者 / Publisher)只需将数据发送给再分配服务器(代理 / Broker)即可,至于这些数据在何种条件下被发送给哪些接收方,则由代理来进行处理的一种系统形态。数据的接收方被称为订阅者(Subscriber)。这是一种由代理统一承担繁琐事务的集权型系统,把代理理解为云服务器的话可能会更容易明白。

代理模型
连接组合的复杂程度由代理来承担
MQTT 并非(像 SOAP 或 AJAX 那样)依赖于 HTTP,而是使用其独有的通信端口(TCP 1883,TLS 8883)来运行。其协议规范也与 HTTP/HTML 完全不同,采用的是按字节为单位的二进制格式,在交换大约十几字节的短消息时,数据效率出色也是它的一大特点。
主题和消息
那么,数据传输的基本在于向 “谁” 传达 “什么”。“什么” 进一步由 “用于识别它的名称” 和 “其中的内容” 构成。在 MQTT 中,“什么” 是由被称为 「主题(Topic)」的字符串来识别的。主题类似于所谓的文件系统的路径结构,可以附上以 “/” 开头的任意层级的名称。MQTT 的主题名称只有 “以 / 开头的 Unicode UTF-8 字符串”、“全长要在 65535 字节以下”、“不能包含空字符”、“$、#、+ 作为保留字被赋予了特别的功能” 等规定,具有非常高的自由度(※注)。
※注:只是说在原始规范中没有既定的规定,根据具体的实现方式,存在有不同的既定规则和上限的可能性。
在 MQTT 中,主题和发布者之间没有直接关系。多个发布者也可以向同一个主题发送消息(※注)。MQTT 完全不关心消息是从哪个发布者发送过来的,并且对于多个发布者 “几乎同时” 发送消息时的优先级和到达顺序,既不进行管理也不提供保证。
※注:如果出现无论谁都能发送消息会造成困扰的情况,是可以通过使用用户名和密码来限制对主题的访问条件来实现对代理的设置的。这被称为 “权限(Permission)” 功能,许多 MQTT 代理都有此功能。不过,MQTT 规范只是设定了 “可以指定用户名和密码” 这样的框架,至于如何利用它来实现权限则由具体的实现方式来决定。
关于数据的 “内容”,MQTT 规定了 “不做既定要求”(※注)。例如,要传达温度为 25℃,无论是用二进制的 0x00 0x19,还是无单位的字符串 “25”,或者是带单位的字符串 “25C”“77F”“298K”,只要应用程序能够解读就可以。传统的消息型协议会有某些数据类型(如整数或字符串),相比之下,这种高度的自由度甚至会让人觉得有些不同寻常。话虽如此,如果每个发布者发送的消息都各不相同,确实可能会导致系统出现问题,所以实际上很可能会使用 XML 或 JSON 等在网络领域已经在使用的格式。另外,MQTT 的消息长度采用了一种有点特殊的编码形式,可表达的最大长度为 256 兆字节。
※注:在规格说明书的原文中是 "The content and format of the data is application specific".
关于将数据传达给 “谁” 这一点,MQTT 通过会话来识别 “谁”。也就是说,MQTT 的接收方(订阅者)是以始终保持连接的状态来使用为前提的。例如,像电子邮件那样,处于断开连接状态的 “某人” 登录后能一并接收已积累的消息这种运用方式,MQTT 并没有考虑(※注)。就如同开头所写的那样,“与 MQTT 这个名称相反,它没有队列(Queue)功能”。
※注:不过,作为可选规范,存在一种 “保留(Retain)” 功能,即可以存储发送到主题的最后一条消息 。
订阅者向代理指定自己想要接收的主题。当有消息发送到该主题时(由发布者发送),消息会被重新分发给所有订阅者。不存在像在消息配送系统中常见的那种,将位掩码和 “与 / 或” 条件组合而成的分发条件筛选这类复杂功能(※注)。
※注:同样,也可以实现根据用户名来设置消息重新分发条件。是否要实现这一功能,以及要实现到能设置何种复杂的条件,都取决于具体的实现方式和运用情况。
在 MQTT 的订阅者端,有一个名为「通配符」 的机制,可用于同时接收多个主题。 这是通过 + 和 # 的组合来指定的,它们 “被赋予了保留字的特殊功能”。
在 MQTT 的主题中,“+” 表示单一层级的任意匹配,“#” 表示 “其后的所有匹配”。通配符是按层级单位使用的,并不用于字符串的部分匹配。主题 “/bedroom/#” 表示 “以 /bedroom/ 开头的所有主题”,“/+/light” 表示 “两层结构且第二层为 light 的所有主题”。像 “/+room” 或 “/bed#” 这样与字符串组合使用是不可以的。
当使用通配符时,订阅者(理所当然地)会接收来自多个不同主题的消息。然而,在 MQTT 中,既不管理也不保证消息的优先级和到达顺序,甚至不会显示消息是从哪个主题发送过来的。这方面也是 “取决于应用程序”,所以有必要为系统确定某种解决方案,比如 “在消息中附上主题信息” 或者 “从一开始就不依赖通配符” 等等。
使用MQTT
MQTT 有很多是用 “IT 行业常用语言”,比如 Python、JavaScript、Erlang 等编写的实现方式,网上关于 MQTT 的解说很多也充分利用了这些语言。不过这次像 “嵌入式领域从业者” 那样,使用了一个用纯 C 语言编写的名为 mosquitto 的软件包。其版本是 1.4.9 ,但版本问题不会造成特别难以处理的情况。
下载 mosquitto 的源码并执行 make 命令后,在 src/ 目录下应该会生成作为代理的 mosquitto,而在 client/ 目录下会生成发布者程序 mosquitto_pub 以及订阅者程序 mosquitto_sub。 首先启动代理。加上 -v 选项,让其逐一显示连接和收发的信息,这样就能很容易明白其运行情况了。
$ ./src/mosquitto -v
1470848533: mosquitto version 1.4.9 (build date 2016-08-09 09:39:33-0700) starting
1470848533: Using default config.
1470848533: Opening ipv4 listen socket on port 1883.
1470848533: Opening ipv6 listen socket on port 1883.
接下来,打开另一个窗口并启动订阅者。“-h” 用于指定 IP 地址或主机名,“-t” 用于指定主题。
$ ./client/mosquitto_sub -h 192.168.5.79 -t /bedroom/light
接着再另外打开一个窗口来启动发布者。“-h” 和 “-t” 与订阅者的设定方式相同,“-m” 表示消息。
$ ./client/mosquitto_pub -h 192.168.5.79 -t /bedroom/light -m ON
这样的话,在订阅者的窗口中应该会显示重新分发的消息 “ON”。
$ ./client/mosquitto_sub -h 192.168.5.79 -t /bedroom/light
ON
以上就是 MQTT 的一个简单运行示例…… 就这些吗?!没错,就这些。像这样,MQTT 是一个非常简单的协议。在其发展过程中(※注),虽然添加了诸如 QoS 、Retain、Will 等可选功能,但即便完全不使用这些功能,也能够以最单纯的形式 “先让它运行起来”,这也是 MQTT 的魅力之一。
※注:在撰写本文时,MQTT 规范的最新版本是 3.1.1,但与之前的 3.1 版本相比有相当大的修订,甚至存在一些直接不兼容的部分,似乎引发了一些混乱。为什么不命名为 3.2 或 4.0,而是 3.1.1 呢?
MQTT 和安全
实际上,单纯在 TCP 的 1883 端口上运行 MQTT 大概是不太可行的。尤其是在云端运行的情况下,几乎必然要设置 TLS 安全层,至少要进行单向(代理 / 服务器端)的认证和加密。对于发布者和订阅者的认证,推测可能会在 TLS 加密线路上使用用户名 / 密码来进行。不过,也可以将使用客户端证书的 TLS 双向认证设为必要条件,这方面需要根据所使用的实现方式和运用上的要求来进行相应的集成。
使用 TLS 时,每次连接和断开都会产生证书的交换与验证,这会导致通信时间增加以及电力消耗增大。对于由电池驱动且间歇性发布数据的传感器节点而言,这是个大问题,如何处理这一点是集成商需要考虑的。当数据发送间隔较短时,与其贸然地断开和重新连接,或许保持 TLS 线路连接并进入省电模式会更有效率。而当数据发送间隔足够长时,切断电源并每次重新启动和连接可能会更高效。究竟如何判断是 “短” 还是 “长”,这取决于待机时的电力消耗与重新连接和重新认证时的电力消耗之间的平衡,同时也会受到所使用的 MCU 性能以及证书密钥长度的影响,所以无法一概而论。将 TLS 作为隧道实装在始终通电的上行路由器上,并且末端节点与上行路由器之间依赖数据链路层的安全功能(如 WiFi WPA2 等),这或许是正确的做法。MQTT 的原理简单且自由度高,要熟练运用它需要一定的技巧,这也算是 MQTT 的一个特点吧。
总结
以上就是关于 MQTT 的简单总结。这里并没有深入探讨命令以及数据包格式的详细内容,不过 MQTT 在日本也被广泛应用,开发者们在网络上也发布了丰富的日语版(真实有用的)相关信息可供查阅。与蓝牙相比,MQTT 的日语信息不少,而蓝牙不仅日语信息少,而且其内容往往还比较陈旧,或者夹杂着一些错误理解。
总之,MQTT 是一个 “极其简单” 的协议,甚至连消息格式都没有预先确定,给人一种将在运用中可能会面临的许多课题都当作 “取决于应用程序” 而完全抛给实现者和运用者的印象。与那些用 MUST, MUST NOT, SHOULD, SHOULD NOT 来严格规范的近期的 RFC 规范相比,感觉就像是在看3 xx 时代的 RFC 一样。像这样的规范从 IETF / RFC 的框架之外诞生并逐渐被市场所接受,这是很有象征意义的。
在 “蓝牙上 IPv6的话题” 中提到,让蓝牙低功耗的 IPSP 运行起来,其 “吞吐量大概为 10Kbit / 秒”,还提到 “在这种情况下使用 HTTPS 是相当不切实际的”,不过 MQTT 或许适合这样的低速网络。话虽如此,如果在安全层使用 TLS,每次进行连接和认证时都会有数千字节的证书交换流程,这样一来,HTTP 和 MQTT 在报头长度上的差异也就不算什么大问题了。要让 MQTT 作为发挥其特性的系统来运行,关键在于把握要求条件以及所使用组件的特性,并且需要集成人员判断利弊得失,选择合适的设置,这一点是不会改变的。
相关连接