MQTT协议详解(新手必看)
目前的物联网通信协议并没有统一的标准,MQTT 协议(消息队列遥测传输协议)是目前应用最广泛的协议之一。可以这么说,MQTT 协议之于物联网,就像 HTTP 之于互联网。
目前,基本上所有开放云平台(比如,阿里云、腾讯云、青云等)都支持 MQTT 的接入。
MQTT 协议是运行在 TCP 协议栈上的应用层协议,虽然 MQTT 协议的名称有 Message 和 Queue 两个词,但是它并不是像 RabbitMQ 那样的消息队列,这是初学者最容易搞混的一个问题。
与传统的消息队列相比,MQTT 协议有以下几点不同:
在 MQTT 协议里,我们称这个中间方为 Broker,称连接到 Broker 的订阅方和发布方为 Client。
一次典型的 MQTT 协议消息通信流程如下图所示:

图 1 MQTT 协议消息通信流程
后续将发送方称为 Publisher,将订阅方称为 Subscriber。
2017 年 8 月,OASIS MQTT Technical Committee 发布了 MQTT 5.0 草案。2018 年,MQTT 5.0 正式发布。MQTT 5.0 并未改变 MQTT 协议通信的架构和流程,而是在 3.1.1 的基础上推出了一些新特性,用来解决 3.1.1 版本的缺陷和不足,提供更多功能,方便应用的开发。MQTT 5.0 并不向下兼容 MQTT 3.1.1。
Publisher 和 Subscriber 都属于 Client。一个 Client 是 Publisher 还是 Subscriber,只取决于该 Client 当前的状态——是在发布消息还是在订阅消息。当然,一个 Client 可以同时是 Publisher 和 Subscriber。
在大多数情况下,我们不需要自己按照 MQTT 协议规范来实现一个 MQTT Client,因为 MQTT Client 库在很多语言中都有实现,包括 Android、Arduino、Ruby、C、C++、C#、Go、iOS、Java、JavaScript 以及 .NET 等。
下图展示了 MQTT Client 在各个平台上的实现:

图 2 MQTT Client 在各个平台上的实现
如前文所述,Broker 负责接收 Publisher 的消息,并将消息发送给相应的 Subscriber,它是整个 MQTT 协议订阅/发布的核心。
在实际应用中,一个 MQTT Broker 还应该提供如下功能:
下面列举了几个比较常用的 MQTT Broker。
如果要扩展 Mosquitto 的功能,比如自定义的验证方式,实现过程比较复杂,对接现有的业务系统等也较为复杂,需要对 Mosquitto 代码比较熟悉。
如果只是想搭建一个测试 Broker 或者单机验证功能的环境,Mosquitto 还是比较合适的,但如果想在生产系统中使用,则需要自行解决集群和扩展的问题。
EMQX 是用 Erlang 语言编写的,官方提供集群解决方案,并可以通过编写插件的方式对 Broker 的功能进行扩展。
EMQX 的开发者社区比较活跃,青云提供的物联网套件就是基于 EMQX 的。在生产系统中使用 EMQX 已经有几年了,EMQX 的性能在笔者看来是很不错的,唯一的缺点就是 Erlang 这门语言比较小众,而且学习曲线较陡,编写插件的时候可能需要重新学习一门语言。
不过 HiveMQ 是闭源的,只有付费企业版。
除了上面提到的几个常用的 MQTT Broker,你还可以在 https://github.com/mqtt/mqtt.github.io/wiki/servers 找到更多的 MQTT Broker。
除了自建 Broker,我们还可以使用前面提到的阿里云、腾讯云、青云之类的云服务商提供的物联网云平台的 MQTT 协议服务。如果只是抱着学习或者测试的目的,还可以使用一些公共的 MQTT Broker。
MQTT 协议的数据包非常简单,一个 MQTT 协议数据包由固定头(Fix Header)、可变头(Variable Header)、消息体(Payload)这 3 个部分依次组成:
这里我们首先看一下固定头,可变头和消息体将在讲解各种具体类型的 MQTT 协议数据包的时候详细讨论。
MQTT 协议数据包的固定头格式如下图所示:

