请点赞关注,你的支持对我意义重大。
🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。
大家好,我是小彭。
过去两年,我们在掘金平台上发表过一些文章,小彭也收到了大家的意见和鼓励。最近,我会陆续搬运到公众号上。
MQTT 是一种基于发布 - 订阅模型的消息传递协议,在物联网和移动应用有较广泛的应用。如果你的目标是冲击中高级工程师岗位,MQTT 或许是一个不错的亮点。最近,我还发现很多候选人会在简历中写自己 “熟悉 MQTT 协议”,但多数人只是停留在了解或用过的程度。
这篇文章里,我将与你探讨 MQTT 协议的 工作原理 & 协议消息格式 & 核心特性,实战的部分我们会在下篇文章中讨论。如果能帮上忙,请务必点赞加关注,给小彭一点创作的动力。
记录:2022 年 9 月 9 日修订:优化文章结构
学习路线图:
MQTT (Message Queuing Telemetry Transport,消息队列遥测传输) 是一种基于 TCP/IP 协议族的应用层协议。MQTT 协议是专门针对硬件性能低下 & 网络状况不稳定的场景设计的,这使得 MQTT 在物联网和移动应用等受限场景得到广泛应用。
目前,MQTT 主要分为两个大版本:
MQTT 是基于发布 - 订阅模型 (pub/sub) 的消息传递协议,与请求 - 响应模型不同,发布 - 订阅模型主要有三种角色:publisher & subscriber & subscriber:
publisher & subscriber (发布者 & 订阅者): 是指通过网络连接到 MQTT broker 的设备,也叫 客户端 (client)。一个客户端既可以作为消息发布者,也可以作为消息订阅者;
broker (代理): 代理是整个发布 - 订阅模型的核心,也叫 服务端。
当 client 发布某个主题的消息时,broker 会将该消息分发给任何已订阅该主题的 client。通常来说,client 不会存储消息,一旦消息被发送到这些 client,消息就会从 broker 上删除。另外,保留消息、持久连接和服务质量 QoS 可能会导致消息临时存储在 broker 上。
发布 - 订阅模式使得 消息的发布者和订阅者解耦,主要体现为空间解耦和时间解耦:
空间解耦 / 设备解耦: 发布者和订阅者通过 broker 进行消息传递,相互之间感知不到对方的存在。当一个客户端断线时,整个系统可以继续工作;
时间解耦: publisher 和 subscriber 不一定需要同时运行;
图片引用自 https://juejin.cn/post/6976441705067184135 —— cxuan 著
特性 | MQTT 协议 | HTTP 协议 |
---|---|---|
传输层 | TCP | TCP 或 UDP |
分发模型 | 发布 - 订阅模型 | 请求 - 响应模型 |
分发关系 | 1 对 0/1/N | 1 对 1 |
数据安全 | 使用 SSL/TLS | 不一定采用 HTTPS |
加密 | 应用层对有效载荷加密 | 不在应用层加密 |
消息头大小 | 较小 | 较大 |
1、MQTT 协议基于传输层 TCP 协议,而 HTTP 可以基于 TCP 或 UDP(HTTP/3);
2、MQTT 协议采用发布 - 订阅模型,同一个设备既可以是发布者也可以是订阅者;而 HTTP 协议采用请求 - 响应模型,一个设备作为请求方,另一个设备作为响应方;
3、MQTT 消息分发可以是 1 对 0/1/N,而 HTTP 消息分发是 1 对 1;
4、MQTT 协议使用 SSL/TLS 来确保安全,而 HTTP 协议并不规定使用 HTTPS;
5、MQTT 协议在应用层对有效载荷 (payloads) 加密,而 HTTPS 协议不在应用层加密,在传输数据前不会加密数据;
6、MQTT 消息头较小,而 HTTP 消息头较大(HTTP/2 有头部压缩);
物联网和移动应用场景的特点是硬件性能低下和网络状况不稳定,而 MQTT 协议就是专门针对这种环境设计的,主要在四个方面有优势:
1、架构设计: MQTT 协议采用发布 - 订阅模型,使得消息发布者和消息订阅者互相解耦,当一个客户端断线时,整个系统可以继续工作。这使得 MQTT 在网络质量的场景下更具优势;
2、消息大小: MQTT 协议具有非常小的消息头,这使得 MQTT 协议更适应低带宽网络环境;
3、交付能力: MQTT 协议提供了更丰富的消息交付保证能力,它定义了三种消息发布服务质量 (QoS):“最多发一次”、“最少发一次” 和 “正好发一次”。其中,“正好一次” 用于计费系统和 IM App 推送中,能确保用户收到且只收到一次;
4、间歇性连接: MQTT 提供了遗嘱消息和保留消息的特性。遗嘱消息使得客户端端断开连接时,所有订阅的客户端都能收到来自代理的消息;保留消息意味着新订阅的客户端可以立即获得保留的消息(类似粘性消息)。这使得 MQTT 协议更适合网络不稳定的 间歇性连接的场景。
HTTP/2 是 HTTP/1.x 的升级,主要体现在:利用 “多路复用和二进制分帧” 来解决队首阻塞问题,降低了通信时延;利用 “头部压缩” 减少消息头部,降低了传输开销;实现了 服务器推送,允许在不发起请求的情况下将数据推送到客户端,弥补了 Http/1.x 依赖 Websockets 才能实现推送的缺陷。这些改进使得 HTTP/2 也具有适应物联网场景的条件;
WebSockets 是在 Web 浏览器和 Web 服务器之间进行握手的协议,它降低了使用 Http/1.x 进行双工通信的开销。随着 HTTP/2 成为标准,对 websockets 的需求可能会下降;
MQTT 是基于发布订阅模型的协议,因其带宽消耗小而被广泛应用于物联网协议。
结论:这三种协议并没有绝对的优胜者,最好的协议取决于具体的需求和限制条件。但如果只从带宽、电池、功能多样性这些基本条件看,MQTT 在其中是更占优的选择。例如,我司的 IM 产品在 App 端是采用 MQTT 协议的实现,而在 Web 端因为有良好的 WebSocket 能力基础,所以采用的是 WebSocket 传输 MQTT 格式消息。
MQTT 协议的设计特性中包含了一项 “高可靠性交付”,它需要一个保证可靠的底层传输层协议,因此 TCP 协议、TLS 协议、WebSocket 协议都可能作为 MQTT 的底层协议。而无连接的 UDP 协议会丢失或重排数据,不能满足 MQTT 协议的传输需要。
1、基于二进制: MQTT 是一种基于二进制的协议,所谓基于二进制,是指 MQTT 协议操作的元素是二进制数据而不是文本数据;
2、命令 & 命令确认格式: MQTT 消息采用命令 & 命令确认的格式,每个命令消息都有一个关联的命令确认消息,两个消息之间会通过一个 ”包唯一标识“ 字段进行关联???TODO。这与 TCP 的报文确认应答机制是类似的,不过两者的颗粒度是不同的,MQTT 是对整个应用层消息的确认,而 TCP 是对传输层报文段的确认,或者说是对序列号的确认;
3、消息头很小: MQTT 消息头最小只需要 2 字节。
一个 MQTT 消息由三部分组成:
MQTT 消息结构 | 描述 | 长度 |
---|---|---|
固定报头(Fixed header) | 存在于所有 MQTT 消息中 | 2 到 5 字节 |
可变报头(Variable header) | 存在于部分 MQTT 消息中 | 0 或 N 字节 |
载荷(Payloads) | 存在于部分 MQTT 消息中 | 0 或 N 字节 |
1、固定报头
所有 MQTT 消息都包含一个固定报头,固定报头由消息类型、标志位和剩余长度三个部分。固定报头长度为 2 ~ 5 字节,具体取决于 “剩余长度(Remaining Length)” 的大小,剩余长度表示当前消息剩余部分的字节数,包括可变报头和有效载荷的长度,但不包括剩余长度字段本身的字节数。
提示: 如何判断剩余长度的字节数,采用的是前缀无歧义的表示法。
固定报头格式如下:
MQTT 消息类型(MQTT Control Packet type)汇总如下:
消息类型 | 值 | 消息流转方向 | 描述 | 需要有效载荷 |
---|---|---|---|---|
Reserved | 0 | 禁止 | 保留 | / |
CONNECT | 1 | => | 客户端请求连接服务器 | ✔ |
CONNACK | 2 | <= | CONNECT 消息确认 | ✖ |
PUBLISH | 3 | <==> | 客户端发布消息 | 可选 |
PUBACK | 4 | <==> | PUBLISH 消息确认(QoS 1) | ✖ |
PUBREC | 5 | <==> | 发布收到(保证交付第一步) | ✖ |
PUBREL | 6 | <==> | 发布释放(保证交付第二步) | ✖ |
PUBCOMP | 7 | <==> | 发布完成(保证交付第三步) | ✖ |
SUBSCRIBE | 8 | => | 客户端订阅消息 | ✔ |
SUBACK | 9 | <= | SUBSCRIBE 消息确认 | ✔ |
UNSUBSCRIBE | 10 | => | 客户端取消订阅 | ✔ |
UNSUBACK | 11 | <= | UNSUBSCRIBE 消息确认 | ✖ |
PINGREQ | 12 | => | 心跳请求 | ✖ |
PINGRESP | 13 | <= | PINGREQ 消息确认 | ✖ |
DISCONNECT | 14 | => | 客户端断开连接 | ✖ |
Reserved | 15 | 禁止 | 保留 | / |
2、可变报头
不同消息的可变报头内容不一样,不过其中有一个比较通用的字段:
3、载荷
某些 MQTT 消息会包含一个有效载荷,对于 PUBLISH 消息来说,有效载荷就是应用消息。
上一节,我们提到在 MQTT 固定报文头部中会标记 MQTT 消息类型(MQTT Control Packet type) ,这一节我们具体讨论下这些消息类型。
MQTT 的连接总是发生在 client 和 broker 之间,两个 client 之间不会互相感知。请求连接时,client 会向 broker 发送 CONNECT
连接消息,broker 接受连接后会响应 CONNACK
连接确认消息。一旦连接建立,连接会一直保持打开状态,直到 client 发送 DISCONNECT
断开连接消息或连接异常中断。
CONNECT 请求连接:
CONNECT
是 client 发送给 broker 的首个消息,并且在一次连接中,client 只能发送一次 CONNECT
消息,发送的第二个 CONNECT
消息会被 broker 当作违反协议处理,并断开连接。在 CONNECT
消息中,主要包含以下内容:
ClientId 客户端名称: 所有 client 都需要一个名称,broker 会根据 client 名称来跟踪会话,因此 client 名称必须是 唯一的。如果连接到 broker 时已经有一个重名的 clientId,那么会先断开现有 client 的连接,这将可能导致断开和连接的死循环,因为大多数 MQTT client 有断线重连机制;
CleanSession 持久会话: 当 client 连接到 broker 时,可以使用持久连接或非持久连接,CleanSession
标志决定是否使用持久连接(当 CleanSession = 0
时表示持久连接),对于持久会话,broker 会存储会话状态;而对于非持久会话,broker 不会存储 client 的任何内容,具体见第 4.2 节 · 会话状态;
UserName & Password 用户名 & 密码: 用于 broker 认证和授权;
KeepAlive 保活探测间隔: KeepAlive 是以秒单位的时间间隔,指 client 发送两次消息的最大时间间隔,当 client 和 borker 之间在一段时间内没有数据交互时,client 会发送 PINGREQ 探测消息 用于判断连接是否正常,来决定是否要关闭该连接。KeepAlive 是 MQTT 协议的保活机制,从作用上看与 TCP 的 Keepalive 保活机制是非常类似的,不过 MQTT 协议的保活机制是应用层 client 实现的,而 TCP 的保活机制是 “内核” 实现的。
Last Will Message 遗嘱消息: 遗嘱消息用于通知意外停机的 client,每个 client 在连接时可以设置一个遗嘱消息,这个遗嘱消息会存储在 broker 上。当 client 因 “非正常原因” 断开连接时,broker 会将遗嘱消息分发给订阅了 “Will”
主题的 client。另外,这条遗嘱消息还可以设置是否为保留消息(Will Retain
标志)以及服务质量等级(Will Qos
)。
CONNACK 连接确认:
CONNACK
消息用于确认 CONNECT
消息。CONNECT
是 client 发送给 broker 的首个消息,相应地,broker 发送给 client 的首个消息一定是 CONNACK
消息。在 CONNACK
消息中,主要包含以下内容:
SessionPresent 持久会话: SessionPresent
标志表示当前 broker 是否持有与 client 的持久会话。当 broker 接收了一个非持久会话连接(CleanSession = 1
),SessionPresent 的值始终为 0;而当 broker 接收了一个持久会话连接(CleanSession = 0
),则 SessionPresent 的值取决于 broker 是否存储了 ClientId 的会话状态;
ReturnCode 响应码: 用于表示连接请求是否成功,如果响应码不为 0,则表示连接失败。
具体取值如下表:
返回码 | 描述 |
---|---|
0 | 连接已接受 |
1 | 连接被拒绝,不可接受的协议版本 |
2 | 连接被拒绝,标识符被拒绝 |
3 | 连接被拒绝,服务器不可用 |
4 | 连接被拒绝,用户名或密码错误 |
5 | 连接被拒绝,未授权 |
DISCONNECT 断开连接:
DISCONNECT
消息由 client 发送给 broker,用于断开连接。DISCONNECT 消息没有可变报头和有效载荷,也没有对应的确认应答消息,表示一个干净利索地断开连接操作。断开连接后,client 不能再发送除 CONNECT
消息之外的消息,broker 也需要丢弃和当前会话的遗嘱消息。
MQTT 是基于发布订阅模型的协议,在建立连接后,client 可以向 broker 订阅感兴趣的一个或多个话题。
3.2.1 SUBSCRIBE 订阅
SUBSCRIBE
消息由 client 发送给 broker,用于订阅感兴趣的话题,SUBSCRIBE
消息主要包含以下内容:
SUBSCRIBE
消息的有效载荷中至少需要包含一个话题过滤器,每个过滤器由一个 Topic 和 QoS 组成,其中的 QoS 指定了指定 client 接受的最大 OoS 等级。3.2.2 SUBACK 订阅确认
SUBACK
消息用于确认 SUBSCRIBE
消息。SUBACK
消息主要包含以下内容:
具体取值如下表:
返回码 | 描述 |
---|---|
0x00 | 订阅成功,最大 QoS 为 0 |
0x01 | 订阅成功,最大 QoS 为 1 |
0x02 | 订阅成功,最大 QoS 为 2 |
0x80 | 订阅失败 |
3.2.3 UNSUBSCRIBE 退订
UNSUBSCRIBE
消息由 client 发送给 broker,用于退订不感兴趣的话题,UNSUBSCRIBE
消息主要包含以下内容:
3.2.4 UNSUBACK 退订确认
UNSUBACK
消息用于确认 UNSUBSCRIBE
消息。UNSUBACK
消息非常简单,只有一个包唯一标识(位于可变报头)。
当 MQTT client 在连接到 broker 之后就可以发送消息了,每条 PUBLISH
消息都包含一个 topic ,broker 会根据 topic 将消息发送给感兴趣的 client。除此之外,每条消息还会包含一个 Payload,Payload 是真正发布的应用消息,载荷的内容和格式由应用层决定,MQTT 协议层不关心。
3.3.1 PUBLISH 发布
PUBLISH
消息可以由 client 发送给 broker,也可以由 broker 发送给 client,用来运送应用层消息。PUBLISH 消息主要包含以下内容:
QoS 发布服务质量标志: 标记当前 PUBLISH
消息传送的交付保证水平,分为三个等级,具体见第 4.3 节 · 发布服务质量:
RETAIN 保留消息标志: 标记当前 PUBLISH
消息是否为保留消息,当 client 发送给 broker 的 PUBLISH 消息标记 RETAIN = 1 时,broker 会存储该消息,当新的 client 注册订阅时,并且匹配该消息主题时,该保留消息会发送给订阅者,具体见第 4.4 节 · 保留消息;
DUP 重传标志: 标记当前的 PUBLISH
/ PUBREL
消息是否为重复发送消息。 MQTT 协议规定了两种消息重传的场景,具体见第 4.5 节 · 消息重传;
TopicName 话题名: 表示载荷数据的发布通道;
包唯一标识: 只有 QoS1 和 OoS2 的 PUBLISH
消息中存在;
载荷(应用消息): PUBLISH
消息的载荷是真正发布的应用消息,载荷的内容和格式由应用层决定,MQTT 协议层不关心。另外,载荷的数据长度等于:固定报头中的剩余长度(Remaining Lenght)- 可变报头的长度,载荷长度也可以为零。
3.3.2 发布确认
PUBLISH 消息的接收方需要发送确认应答,不同 QoS 等级的 PUBLISH 消息响应的消息不同:
发布服务质量等级 QoS | 期望的确认应答 |
---|---|
QoS 0 | 无确认应答 |
OoS 1 | PUBACK 消息 |
OoS 2 | PUBREC 消息 PUBREL 消息 PUBCOMP 消息 |
当 client 和 broker 在一段时间内没有数据交互时,client 会发送 PINGREQ
探测消息,用于判断连接是否正常,来决定是否要关闭该连接,这就是 MQTT 协议的保活机制。
3.4.1 PINGREQ 探测消息
PINGREQ 消息由 client 发送给 broker。
3.4.2 PINGRESP 探测确认
PINGRESP 消息由 broker 发送给 client,代表 client 是存活的。
MQTT 主题本质上是一种 “寻址形式”,用于将应用层消息分发到期望的客户端。MQTT 主题是一种类似于文件系统的分层结构,使用 “/” 正斜杠 作为分隔符。
4.1.1 主题格式规范
4.1.2 主题通配符
客户端订阅主题时,可以订阅确定的主题(例如 “group/group123”),也可以使用 “通配符” 来同时订阅多个主题。需要注意的是:在发布消息时不允许使用主题通配符,client 每次发布消息只能发布到单个主题。
+
是单级通配符,单级通配符可以用于任何一个主题级别,但只能匹配一个级别。例如:主题 | 匹配主题举例 |
---|---|
group/+/123 | group/vip/123 group/temp/123 |
#
是多级通配符,多级通配符可以匹配多个连续级别。需要注意,多级通配符只能用于主题的最后一个级别。例如:主题 | 匹配主题举例 |
---|---|
group/# | group group/123 group/vip/123 group/temp/123 |
4.1.3 $SYS 主题
$SYS
主题是 broker 上默认创建的只读主题,除此之外,broker 不会默认创建任何主题,所有主题都是由客户端订阅或发布才创建的,都不是永久性的。关于 $SYS
主题的更多介绍在 这里
4.1.4 主题的生存周期
当 client 连接到 broker 时,可以使用持久连接或非持久连接,这是通过 CONNECT
消息中的 CleanSession 标志来决定的(当 CleanSession = 0
时表示持久连接)。对于持久会话,broker 会存储会话状态;而对于非持久会话,broker 不会存储 client 的任何内容。会话状态主要包含以下内容:
4.2.1 客户端存储的会话状态
PUBLISH
消息;PUBLISH
消息。4.2.2 服务端存储的会话状态
PUBLISH
消息;PUBLISH
消息;PUBLISH
消息;PUBLISH
消息。提示: 保留消息不属于会话状态,在会话结束时不会被删除,broker 应该一直存储保留消息直到被 client 删除。
QoS 0 等级的 PUBLISH
消息的交付能力完全依赖于底层传输层,QoS 1 和 QoS 2 等级开始在应用层提高 PUBLISH
消息的交付能力。当消息丢失时,发送端会重新发送早前尝试发送过的 PUBLISH
消息(DUP = 1),接收者收到消息也会发送确认响应消息。
4.3.1 QoS 0 · 最多发一次
在 QoS 0 的等级的 PUBLISH
消息中不包含包唯一标识。发送者不考虑消息交付结果,接收者也不发送响应。接收者最多只能收到一次消息,也有可能一次也收不到。
4.3.2 OoS 1 · 最少发一次
在 QoS 1 等级的 PUBLISH
消息中包含包唯一标识,发送方会一直将该消息当作 “未确认” 的消息,直到收到对应的 PUBACK
确认消息。具体消息流如下:
提示: 实际的消息传递是在 client 和 broker 之间进行的,这 4 个步骤是简化为发送方和接收方之间的消息传递。
PUBLISH
(QoS = 1, DUP = 0, )消息;PUBLISH
消息,并响应 PUBACK
()确认消息;PUBACK
消息,并删除存储的应用消息。4.3.3 QoS2 · 正好发一次
QoS 2 是最高的服务质量,保证消息不会丢失也不会重复,缺点是会增加开销。在 QoS 2 等级的 PUBLISH
消息中包含包唯一标识,发送者会一直将该消息当作 “未确认” 的消息,知道收到对应的 PUBCOMP
确认消息。
PUBLISH
(QoS = 2, DUP = 0, )消息;PUBLISH
消息,并存储消息;PUBREC
()消息;PUBREC
消息,并发送 PUBREL
()消息;PUBCOMP
()消息;PUBCOMP
消息,并删除存储的应用消息。当 client 发布某个主题的消息时,broker 会将该消息分发给任何已订阅该主题的 client,随后这条消息会从 broker 上删除。可以设置 RETAIN
保留标志设置该 PUBLISH 消息为保留消息,broker 会存储该主题的最后一条保留消息,当新的 client 注册订阅时,并且匹配该消息主题时,该保留消息会发送给订阅者。需要注意:broker 只会为每个主题保存最近一条保留消息,新收到的 RETAIN = 1 的消息会覆盖原本那条保留消息;
持久会话 & 服务质量等级 & 保留消息都会影响新订阅者是否接受消息,总结如下表:
标记 DUP = 1
的消息是被重复发送的消息,MQTT 消息重传有 2 种场景:
需要注意:DUP 标志只对 OoS > 0 的消息有效,所有 QoS = 0 的消息 DUP 标志必须设置为 0;
TCP 协议有报文重传机制,为什么 MQTT 协议还有消息重传机制?
TCP 协议的报文重传机制是对所有 TCP 报文有效的重传机制,而 MQTT 协议的消息重传机制只对一小部分消息有效,用于实现更可靠的消息交付保证。虽然 TCP 协议在一般情况下可以保证不丢包,但是这并不是绝对的,依然存在请求超时或者连接中断等情况。而 MQTT 协议的 QoS 1 和 QoS 2 要求更可靠的交付能力,并且需要在客户端重连后也能保证交付。因此,MQTT 协议也定义了一个消息重传机制。
到这里,关于 MQTT 协议的工作原理 & 协议消息格式 & 核心特性等内容就介绍完了。我知道你应该会对 MQTT 协议的实战应用更加感兴趣,下一篇文章里,我将带你实现基于 MQTT 协议的 IM 服务,请关注。
我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐]私信我提问。