在传统的互联网通信中,往往采用 HTTP 作为应用层协议。HTTP 应用广泛、内容丰富、生态完善,使用起来非常方便。
然而,HTTP 在嵌入式物联网的应用中,存在许多问题:首先 HTTP 过于复杂,HTTP 缓存、连接管理、认证等都是十分复杂的机制;哪怕忽略这方面的内容,HTTP 请求和响应也携带了较多无用的信息,需要较大缓存空间,且解析数据比较麻烦。
MQTT(Message Queuing Telemetry Transport, 消息队列遥测传输)协议是一种轻量级的通讯协议,由 IBM 在 1999 年发布,是一种低开销、低带宽占用的即时通讯协议,可以用极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务,在物联网、小型设备、移动应用等方面有较广泛的应用。
MQTT 是一个基于客户端-服务器的消息发布/订阅(publish/subscribe)传输协议,基于 TCP 服务实现,是一个应用层协议。MQTT 经过许多年的发展,目前主流版本有 3.1.1 和 5.0 ,不过 5.0 用的不多。这篇文章 对比了 MQTT 的各个版本,并给出了许多有用的资源。
接下来介绍 MQTT 3.1.1 协议的基本内容。
MQTT协议包含内容
消息的发布和订阅
MQTT 使用一种特别的消息发布/订阅模式,每一台主机可以发布一个消息,也可以接收一个消息。但需要注意的是,消息不是点对点直接从发送端到达接收端,而是由 MQTT 服务器(称为 MQTT Broker)分发的。
主题(topic)是一种消息分类的方式,每一台主机可以订阅(subscribe)一个主题。订阅主题后,一台主机可以发布(publish)该主题的消息,也可以接收该主题的消息,不同主题之间的消息各自独立,互不影响。
下图展示了经由 MQTT Broker 订阅与发布消息的工作场景:
由于 Publisher 与 Subscriber 并不会直接交互,因此两者无需知道对方的 IP 地址和端口等信息,也不一定需要同时运行。所有这一切都交给 Broker 处理。
主题
主题用于过滤消息,一个客户端只会接收到有订阅主题的消息。
主题的表现形式是一个 UTF-8 字符串,主题之间可以存在层级关系,不同层级之间以斜杠 /
划分,类似于操作系统的文件体系。例如,'home/lamp/red'
就是一个合适的多级主题,订阅该主题的客户端只会收到该主题下的消息。
多层主题的用途是可以通过通配符来一次影响多个主题。通配符主要有两个:
单级通配符:使用加号 +
作为某层主题时,可以匹配该层的所有主题。例如:
'home/lamp/+'
- 可以匹配如下主题:
'home/lamp/red'
、'home/lamp/blue'
、'home/lamp/'
- 但是不会匹配如下主题:
'home/lamp'
、'home/lamp/red/1'
、'home/led/red'
多级通配符:使用井号 #
作为某层主题时,可以该层及包含的所有子层级的主题。例如:
'home/#'
- 可以匹配如下主题:
'home/lamp'
、'home/lamp/red'
、'home'
- 特别地,使用单独
#
会收到所有主题的消息
通过设计并订阅合理的主题,就可以自由管理每台设备应该接收的消息。
MQTT 报文结构
接下来简要介绍 MQTT 的报文。MQTT 的报文结构为:
MQTT 报文结构大致可分为 3 个部分:
- 固定头:必须存在,包含必要信息并决定报文的整体结构
- 可变头:可选,消息类型决定了可变头是否存在及其具体内容
- 负载:可选,表示客户端收到的具体内容
固定头第一字节的前 4 个比特表示消息类型。MQTT 一共有 16 种消息类型,分别为:
值 | 名称 | 流向 | 含义 | 值 | 名称 | 流向 | 含义 |
---|---|---|---|---|---|---|---|
0 |
Reserved | 保留 | 保留 | 1 |
CONNECT | 客户端到服务器 | 请求连接 |
2 |
CONNACK | 服务器到客户端 | 连接确认 | 3 |
PUBLISH | 双向 | 发布消息 |
4 |
PUBACK | 双向 | 发布确认 | 5 |
PUBREC | 双向 | 发布收到(保证第1部分到达) |
6 |
PUBREL | 双向 | 发布释放(保证第2部分到达) | 7 |
PUBCOMP | 双向 | 发布完成(保证第3部分到达) |
|
|
||||||
8 |
SUBSCRIBE | 客户端到服务器 | 请求订阅 | 9 |
SUBACK | 服务器到客户端 | 订阅确认 |
10 |
UNSUBSCRIBE | 客户端到服务器 | 取消订阅 | 11 |
UNSUBACK | 服务器到客户端 | 取消订阅确认 |
12 |
PINGREQ | 客户端到服务器 | PING 请求 | 13 |
PINGRESP | 服务器到客户端 | PING 应答 |
14 |
DISCONNECT | 客户端到服务器 | 中断连接 | 15 |
Reserved | 保留 | 保留 |
这最开头的 4 个比特决定了后 4 个比特以及接下来部分的内容。不同的报文可变头的内容也不一样,对报文完整的结构描述可以参见文档。
接下来通过一个具体的操作实验了解 MQTT 通信的基本过程,并简要介绍几个报文的组成。
MQTT测试实验
搭建环境
为了研究 MQTT 的运行,需要搭建一个可以使用 MQTT 的运行环境。
首先安装 mosquitto ,它是一个开源、跨平台的 MQTT broker ,提供轻量级的 MQTT 发布/订阅实现。mosquitto 的官网为 https://mosquitto.org/ 。
似乎 mosquitto 2.0 及以上才支持 MQTT 5 。各个版本的安装细节可以参照 https://mosquitto.org/download/ 。可以在 Linux 上直接通过命令安装 mosquitto 。例如,以下是在 Ubuntu 上安装 mosquitto 的命令:
然后检查安装的版本并启动服务:
可以看到 MQTT 默认运行的端口号为 1883 。
接下来需要一个合适的 MQTT 客户端用于发布消息。这里推荐 MQTTX ,它是一个开源跨平台的 MQTT 桌面客户端,功能丰富且界面精美,使用起来就像社交聊天软件一样方便。它可以在 https://mqttx.app/ 下载并像一般的软件一样安装。
MQTTX 的使用可以参考文档,以下简单介绍了界面各个部分的作用:
可以在软件中建立两个连接,分别订阅同一个主题,然后在一个连接中发送一些消息,即可在另一个连接中收到同样的信息。如果想研究通信过程中交换了哪些报文,可以使用 WireShark 等软件捕获并分析。以下是一次订阅并发送消息的过程中,WireShark 捕获到的信息:(蓝色是 client 向 broker 发送,紫色是 broker 回复 client )
模拟通信
接下来使用套接字程序模拟简单的通信(消息发布)过程,可以使用任意支持套接字编程的语言编写程序,以下使用 Python 。消息发布的过程可以参考文档的相关部分。
首先必须发送 CONNECT 报文连接到 broker ,因此程序需要发送如下字节:
以上各个字节的含义分别是固定头、剩余长度值(除了前两字节)、协议名长度(两个字节)、协议名 'MQTT'
(四个字节)、版本号(4 代表 v3.1.1)、标志位、会话状态的生存时间(两个字节)、客户端 ID 长度(两个字节)、客户端 ID 'Python socket'
。更多细节建议参考官方文档 。
接下来编写 Python 套接字程序,将以上字节发送给 mosquitto :
运行以上程序,可以在 mosquitto 中看到活动记录:
同时 Python 程序也会接收到它的响应信息 b' \x02\x00\x00'
,该信息的首个字节为 0x20(等价于空格的 ASCII 码),说明这是一个 CONNACK 连接确认信息,剩余长度为 0x02 ,分别用于初始化会话设置和表示连接已接受(响应状态码)。
接下来尝试发布消息,此时控制报文类型为 PUBLISH(3) ,并且第一个字节的后 4 位有了意义,这些标志位的细节可以参见文档。例如,第一个字节为 0x34(或 0b0011 0100),代表这是 PUBLISH 报文、第一次请求发送、只分发一次、不保留消息。
这些标志位中值的注意的是服务质量(Quality of Service, QoS)标志位,它是 MQTT 的一个特性,用于处理复杂环境下嵌入式网络的中断和干扰,避免信息丢失。QoS 有三个等级,不同等级下消息的发送方需要采取不同的措施应对当前的网络情况:
- QoS 0 :At most once delivery ,仅发送消息,不考虑丢失及重发>
- QoS 1 :At least once delivery ,发送消息后需要关注 ACK 响应并可能重发,保证消息至少能到达一次,但无法保证消息重复
- QoS 2 :Exactly once delivery ,使用复杂的消息重发机制,保证消息到达对方并且严格只到达一次,但开销最大
本次发布消息时,总共发送如下字节:
各个字节的含义分别是固定头、剩余长度值、主题长度(两个字节)、主题 'demo/test'
、消息标识符(两个字节)、消息 'hello'
。
不过仅仅发送以上报文,还不能在客户端中收到包含的信息。阅读文档的相关部分可以发现,在接收到 PUBLISH 的响应 PUBREC 后,发送端还需要响应 PUBREL ,内容为:
包括固定头、剩余长度值和消息标识符(两个字节)。
在确认发布完毕后,broker 会将该消息发送给其订阅者,然后发布者便可以主动取消连接了:
将以上字节发送出去后,便可以在 MQTTX 订阅对应主题的连接中接收到发送的消息了:
同时,broker 显示的完整活动记录如下:
对 MQTT 简单通信的分析就到这里为止,感兴趣的话可以参考以上步骤分析消息的订阅与分发过程。
关于遗嘱消息
遗嘱消息(will message)是 MQTT 的特点之一,用于在某些设备意外断线时,将一个特定的消息发送给第三方。
遗嘱是一种特殊的 PUBLISH 消息,在设备意外断线时,由 broker 将其发布到特定的主题上,因此 broker 需要提前存储遗嘱消息。
遗嘱消息会在设备与服务端连接时,通过客户端的 CONNECT 报文指定。如果 CONNECT 报文标志字节的第 2 位遗嘱标志位被置 1 ,则该报文包含需要记录的遗嘱消息,并在 CONNECT 报文的最后包含以下信息:
- 遗嘱主题长度
- 遗嘱主题
- 遗嘱消息长度
- 遗嘱消息
遗嘱相关的信息将会保存在服务器中,并在发生意外时被推送到订阅的客户端中,直到客户端主动断开连接才被清除。
本次对 MQTT 的简单介绍就到这里为止了。其余部分官方文档已经写的足够详细,这里不再重复。总的来说,MQTT 相比 HTTP ,报文非常短小紧凑,几乎每一个字节都没有浪费,极大缓解了嵌入式网络带宽不足的问题。同时,MQTT 也包含了较为完善的错误处理机制,可以在网络条件较差的情况下及时纠错。
参考资料/延伸阅读
https://mqtt.org/
MQTT 官网,从中可以找到许多有用的资料
https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
MQTT 5 最新文档
https://developer.ibm.com/articles/iot-mqtt-why-good-for-iot/
IBM 的 MQTT 介绍文档
https://github.com/mqtt/mqtt.org/wiki
MQTT 社区 wiki
https://www.emqx.com/zh/mqtt/public-mqtt5-broker
MQTTX 的公司提供的一个用于测试学习的 MQTT 服务器
https://www.emqx.com/zh/mqtt
它同时提供的 MQTT 介绍文章
https://mcxiaoke.gitbooks.io/mqtt-cn/content/mqtt/01-Introduction.html
MQTT 协议中文版
https://www.hivemq.com/mqtt-essentials/
MQTT 系列介绍文章