【基础】MQTT -- MQTT协议详解
- 与 Broker 建立连接
- CONNECT 数据包
- CONNACK 数据包
- 断开连接
- DISCONNECT 数据包
- 订阅与发布
- PUBLISH 数据包
- SUBSCRIBE 数据包
- SUBACK 数据包
- UNSUBSCRIBE 数据包
- UNSUBACK 数据包
本文内容针对 MQTT 3.1.1 版本,从连接、发布与订阅等方面对协议内容进行介绍。
MQTT 基础内容可以参看文章:【基础】MQTT – MQTT 协议基础
与 Broker 建立连接
客户端 Client 在进行消息的订阅与发布之前,需要与 Broker 服务器进行连接:
-
Client 向 Broker 发送 CONNECT 数据包;
-
Broker 接收到 CONNECT 数据包后,若允许 Client 接入,则生成返回码为 0 的 CONNACK 包表示连接建立成功;若允许接入则生成返回码为非零值的 CONNACK 包以标识接入失败原因,并断开 TCP 连接;
CONNECT 数据包
数据包结构包括固定头、可变头以及消息体三部分。
固定头
固定头中字节1的高四位为 1 标识为 CONNECT 数据包,如下图所示。
可变头
可变头由四部分构成,分别为协议名称、协议版本、连接标识以及 Keepalive。
协议名称是一个 UTF-8 编码的字符串,该字符串存在两个字节的前缀用于指示字符串的长度。协议名称的值固定为“MQTT”,加上长度指示字段共 6 个字节,入下图所示。
若协议的名称不正确,Broker 将会断开与 client 的连接。
协议版本占据一个字节,是一个无符号的整数,MQTT 3.1.1 版本的版本号为 4,如下图。
连接标识占据一个字节,字节不同的位用于标识不同的连接选项,如下所示。
-
用户名标识(User Name Flag):标识消息体中是否含有用户名字段,占据 1 bit,取值为 0 或 1;
-
密码标识(Password Flag):标识消息体重是否含有密码字段,占据 1bit,取值为 0 或 1;
-
遗嘱消息 Retain 标识(Will Retain):标识遗嘱消息是否为 Retain 消息,占据 1 bit,取值为 0 或 1;
-
遗嘱消息 QoS 标识(Will QoS):标识遗嘱消息的 QoS,占据 2 bit,取值为 0、1、2;
-
遗嘱标识(Will Flag):标识是否使用遗嘱消息,占据 1 bit,取值为 0 或 1;
-
会话清除标识(Clean Session):标识 client 是否建立持久会话,占据 1 bit,取值为 0 或 1。当该标识为 0 时代表建立持久化连接,Broker 将存储该 client 订阅的主题和未接收的消息;若标识为 1 则 Broker 不会存储上述数据,同时会在建立连接时清除之前保存的该 client 的数据。
Keepalive 占据两个字节,代表连接保活设置,该设置是一个单位为秒的时间间隔,在所设置的时间间隔内 Broker 与 client 至少要由一次消息交互,否则 Broker 与 client 会认为他们之间的连接已经断开。
消息体
消息体可存在五部分内容,分别为客户端标识符、遗嘱主题、遗嘱 QoS、遗嘱消息、用户名和密码。处客户端标识符外,其余四部分内容是可选的,根据可变头内的对应标识来判断相应的内容是否存在。
消息体包含有两个字节的前缀,用于指示数据内容的长度,如下图所示。
消息体内容详情如下:
-
客户端标识符(Client Identifier):用于标识 Client 的字段。MQTT 3.1.1中该字段的长度为 1~23 个字节,且只能包含数字和英文字母的大小写。Broker 通过该字段唯一标识 Client。(MQTT 协议中要求 Client 在连接时必须携带客户端标识符,但也允许 Broker 实现连接时接受标识符为空的 CONNECT 数据包,此时该客户端的标识由 Broker 唯一分配。若需要使用持久性会话,则必须为 Client 设定一个唯一的标识符);
-
用户名(Username):若可变头中用户名标识为 1,则证明消息体中包含用户名字段,Broker 可根据用户名和密码对接入的 Client 进行身份校验。(不同的 Client 需要使用不用的客户端标识符,但可以使用相同的用户名密码进行连接,类似于端游的客户端);
-
密码(Password):若可变头中密码标识为 1,则证明消息体中包含密码字段;
-
遗嘱主题(Will Topic):若可变头中遗嘱标识为 1,则证明消息体中将包含遗嘱主题。当 Client 非正常断开连接时,Broker 将向指定的主题发布遗嘱消息;
-
遗嘱消息(Will Message):若可变头中遗嘱标识为 1,则证明消息体中将包含遗嘱消息。当 Client 非正常断开连接时,Broker 将向指定的遗嘱主题发布遗嘱消息字段的内容;
CONNACK 数据包
当 Broker 收到 Client 发送的 CONNECT 数据包后,将会检查并校验数据报的内容,然后回复 CONNACK 数据包给 Client。
固定头
固定头中的数据包类型字段值为 2 代表 CONNACK 数据包,且其数据包剩余长度固定为 2。
可变头
可变头包含两个字节,分别指示连接确认标识和连接返回码:
-
连接确认标识:高 7 位都是保留的,必须设置为 0;最后 1 位为会话存在标识,值为 0 或者 1。若 Client 连接时设置的 Clean Session = 1,则该值始终为 0;若 Client 连接时设置的 Clean Session = 0,则存在两种情况:当 Broker 中保存了该 Client 之前的持久性会话时,该值为 1,若不存在会话数据,则该值为 0。
-
返回码:用于标识 Client 与 Broker 的连接是否建立成功:
-
0
:表示连接已建立; -
1
:表示连接被拒绝,不允许的协议版本; -
2
:表示连接被拒绝,Client Identifier 被拒绝(格式不规范); -
3
:表示连接被拒绝,服务器不可用; -
4
:表示连接被拒绝,错误的用户名或密码; -
5
:表示连接被拒绝,未授权(该返回码一般用于不使用用户名和密码而使用 ip 地址和 Client Identifier 进行验证时标识客户端没有通过验证);
-
消息体
CONNACK 数据包没有消息体。
断开连接
MQTT 协议中,断开连接可以由 Client 或 Broker 二者任意一方发起。
Client 主动断开连接
Client 主动断开连接需要向 Broker 发送一个 DISCONNECT 数据包,该数据包只存在固定头,不存在可变头和消息体。
Broker 主动断开连接
MQTT 协议规定,在没有收到 Client 的 DISCONNECT 数据包之前都应该保持连接,只有当 Broker 在 Keepalive 的时间间隔内没有收到 Client 发送的任何 MQTT 协议数据包时才会主动断开连接。
Broker 在主动断开连接之前不需要像 Client 发送任何数据包,直接关闭底层 TCP 连接即可。
DISCONNECT 数据包
主动断开连接的数据包主要涉及 MQTT 遗嘱消息的特性,Broker 会依据该数据包断定 Client 是正常断开连接。若直接断开 TCP 连接,则 Broker 会认为是异常断开,则会向指定的主题推送遗嘱消息。
固定头
订阅与发布
这里需要首先明确两组概念:发布者(Pubhsher)和订阅者(Subscriber)、发送方(Sender)和接收方(Receiver)。
发布者(Pubhsher)和订阅者(Subscriber)
Pubhsher 与 Subscriber 是相对于 Topic 来说的概念,其对象只能是 Client。当一个 Client 向某个 Topic 推送消息时,那么它就是发布者;当 Client 订阅了某个 Topic,那么它就是订阅者。
发送方(Sender)和接收方(Receiver)
Sender 与 Receiver 是相对于消息来说的概念,其对象可以为 Broker 或者 Client。谁输出消息,谁就是发送方;消息的送达点则为接收方。
PUBLISH 数据包
PUBLISH 包用于在 Sender 与 Receiver 之间传输消息数据。
固定头
固定头中数据包类型字段值为 3 标识 PUBLISH 数据包,当前字节的低四位为标识位,存在如下三个字段:
-
消息重复标识(DUP):占据 1 bit,值为 0 或 1。当该字段值为 1 时,表示该消息是一条重发消息,因为接收方没有确认收到之前的消息。该标识只在 QoS 大于 0 时使用;
-
QoS:占据 2 bit,值为 0、1、2,该字段代表 PUBLISH 消息的服务质量级别;
-
保留标识(Retain):占据 1 bit,值为 0 或 1。当消息的发送方为 Client 且该字段为 1 时,表示 Broker 应保存该消息,并在之后有新的 Client 订阅该消息中指定的主题时主动向该客户端推送该条消息(这种消息也被称为 Retain 消息);当消息的发送方为 Broker 且该字段为 1 时,表示该消息是一条 Retain 消息;
固定头结构如下所示:
可变头
PUBLISH 数据包的可变头包含两部分信息:主题名以及包标识符,其中包标识符仅在 QoS 为 1 或 2 的 PUBLISH 数据包中存在。
主题名是一个 UTF-8 编码的字符串,其前两个字节用于指示主题字符串的长度,因此主题名的最大长度为 65535 字节。
主题的命名建议遵守下列规范:
-
主题名称应包含层级,不同的层级之间使用
\
划分; -
主题名称开头不要使用
\
; -
主题中不要使用空格;
-
主题只使用 ASCII 字符;
-
主题名称应在保证可读性的前提下尽量短;
-
主题名称对大小写敏感;
-
可以将设备的唯一标识添加到主题当中;
-
以
$
开头的主题为 Broker 的预留主题,应用程序不要使用该字符开头的主题;
可变头的结构如下所示:
消息体
PUBLISH 数据包的消息体中即要发送的额数据,其可以为任意格式的数据(二进制数据、JSON、文本…)。消息体的长度可以用固定头中的数据包剩余长度减去可变头的长度获取。
SUBSCRIBE 数据包
Client 若想接收某个主题的消息,则需要向 Broker 发送 SUBSCRIBE 数据包订阅对应主题。
固定头
固定头中数据包类型字段的值为 8,表示 SUBSCRIBE 数据包。
可变头
可变头内容为包标识符,占据两字节。数据包标识需要保证从 Sender 到 Receiver 的一次消息交互中唯一。
消息体
SUBSCRIBE 数据包的消息体由 Client 要订阅的主题列表构成。其主题名可以包含通配符,包括单层通配符+
和多层通配符#
。使用通配符可以订阅满足匹配条件的所有主题。通常将该包中的主题名称为主题过滤器。
其中:
-
单层通配符可用于指代任意一个层级,例如
company/2ndfloor/+/electric
; -
多层通配符可用于指代任意多个层级,但其必须用在
/
后面,且必须为最后一个字符,例如company/2ndfloor/#
每一个主题过滤器都是一个 UTF-8 编码的字符串,该字符串后会跟着一个字节用于描述订阅该主题的 QoS,其格式如下:
SUBACK 数据包
Broker 在收到 SUBSCRIBE 数据包后,都会回复一个 SUBACK 数据包作为应答。
固定头
固定头中的数据包类型字段为 9 标识 SUBACK 数据包。
可变头
可变头仅包含占据两个字节的包标识符。
消息体
SUBACK 数据包的消息体包含一组返回码,返回码的数量与 SUBSCRIBE 数据包的订阅列表相对应,用于指示订阅结果。
返回码及其含义如下所示:
-
0
:订阅成功,且最大可用 QoS 为 0; -
1
:订阅成功,且最大可用 QoS 为 1; -
2
:订阅成功,且最大可用 QoS 为 2; -
128
:订阅失败;
UNSUBSCRIBE 数据包
UNSUBSCRIBE 数据包用于取消某些主题的订阅。
固定头
固定头中数据包类型字段的值为 10,表示 UNSUBSCRIBE 数据包。
可变头
可变头仅包含占据两个字节的包标识符。
消息体
UNSUBSCRIBE 数据包的消息体主要包含要取消的主题过滤器列表,主题过滤规则与 SUBSCRIBE 数据包规则相同,但是不包含 QoS 字段。
需要注意,在取消订阅时,主题名中的通配符不起作用,仅作为字符使用。当且仅当取消订阅的主题名的每一个字符都与订阅时指定的主题名相同时,才会取消主题的订阅。
UNSUBACK 数据包
Broker 在收到 UNSUBSCRIBE 后,会回复给 Client 一个 UNSUBACK 包作为响应。
固定头
固定头中数据包类型字段的值为 11,表示 UNSUBACK 数据包。
可变头
可变头仅包含占据两个字节的包标识符。
消息体
UNSUBACK 数据包不存在消息体。