图 3 MQTT协议数据包的固定头格式
MQTT 协议数据包类型如下表所示:
在不同类型的数据包中,标识位的定义是不一样的,每种数据包对应的标识位如下表所示。
这个字段最少为 1 个字节,最多为 4 个字节。其中,每一个字节的最高位叫作延续位(Continuation Bit),用于标识在这个字节之后是否还有一个用于表示剩余长度的字节。剩下的低 7 位用于标识值,范围为 0~127。
例如,剩余长度字段的第一个字节的最高位为 1,那么意味着剩余长度至少还有 1 个字节,然后继续读下一个字节,下一个字节的最高位为 0,那么剩余长度字段到此为止,一共 2 个字节。
剩余长度字段可标识的数据包长度如下表所示:
所以,4 个字节最多可标识的数据包长度为(0xFF,0xFF,0xFF,0x7F)=268435455 字节,即 256MB,这是 MQTT 协议中数据包的最大长度。
目前,基本上所有开放云平台(比如,阿里云、腾讯云、青云等)都支持 MQTT 的接入。
MQTT 协议是运行在 TCP 协议栈上的应用层协议,虽然 MQTT 协议的名称有 Message 和 Queue 两个词,但是它并不是像 RabbitMQ 那样的消息队列,这是初学者最容易搞混的一个问题。
与传统的消息队列相比,MQTT 协议有以下几点不同:
- 传统消息队列在发送消息前必须先创建相应的队列。在 MQTT 协议中,不需要预先创建要发布的主题(可订阅的 Topic);
- 传统消息队列中,未被消费的消息会被保存在某个队列中,直到有一个消费者将其消费。在 MQTT 协议中,如果发布一个没有被任何客户端订阅的消息,这个消息将被直接扔掉;
- 传统消息队列中,一个消息只能被一个客户端获取。在 MQTT 协议中,一个消息可以被多个订阅者获取,MQTT 协议也不支持指定消息被单一的客户端获取。
MQTT协议的通信模型
就像我们之前提到的,MQTT 协议的通信是通过发布/订阅的方式来实现的,消息的发布方和订阅方通过这种方式进行解耦,它们之间没有直接的连接,所以需要一个中间方来对信息进行转发和存储。在 MQTT 协议里,我们称这个中间方为 Broker,称连接到 Broker 的订阅方和发布方为 Client。
一次典型的 MQTT 协议消息通信流程如下图所示:

图 1 MQTT 协议消息通信流程
- 发布方和订阅方都建立了到 Broker 的 TCP 连接;
- 订阅方告知 Broker 它要订阅的消息主题(Topic);
- 发布方将消息发送到 Broker,并指定消息的主题;
- Broker 接收到消息以后,检查都有哪些订阅方订阅了这个主题,然后将消息发送到这些订阅方;
- 订阅方从 Broker 获取该消息;
- 如果某个订阅方此时处于离线状态,Broker 可以先为它保存此条消息,当订阅方下次连接到 Broker 的时候,再将这条消息发送到订阅方。
后续将发送方称为 Publisher,将订阅方称为 Subscriber。
MQTT的不同版本
MQTT 协议目前有 2 个常用版本——3.1.1 和 5.0,支持和使用最广泛的版本是 3.1.1。2017 年 8 月,OASIS MQTT Technical Committee 发布了 MQTT 5.0 草案。2018 年,MQTT 5.0 正式发布。MQTT 5.0 并未改变 MQTT 协议通信的架构和流程,而是在 3.1.1 的基础上推出了一些新特性,用来解决 3.1.1 版本的缺陷和不足,提供更多功能,方便应用的开发。MQTT 5.0 并不向下兼容 MQTT 3.1.1。
MQTT Client
任何终端,无论是嵌入式设备,还是服务器,只要运行了 MQTT 协议的库或者代码并连接了 MQTT Broker,我们都称其为 MQTT Client。Publisher 和 Subscriber 都属于 Client。一个 Client 是 Publisher 还是 Subscriber,只取决于该 Client 当前的状态——是在发布消息还是在订阅消息。当然,一个 Client 可以同时是 Publisher 和 Subscriber。
在大多数情况下,我们不需要自己按照 MQTT 协议规范来实现一个 MQTT Client,因为 MQTT Client 库在很多语言中都有实现,包括 Android、Arduino、Ruby、C、C++、C#、Go、iOS、Java、JavaScript 以及 .NET 等。
下图展示了 MQTT Client 在各个平台上的实现:

