MQTT协议简介

在传统的互联网通信中,往往采用 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 个部分:

  1. 固定头:必须存在,包含必要信息并决定报文的整体结构
  2. 可变头:可选,消息类型决定了可变头是否存在及其具体内容
  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 的命令:

$ sudo apt-get update $ sudo apt-get install -y mosquitto mosquitto-clients

然后检查安装的版本并启动服务:

$ mosquitto -v 1660444055: mosquitto version 2.0.10 starting 1660444055: Using default config. 1660444055: Starting in local only mode. Connections will only be possible from clients running on this machine. 1660444055: Create a configuration file which defines a listener to allow remote access. 1660444055: For more details see https://mosquitto.org/documentation/authentication-methods/ 1660444055: Opening ipv4 listen socket on port 1883. 1660444055: Opening ipv6 listen socket on port 1883. 1660444055: mosquitto version 2.0.10 running

可以看到 MQTT 默认运行的端口号为 1883 。

接下来需要一个合适的 MQTT 客户端用于发布消息。这里推荐 MQTTX ,它是一个开源跨平台的 MQTT 桌面客户端,功能丰富且界面精美,使用起来就像社交聊天软件一样方便。它可以在 https://mqttx.app/ 下载并像一般的软件一样安装。

MQTTX 的使用可以参考文档,以下简单介绍了界面各个部分的作用:

可以在软件中建立两个连接,分别订阅同一个主题,然后在一个连接中发送一些消息,即可在另一个连接中收到同样的信息。如果想研究通信过程中交换了哪些报文,可以使用 WireShark 等软件捕获并分析。以下是一次订阅并发送消息的过程中,WireShark 捕获到的信息:(蓝色是 client 向 broker 发送,紫色是 broker 回复 client )

模拟通信

接下来使用套接字程序模拟简单的通信(消息发布)过程,可以使用任意支持套接字编程的语言编写程序,以下使用 Python 。消息发布的过程可以参考文档的相关部分

首先必须发送 CONNECT 报文连接到 broker ,因此程序需要发送如下字节:

10 19 00 04 4D 51 54 54 04 02 00 3C 00 0D 50 79 74 68 6F 6E 20 73 6F 63 6B 65 74

以上各个字节的含义分别是固定头、剩余长度值(除了前两字节)、协议名长度(两个字节)、协议名 'MQTT'(四个字节)、版本号(4 代表 v3.1.1)、标志位、会话状态的生存时间(两个字节)、客户端 ID 长度(两个字节)、客户端 ID 'Python socket' 。更多细节建议参考官方文档

接下来编写 Python 套接字程序,将以上字节发送给 mosquitto :

import socket
server_name = 'localhost'
server_port = 1883

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((server_name, server_port))

with open('mqtt-connect.bin', 'rb') as file:
    client.send(file.read())  # CONNECT
print(client.recv(1024))      # CONNACK
client.close()

运行以上程序,可以在 mosquitto 中看到活动记录:

1660459407: New connection from 127.0.0.1:13080 on port 1883. 1660459407: New client connected from 127.0.0.1:13080 as Python socket (p2, c1, k60). 1660459407: No will message specified. 1660459407: Sending CONNACK to Python socket (0, 0) 1660459407: Client Python socket closed its connection.

同时 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 ,使用复杂的消息重发机制,保证消息到达对方并且严格只到达一次,但开销最大

本次发布消息时,总共发送如下字节:

34 12 00 09 64 65 6D 6F 2F 74 65 73 74 AE 00 68 65 6C 6C 6F

各个字节的含义分别是固定头、剩余长度值、主题长度(两个字节)、主题 'demo/test'、消息标识符(两个字节)、消息 'hello'

不过仅仅发送以上报文,还不能在客户端中收到包含的信息。阅读文档的相关部分可以发现,在接收到 PUBLISH 的响应 PUBREC 后,发送端还需要响应 PUBREL ,内容为:

62 02 AE 00

包括固定头、剩余长度值和消息标识符(两个字节)。

在确认发布完毕后,broker 会将该消息发送给其订阅者,然后发布者便可以主动取消连接了:

E0 00

将以上字节发送出去后,便可以在 MQTTX 订阅对应主题的连接中接收到发送的消息了:

同时,broker 显示的完整活动记录如下:

1660476177: New connection from 127.0.0.1:11074 on port 1883. 1660476177: New client connected from 127.0.0.1:11074 as Python socket (p2, c1, k60). 1660476177: No will message specified. 1660476177: Sending CONNACK to Python socket (0, 0) 1660476177: Received PUBLISH from Python socket (d0, q2, r0, m44544, 'demo/test', ... (5 bytes)) 1660476177: Sending PUBREC to Python socket (m44544, rc0) 1660476177: Received PUBREL from Python socket (Mid: 44544) 1660476177: Sending PUBLISH to MyPC (d0, q0, r0, m0, 'demo/test', ... (5 bytes)) 1660476177: Sending PUBCOMP to Python socket (m44544) 1660476177: Client Python socket closed its connection.

对 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 系列介绍文章

京ICP备2021034974号
contact me by hello@frozencandles.fun