微信关注公众号【一只故事】,一起聊聊物联网,聊聊Java后端技术,聊聊你的故事
表哥,今天我们来聊聊 MQTT 协议吧。
已端好小板凳,前排围观
我们将从以下几个方面进行讲解:
为什么是 MQTT
MQTT 是目前使用最为广泛的物联网通讯协议,目前已占据了物联网通讯协议的半壁江山。
各大厂物联网开发平台都提供了对MQTT协议的支持,并占有很大比重。
那为什么是 MQTT 了?不妨看看官方协议文档给的摘要说明。
简单概括下来就是
- 使用发布/订阅消息模式,提供了一对多的消息分发和应用之间的解耦。
- 提供三种等级的消息服务质量:最多一次、至少一次和仅有一次。
- 很小的传输消耗和协议数据交换,最大限度减少网络流量
- 异常连接断开发生时,能通知到相关各方。
MQTT通讯模型
MQTT 是一个客户端服务端架构的发布/订阅模式的消息传输协议。在 MQTT 通讯模型中包括 MQTT Broker 和 MQTT Client,消息流转不是端到端的,而是 Client 与 Broker 之间的。
消息订阅者通过向Broker订阅某个主题,消息发布者发了个某个主题的消息,此时Broker会将此消息流转到订阅了该主题的订阅者。
看到此处的小伙伴会发现这和主流的消息队列,如rabbitmq、rocketmq的生产消费模型很像,其实不然,可以看看他们之间的对比。
传统的消息中间件,例如消息队列 MQ、消息队列 Kafka 等都是面向微服务大数据等领域,负责消息的存储和转发,消息的生产者和消费者都是服务端应用。这种设计很适合服务端技术栈固定、语言平台固定的场景。而移动互联网和 IoT 领域则有所不同,这类场景更侧重于多语言多平台的海量设备接入,消息的生产和消费过程的业务属性很突出,传统的消息中间件并不适合这些领域。
微消息队列 MQTT 在设计上是一个面向移动互联网和 IoT 领域的无状态网关,只关心海量移动端设备的接入、管理和消息传输,消息数据的存储则都会路由给后端存储产品。
适用场景 | |
MQTT | 面向移动端场景,移动端场景一般都具备海量设备,单设备数据较少的特点。因此,微消息队列 MQTT 适用于拥有大量在线客户端(很多企业设备端过万,甚至上百万),但每个客户端消息较少的场景。 |
消息队列 MQ | 面向服务端的消息引擎,主要用于服务组件之间的解耦、异步通知、削峰填谷等,服务器规模较小(极少企业服务器规模过万),但需要大量的消息处理,吞吐量要求高。因此,消息队列 MQ 适用于服务端进行大批量的数据处理和分析的场景。 |
更多详细对比,读者可以参考阿里云物联网开发平台介绍:
https://help.aliyun.com/document_detail/94521.html
MQTT主题
上面就提到过主题,主题是消息流转的媒介。订阅和发布必须要有主题,只有当订阅了某个主题之后,只能收到相应主题发送过来的消息,以此来达到设备和云端的通讯。一个 MQTT Client 可以同时订阅多个主题,同一个主题也可以被多个 MQTT Client 订阅。
主题层级分隔符
斜杠用于分割主题的每个层级,为主题提供一个分层结构。
单层通配符
加号 +
用于单个主题层级匹配的通配符。在主题过滤器的任意层级都可以使用单层通配符,包括第一个和最后一个层次。然而它必须占据过滤器的整个层级。也可以在主题过滤器的多个层级使用它。
例如 device/+
的主题可以匹配 /device/1
、/device/2
,但是不能匹配 /device/1/1
。
多层通配符
#
号用于匹配层级中任何层级的通配符。多层通配符表示它的父级和任意数量的自层级。多层通配符必须位于它自己的层级或者跟在主题层级分隔符的后面。不管哪种情况,它都必须是主题过滤器的最后一个字符。
例如 device/#
的主题都可以匹配 /device/1
、/device/2
,/device/1/1
的主题消息。
$ 号开头的主题
服务端不能将 $
字符开头的主题名匹配通配符(#
或 +
)开头的主题过滤器。服务端应该阻止客户端使用这种主题名与其它客户端交换消息。服务端实现可以将 $
开头的主题名用作其他目的。例如我们后面讲到的 MQTT 服务端 EMQX 就使用 $SYS
作为系统主题,客户端订阅相应的系统主题可以感知设备上下线状态,服务端运行数据等等
MQTT控制报文
MQTT Client 和 Broker 之间通过交换 MQTT 控制报文来通信。控制报文有很多,MQTT Client 向 MQTT Broker 发送连接报文,发布订阅主题消息报文等等。这一节描述这些报文的格式。 MQTT 控制报文格式部分组成:
- 固定报文头,所有控制报文都包含
- 可变报文头,部分控制报文包含
- 有效负载 Payload,部分控制报文包含
固定报文头
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
字节 1 | MQTT 数据包类型 | 用于指定控制报文类型的标志位 | ||||||
从第二个字节开始 | 剩余长度 |
固定报文头的高 4 位用于指定数据包的类型:
名字 | 值 | 报文流动方向 | 描述 |
Reserved | 0 | 禁止 | 保留 |
CONNECT | 1 | 客户端到服务端 | 客户端请求连接服务器 |
CONNACK | 2 | 服务端到客户端 | 连接报文确认 |
PUBLISH | 3 | 两个方向都允许 | 客户端向服务器发送消息 |
PUBACK | 4 | 两个方向都允许 | Qos1消息发布收到确认 |
PUBREC | 5 | 两个方向都允许 | 发布收到(保证交付第一步) |
PUBREL | 6 | 两个方向都允许 | 发布释放(保证交付第二步) |
PUBCOMP | 7 | 两个方向都允许 | Qos2 消息发布完成(保证消息第三步) |
SUBSCRIBE | 8 | 客户端到服务端 | 客户端请求订阅 |
SUBACK | 9 | 服务端到客户端 | 订阅请求报文确认 |
UNSUBSCRIBE | 10 | 客户端到服务端 | 客户端取消订阅请求 |
UNSUBACK | 11 | 服务端到客户端 | 取消订阅报文确认 |
PINGREQ | 12 | 客户端到服务端 | 心跳请求 |
PINGRESP | 13 | 服务端到客户端 | 心跳响应 |
DISCONNECT | 14 | 客户端到服务端 | 客户端断开连接 |
Reserved | 15 | 禁止 | 保留 |
固定报文头的低四位包含每个 MQTT 控制报文类型特定的标识。表格中的“保留”的标志位,都是保留给以后使用,这里不用过多在意。
控制报文 | 固定报文标识 | Bit 3 | Bit2 | Bit1 | Bit0 |
CONNECT | 保留 | 0 | 0 | 0 | 0 |
CONNACK | 保留 | 0 | 0 | 0 | 0 |
PUBLISH | 用于 MQTT 3.1.1 | DUP | QoS | QoS | RETAIN |
PUBACK | 保留 | 0 | 0 | 0 | 0 |
PUBREC | 保留 | 0 | 0 | 0 | 0 |
PUBREL | 保留 | 0 | 0 | 1 | 0 |
PUBCOMP | 保留 | 0 | 0 | 0 | 0 |
SUBSCRIBE | 保留 | 0 | 0 | 1 | 0 |
SUBACK | 保留 | 0 | 0 | 0 | 0 |
UNSUBSCRIBE | 保留 | 0 | 0 | 1 | 0 |
UNSUBACK | 保留 | 0 | 0 | 0 | 0 |
PINGREQ | 保留 | 0 | 0 | 0 | 0 |
PINGRESP | 保留 | 0 | 0 | 0 | 0 |
DISCONNECT | 保留 | 0 | 0 | 0 | 0 |
- DUP:控制报文的重复分发标识
- QoS:有两位,PUBLISH 报文服务质量等级(0、1、2)
- RETAIN :PUBLISH 报文保留标识
介绍 MQTT 特点的时候,我们提到过 MQTT Client 向 Broker 发送消息的时候可以指定消息质量:最多一次,最少一次和只有一次。至少一次肯定就意味着消息可能会重复发送,DUP 就用于标识是否是重复的报文,消息重发在 MQTT Client 库中已经默认你实现。RETAIN 保留消息,后面会详细讲到。
剩余长度
从第二字节开始,表示剩余长度。剩余长度用于标识可变报文头和负载数据的总的字节数,不包含其本身的字节数。
剩余长度使用一个变长的编码方案,最小是一个字节,最大是 4 个字节。每一个字节的最高位标识后面一个字节也是剩余长度标识,低 7 位有效位用于编码数据。所以按照最大是 4 个字节算,允许发送 (0xFF, 0xFF, 0xFF, 0x7F)大小的控制报文,约 256M。
所以一个 MQTT 控制报文最大传输的数据约为 256M。
字节数 | 最小值 | 最大值 |
1 | 0 (0x00) | 127 (0x7F) |
2 | 128 (0x80, 0x01) | 16 383 (0xFF, 0x7F) |
3 | 16 384 (0x80, 0x80, 0x01) | 2 097 151 (0xFF, 0xFF, 0x7F) |
4 | 2 097 152 (0x80, 0x80, 0x80, 0x01) | 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F) |
可变报文头
某些 MQTT 控制报文包含一个可变报头部分。它在固定报头和负载之间。可变报头的内容根据报文类型的不同而不同。可变报头的报文标识符(Packet Identifier)字段存在于在多个类型的报文里。
报文标识符 Packet Identifier
报文标识符只存在某些特定的控制报文中,如 SUBSCRIBE,UNSUBSCRIBE 和 PUBLISH(QoS 大于 0),用于标识该报文的唯一性。客户端向服务器发送这些唯一性的报文,服务器相应的,会带上该报文标识回复 ACK 给客户端。另外的对于 PUBLISH(QoS 大于 0)的报文,当消息重发的时候,报文标识符也不会改变。
客户端和服务端彼此独立地分配报文标识符。因此,客户端服务端组合使用相同的报文标识符可以实现并发的消息交换。
有效负载
某些 MQTT 控制报文在报文的最后部分包含一个有效载荷。对于 PUBLISH 来说有效载荷就是应用消息。
每个类型报文具体格式可以看看 MQTT 官方协议文档。协议文档可以公众号回复 [MQTT] 关键字领取
消息服务质量
MQTT 协议中规定了消息服务质量,它保证了在不同的网络环境下消息传递的可靠性,QoS 的设计是 MQTT 协议里的重点。作为专为物联网场景设计的协议,MQTT 的运行场景不仅仅是 PC,而是更广泛的窄带宽网络和低功耗设备,如果能在协议层解决传输质量的问题,将为物联网应用的开发提供极大便利。
MQTT 发布消息不是端到端的,是客户端与服务器之间的。订阅者收到 MQTT 消息的 QoS 级别,最终取决于发布消息的 QoS 和主题订阅的 QoS,准确来说是取两者的最小值。
发布消息的 QoS | 主题订阅的 QoS | 接受消息的 QoS |
0 | 0 | 0 |
0 | 1 | 0 |
0 | 2 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
1 | 2 | 1 |
2 | 0 | 0 |
2 | 1 | 1 |
2 | 2 | 2 |
QoS0:最多分发一次
当 QoS 为 0 时,消息的分发依赖于底层网络的能力。发布者只会发布一次消息,接收者不会应答消息,发布者也不会储存和重发消息。消息在这个等级下具有最高的传输效率,但可能送达一次也可能根本没送达。
这种情况下,如果开发人员想要保证消息的稳定送达,必须在业务上层设计一套消息重发机制。
QoS1:至少分发一次
当 QoS 为 1 时,可以保证消息至少送达一次。MQTT 通过简单的 ACK 机制来保证 QoS 1。发布者会发布消息,并等待接收者的 PUBACK 报文的应答,如果在规定的时间内没有收到 PUBACK 的应答,发布者会将消息的 DUP 置为 1 并重发消息。接收者接收到 QoS 为 1 的消息时应该回应 PUBACK 报文,接收者可能会多次接受同一个消息,无论 DUP 标志如何,接收者都会将收到的消息当作一个新的消息并发送 PUBACK 报文应答。
QoS2: 仅分发一次
当 QoS 为 2 时,发布者和订阅者通过两次会话来保证消息只被传递一次,这是最高等级的服务质量,消息丢失和重复都是不可接受的。使用这个服务质量等级会有额外的开销。
发布者发布 QoS 为 2 的消息之后,会将发布的消息储存起来并等待接收者回复 PUBREC 的消息,发送者收到 PUBREC 消息后,它就可以安全丢弃掉之前的发布消息,因为它已经知道接收者成功收到了消息。发布者会保存 PUBREC 消息并应答一个 PUBREL,等待接收者回复 PUBCOMP 消息,当发送者收到 PUBCOMP 消息之后会清空之前所保存的状态。
当接收者接收到一条 QoS 为 2 的 PUBLISH 消息时,他会处理此消息并返回一条 PUBREC 进行应答。当接收者收到 PUBREL 消息之后,它会丢弃掉所有已保存的状态,并回复 PUBCOMP。
无论在传输过程中何时出现丢包,发送端都负责重发上一条消息。不管发送端是 Publisher 还是 Broker,都是如此。因此,接收端也需要对每一条命令消息都进行应答。
如何选取 QoS
QoS 级别越高,流程越复杂,系统资源消耗越大。应用程序可以根据自己的网络场景和业务需求,选择合适的 QoS 级别,比如在同一个子网内部的服务间的消息交互往往选用 QoS 0;而通过互联网的实时消息通信往往选用 QoS 1;QoS 2 使用的场景相对少一些,适合一些支付请求之类的要求较高的场景。
MQTT中一些比较重要的名词概念
保留标志 RETAIN
在MQTT PUBLISH报文中设置发布的消息是否为保留消息,如果为 true,那么消息会一直驻留在 Broker 中,新上线的 Client 如果订阅了该主题的消息,那么就会收到该保留消息。这个功能使用的场景也非常多。
保留消息越来越多,势必会对服务器造成资源消耗,清除不必要的保留消息有两种方式:
- 向该主题发送一个空的消息
- Broker 设置保留消息的超时时间
保持时间 Keep Alive
在MQTT CONNECT报文中可以设置Keep Alive时间。保活时间指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。如果没有任何其它的控制报文可以发送,客户端必须发送一个 PINGREQ 报文(MQTT Client 包会帮我们定时发送心跳报文),否则 MQTT Broker 会强制关闭和 Client 之间的连接。这种情况经常出现在嵌入式设备网络不稳定的时候。
清除会话 Clean Session
在MQTT CONNECT报文中可以设置Clean Session 。当客户端指定持久会话时,也就是Clean Session设置为false,即时客户端断线,断线期间 Broker 会保留其他客户端向该客户端订阅的主题上发送的消息。当客户端重新上线后,会接收到离线期间的消息。
遗愿消息 Last Will
在MQTT CONNECT报文中可以设置遗愿主题和遗愿消息。
客户端连接服务器如果指定遗愿消息,那么该主题的遗愿消息会驻留在 MQTT Broker 中。
特别注意的是,当 MQTT 客户端异常下线时(客户端断开前未向服务器发送 DISCONNECT 消息,Broker通过心跳检测到客户端已下线),MQTT 消息服务器才会发布遗愿消息。MQTT 主动通过 DISCONNECT 报文断开与服务器连接时,则不会发送遗愿消息。
以上就是MQTT协议的核心内容,其实协议简单,关键是协议的实现者,需要在理解协议的基础之上去编码和大量的测试。表哥,你还有啥问题吗?
我看MQTT client已经有非常多语言库了,MQTT Broker你要啥好的推荐的吗?
给你一张图自己去领悟吧。
是目前比较火的一个MQTT Broker。表哥你不是netty玩的6吗,可以自己去实现一个,可以参考JMQTT的设计,JMQTT集群也有解决方案。
好的,我这就去了解了解