图 2 MQTT Client 在各个平台上的实现
MQTT Broker
搭建一个完整的 MQTT 协议环境,除了需要 MQTT Client 外,我们还需要一个 MQTT Broker。如前文所述,Broker 负责接收 Publisher 的消息,并将消息发送给相应的 Subscriber,它是整个 MQTT 协议订阅/发布的核心。
在实际应用中,一个 MQTT Broker 还应该提供如下功能:
- 可以对 Client 的接入进行授权,并对 Client 进行权限控制;
- 可以横向扩展,比如集群,满足海量的 Client 接入;
- 有较好的扩展性,可以比较方便地接入现有业务系统;
- 易于监控,满足高可用性。
下面列举了几个比较常用的 MQTT Broker。
1) Mosquitto
Mosquitto 是一款用 C语言编写的开源 MQTT Broker,单机配置和运行 Mosquitto 比较简单,官方并没有集群的解决方案。如果要扩展 Mosquitto 的功能,比如自定义的验证方式,实现过程比较复杂,对接现有的业务系统等也较为复杂,需要对 Mosquitto 代码比较熟悉。
如果只是想搭建一个测试 Broker 或者单机验证功能的环境,Mosquitto 还是比较合适的,但如果想在生产系统中使用,则需要自行解决集群和扩展的问题。
2) EMQX
EMQX 是由我国开发并提供商业支持的 MQTT Broker。EMQX 同时提供开源社区版和付费企业版。EMQX 是用 Erlang 语言编写的,官方提供集群解决方案,并可以通过编写插件的方式对 Broker 的功能进行扩展。
EMQX 的开发者社区比较活跃,青云提供的物联网套件就是基于 EMQX 的。在生产系统中使用 EMQX 已经有几年了,EMQX 的性能在笔者看来是很不错的,唯一的缺点就是 Erlang 这门语言比较小众,而且学习曲线较陡,编写插件的时候可能需要重新学习一门语言。
3) HiveMQ
HiveMQ 是用 Java 编写的 MQTT Broker,支持集群,同时也可以通过插件的方式对功能进行扩展。Java 语言的受众较广,功能扩展相对简单。不过 HiveMQ 是闭源的,只有付费企业版。
4) VerneMQ
VerneMQ 是开源的、用 Erlang 编写的 MQTT Broker,且由一家位于瑞士的公司提供商业服务。VerneMQ 同样支持集群,并可以使用插件的方式对功能进行扩展。除了上面提到的几个常用的 MQTT Broker,你还可以在 https://github.com/mqtt/mqtt.github.io/wiki/servers 找到更多的 MQTT Broker。
除了自建 Broker,我们还可以使用前面提到的阿里云、腾讯云、青云之类的云服务商提供的物联网云平台的 MQTT 协议服务。如果只是抱着学习或者测试的目的,还可以使用一些公共的 MQTT Broker。
MQTT协议数据包格式
不同于 HTTP,MQTT 协议使用的是二进制数据包。MQTT 协议的数据包非常简单,一个 MQTT 协议数据包由固定头(Fix Header)、可变头(Variable Header)、消息体(Payload)这 3 个部分依次组成:
- 固定头:存在于所有的MQTT协议数据包中,用于表示数据包类型及对应标识,表明数据包大小。
- 可变头:存在于部分类型的MQTT协议数据包中,具体内容由相应类型的数据包决定。
- 消息体:存在于部分MQTT协议数据包中,存储消息的具体数据。
这里我们首先看一下固定头,可变头和消息体将在讲解各种具体类型的 MQTT 协议数据包的时候详细讨论。
MQTT 协议数据包的固定头格式如下图所示:

图 3 MQTT协议数据包的固定头格式
1) 数据包类型
MQTT 协议数据包的固定头的第一个字节的高 4 位用于指定该数据包的类型。MQTT 协议数据包类型如下表所示:
名称 | 值 | 方向 | 描述 |
---|---|---|---|
Reserved | 0 | 不可用 | 保留位 |
CONNECT | 1 | Client 到 Broker | Client 请求连接到 Broker |
CONNACK | 2 | Broker 到 Client | 连接确认 |
PUBLISH | 3 | 双向 | 发布消息 |
PUBACK | 4 | 双向 | 发布确认 |
PUBREC | 5 | 双向 | 发布收到 |
PUBREL | 6 | 双向 | 发布释放 |
PUBCOMP | 7 | 双向 | 发布完成 |
SUBSCRIBE | 8 | Client 到 Broker | Client 请求订阅 |
SUBACK | 9 | Broker 到 Client | 订阅确认 |
UNSUBSCRIBE | 10 | Client 到 Broker | Client 请求取消订阅 |
UNSUBACK | 11 | Broker 到 Client | 取消订阅确认 |
PINGREQ | 12 | Client 到 Broker | PING 请求 |
PINGRESP | 13 | Broker 到 Client | PING 应答 |
DISCONNECT | 14 | Client 到 Broker | Client 主动中断连接 |
Auth | 15 | 双向 | 用于交换授权信息(MQTT5.0) |
2) 数据包标识位
MQTT 协议数据包的固定头的第一个字节的低 4 位用于指定数据包的标识位(Flag)。在不同类型的数据包中,标识位的定义是不一样的,每种数据包对应的标识位如下表所示。
数据包 | 标识位 | 位 3 | 位 2 | 位 1 | 位 0 |
---|---|---|---|---|---|
CONNECT | 保留位 | 0 | 0 | 0 | 0 |
CONNACK | 保留位 | 0 | 0 | 0 | 0 |
PUBLISH | MQTT3.1.1/5.0 使用 | DUP | QoS | QoS | Retain |
PUBACK | 保留位 | 0 | 0 | 0 | 0 |
PUBREC | 保留位 | 0 | 0 | 0 | 0 |
PUBREL | 保留位 | 0 | 0 | 0 | 0 |
PUBCOMP | 保留位 | 0 | 0 | 0 | 0 |
SUBSCRIBE | 保留位 | 0 | 0 | 0 | 0 |
SUBACK | 保留位 | 0 | 0 | 0 | 0 |
UNSUBSCRIBE | 保留位 | 0 | 0 | 0 | 0 |
UNSUBACK | 保留位 | 0 | 0 | 0 | 0 |
PINGREQ | 保留位 | 0 | 0 | 0 | 0 |
PINGRESP | 保留位 | 0 | 0 | 0 | 0 |
Auth | 保留位 | 0 | 0 | 0 | 0 |
3) 数据包剩余长度
从固定位的第二个字节开始,是用于标识当前数据包剩余长度的字段,剩余长度等于可变头长度加上消息体长度。这个字段最少为 1 个字节,最多为 4 个字节。其中,每一个字节的最高位叫作延续位(Continuation Bit),用于标识在这个字节之后是否还有一个用于表示剩余长度的字节。剩下的低 7 位用于标识值,范围为 0~127。
例如,剩余长度字段的第一个字节的最高位为 1,那么意味着剩余长度至少还有 1 个字节,然后继续读下一个字节,下一个字节的最高位为 0,那么剩余长度字段到此为止,一共 2 个字节。
剩余长度字段可标识的数据包长度如下表所示:
字节数 | 可标识的最小长度 | 可标识的最大长度 |
---|---|---|
1 | 0 (0x00) | 127 (0x7F) |
2 | 128 (0x80, 0x01) | 16383 (0xFF, 0x7F) |
3 | 16384 (0x80, 0x80, 0x01) | 2097151 (0xFF, 0xFF, 0x7F) |
4 | 2097152 (0x80, 0x80, 0x80, 0x01) | 268435455 (0xFF, 0xFF, 0xFF, 0x7F) |
所以,4 个字节最多可标识的数据包长度为(0xFF,0xFF,0xFF,0x7F)=268435455 字节,即 256MB,这是 MQTT 协议中数据包的最大长度。