背景
上一讲 MQTT 协议学习:通信报文的构成介绍了在MQTT通信中,各报文的通信流程;从本讲开始,我们开始介绍实际中使用的报文,以及它们的组成。
CONNECT - 连接请求 报文
客户端到服务端的网络连接建立后,客户端发送给服务端的第一个报文必须是CONNECT, 连接服务端
报文。
在一个网络连接上,客户端只能发送一次CONNECT报文。服务端必须将客户端发送的第二个CONNECT报文当作协议违规处理并断开客户端的连接。
服务端可以检查CONNECT报文的内容是不是满足任何进一步的限制,可以执行身份验证和授权检查。如果任何一项检查没通过,它应该发送一个适当的、返回码非零的CONNACK响应,并且必须关闭这个网络连接。
有效载荷包含一个或多个编码的字段。包括客户端的唯一标识符,Will主题,Will消息,用户名和密码。除了客户端标识之外,其它的字段都是可选的,基于标志位来决定可变报头中是否需要包含这些字段。
CONNECT 报文的固定头
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
byte 1 | MQTT报文类型 (0x1) | 保留位(0x0) |
CONNECT 的 可变报头
CONNECT报文的可变报头按下列次序包含四个字段:协议名(Protocol Name),协议级别(Protocol Level),连接标志(Connect Flags)和保持连接(Keep Alive)。
0040 10 30 00 04 4d 51 54 54 04 c2 00 3c 00 17 6d 6f .0..MQTT...<..mo
0050 73 71 2d 66 5a 4a 69 30 75 51 78 38 4d 6b 55 64 sq-fZJi0uQx8MkUd
0060 55 61 42 52 5a 00 05 61 64 6d 69 6e 00 04 72 6f UaBRZ..admin..ro
0070 6f 74 ot
协议名称 Protocol Name
协议名称(Protocol Name):值固定为字符 “MQTT”的UTF-8编码的字符串,MQTT规范的后续版本不会改变这个字符串的偏移和长度。占用6个字节。
说明 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
---|---|---|---|---|---|---|---|---|---|
协议名 | |||||||||
byte 1 | 长度 MSB (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
byte 2 | 长度 LSB (4) | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
byte 3 | ‘M’ | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 |
byte 4 | ‘Q’ | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 |
byte 5 | ‘T’ | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 |
byte 6 | ‘T’ | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 |
协议级别 Protocol Level
协议版本(Protocol Level):对 MQTT 3.1.1 来说,值为 4。占用1个字节。
对于3.1.1版协议,协议级别字段的值是4(0x04)。如果发现不支持的协议级别,服务端必须给发送一个返回码为0x01(不支持的协议级别)的CONNACK报文响应CONNECT报文,然后断开客户端的连接
协议级别 | 说明 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|
byte 7 | Level(4) | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
连接标志 Connect Flags
连接标志字节包含一些用于指定MQTT连接行为的参数。它还指出有效载荷中的字段是否存在。 byte8[0]必须为0,否则断开连接。
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
连接标志 | User Name Flag | Password Flag | Will Retain | Will Qos | Will Qos | Will Flag | Clean Session | Reserver |
byte 8 | X | X | X | X | X | X | X | 0 |
关于 遗嘱(byte8[5:2]) 有关的知识可以参考《MQTT 协议学习:Retained(保留消息) 和LWT(最后遗嘱)》
关于 会话(byte8[1]) 有关知识可以参考 《MQTT 协议学习:003-MQTT协议中的Qos等级》
用户名标识(User Name Flag):消息体中是1否0有用户名字段。
密码标识(Password Flag):消息体中是1否0有密码字段。
遗嘱消息 Retain 标识(Will Retain):标识遗嘱消息是1否0是 Retain 消息。
如果 遗嘱标识 被设置为0,遗嘱保留(Will Retain)标志也必须设置为0。
如果遗嘱标志被设置为1:
- 如果遗嘱保留被设置为0,服务端必须将遗嘱消息当作非保留消息发布。
- 如果遗嘱保留被设置为1,服务端必须将遗嘱消息当作保留消息发布。
遗嘱消息 QOS 标识(Will Qos):标识遗嘱消息的 Qos,2bit。
如果遗嘱标志被设置为0,遗嘱QoS也必须设置为0(0x00)
遗嘱标识(Will Flag):标识是1否0使用遗嘱消息。
会话清除标识(Clean Session):标识 Client 是0否1建立一个持久化的会话。当 Clean Session 的标识设为 0 时,代表 Client 希望建立一个持久会话的连接,Broker 将存储该 Client 订阅的主题和未接受的消息,否则(设置为1) Broker 不会存储这些数据,同时在建立连接时清除这个 Client 之前存在的持久化会话所保存的数据。持久会话只在 QoS 等级 大于等于 1 的消息 中有效。
下面是引用中文文档的2段描述:
- 如果清理会话(Clean Session)标志被设置为0,服务端必须基于当前会话(使用客户端标识符识别)的状态恢复与客户端的通信。如果没有与这个客户端标识符关联的会话,服务端必须创建一个新的会话。在连接断开之后,当连接断开后,客户端和服务端必须保存会话信息。当清理会话标志为0的会话连接断开之后,服务端必须将之后的QoS 1和QoS 2级别的消息保存为会话状态的一部分,如果这些消息匹配断开连接时客户端的任何订阅。服务端也可以保存满足相同条件的QoS 0级别的消息。
- 如果清理会话(Clean Session)标志被设置为1,客户端和服务端必须丢弃之前的任何会话并开始一个新的会话。会话仅持续和网络连接同样长的时间。与这个会话关联的状态数据不能被任何之后的会话重用。
小技巧:
要确保不丢失连接断开期间的消息,需要使用QoS 1或 QoS 2级别,同时将清理会话标志设置为0。
清理会话标志0的客户端连接时,如果打算在之后的某个时间点重连到这个服务端,客户端连接应该只使用清理会话标志0。当客户端决定之后不再使用这个会话时,应该将清理会话标志设置为1最后再连接一次,然后断开连接。
连接保活(Keep Alive): 设置一个单位为秒的时间间隔,指在 client 传输完成一个控制报文的时刻到发送下一个报文的时刻,client 与 broker 两者之间允许空闲的最大时间间隔。,可以参考:《MQTT 协议学习:Keep Alive 和连接保活》。
- Keep Alive 的最大值为 18 小时 12 分 15 秒(65535秒,实际上就是 0xff, 1个字);
- Keep Alive 值如果设为 0 的话,代表不使用 Keep Alive 机制。
Bit | 7 6 5 4 3 2 1 0 |
---|---|
byte 9 | 保持连接 Keep Alive MSB |
byte 10 | 保持连接 Keep Alive LSB |
CONNECT 的 有效载荷 Payload
CONNECT报文的有效载荷(payload)包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否包含这些字段。
如果包含的话,必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码。
客户端标识符 Client Identifier
客户端标识符 (Client Id) 必须存在而且必须是CONNECT报文有效载荷的第1个字段,必须用UTF-8进行编码。
这个字段由一个两字节的长度和客户端标识符的实际部分组成,表示为零字节或多个字节序列。长度给出了跟在后面的数据的字节数,不包含长度字段本身占用的两个字节。
服务端使用客户端标识符 (Client Id) 识别客户端。连接服务端的每个客户端都有唯一的客户端标识符(Client Id)。客户端和服务端都必须使用ClientId识别两者之间的MQTT会话相关的状态,它是在处理QoS级别1和2的消息ID中的关键。
服务端必须允许1到23个字节长的UTF-8编码的客户端标识符,客户端标识符只能包含大小写字母和数字
即 : "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
UTF-8 编码字符串的结构 Structure of UTF-8 encoded strings
二进制位 | 7-0 |
---|---|
byte 1 | 字符串长度的最高有效字节(MSB) |
byte 2 | 字符串长度的最低有效字节(LSB) |
byte 3 …. | 如果长度大于0,这里是UTF-8编码的字符数据。 |
服务端可以允许编码后超过23个字节的客户端标识符 (ClientId)。服务端可以允许包含非大小写字母和数字字符的客户端标识符。
服务端可以允许客户端提供一个零字节的客户端标识符 (ClientId) ,如果这样做了,服务端必须将这看作特殊情况并分配唯一的客户端标识符给那个客户端。然后它必须假设客户端提供了那个唯一的客户端标识符,正常处理这个CONNECT报文。
如果服务端拒绝了这个ClientId,它必须发送返回码为0x02(表示标识符不合格)的CONNACK报文响应客户端的CONNECT报文,然后关闭网络连接。
如果客户端提供了一个零字节的客户端标识符,它必须同时将清理会话标志设置为1。
如果客户端提供的Client Id为零字节且清理会话标志为0,服务端必须发送返回码为0x02(表示标识符不合格)的CONNACK报文响应客户端的CONNECT报文,然后关闭网络连接。
对于服务端来说:如果Client Id表明客户端已经连接到这个服务端,那么服务端必须断开原有的客户端连接。
遗嘱主题 Will Topic
如果遗嘱标志被设置为1,有效载荷的下一个字段是遗嘱主题(Will Topic)。遗嘱主题必须UTF-8编码字符串。
这个字段由一个两字节的长度和遗嘱主题的实际部分组成,表示为零字节或多个字节序列。长度给出了跟在后面的数据的字节数,不包含长度字段本身占用的两个字节。
二进制位 | 7-0 |
---|---|
byte 1 | 字符串长度的最高有效字节(MSB) |
byte 2 | 字符串长度的最低有效字节(LSB) |
byte 3 …. | 如果长度大于0,这里是UTF-8编码的字符数据。 |
遗嘱消息 Will Message
如果遗嘱标志被设置为1,有效载荷的下一个字段是遗嘱消息。遗嘱消息定义了将被发布到遗嘱主题的应用消息。
这个字段由一个两字节的长度和遗嘱消息的有效载荷组成,表示为零字节或多个字节序列。长度给出了跟在后面的数据的字节数,不包含长度字段本身占用的两个字节。
二进制位 | 7-0 |
---|---|
byte 1 | 字符串长度的最高有效字节(MSB) |
byte 2 | 字符串长度的最低有效字节(LSB) |
byte 3 …. | 如果长度大于0,这里是UTF-8编码的字符数据。 |
遗嘱消息被发布到遗嘱主题时,它的有效载荷只包含这个字段的数据部分,不包含开头的两个长度字节。
用户名 User Name
如果用户名(User Name)标志被设置为1,有效载荷的下一个字段就是它。用户名必须是UTF-8编码字符串。服务端可以将它用于身份验证和授权。
这个字段由一个两字节的长度和用户名的实际数据组成,表示为零字节或多个字节序列。长度给出了跟在后面的数据的字节数,不包含长度字段本身占用的两个字节。
Bit | 7 - 0 |
---|---|
byte 1 | 数据长度 MSB |
byte 2 | 数据长度 LSB |
byte 3 …. | 如果长度大于0,这里就是数据部分 |
密码 Password
如果密码(Password)标志被设置为1,有效载荷的下一个字段就是它。密码字段包含一个两字节的长度字段,长度表示二进制数据的字节数(不包含长度字段本身占用的两个字节),后面跟着0到65535字节的二进制数据。
Bit | 7 - 0 |
---|---|
byte 1 | 数据长度 MSB |
byte 2 | 数据长度 LSB |
byte 3 …. | 如果长度大于0,这里就是数据部分 |
CONNECT 连接成功以后的响应
服务器对于 CONNECT 请求的验证步骤:
1)网络连接建立后,如果服务端在合理的时间内没有收到CONNECT报文,服务端应该关闭这个连接。
2)服务端必须按照3.1节的要求验证CONNECT报文,如果报文不符合规范,服务端不发送CONNACK报文直接关闭网络连接。
3)服务端可以检查CONNECT报文的内容是不是满足任何进一步的限制,可以执行身份验证和授权检查。如果任何一项检查没通过,按照3.2节的描述,它应该发送一个适当的、返回码非零的CONNACK响应,并且必须关闭这个网络连接。
如果验证成功,服务端会执行下列步骤。
1)如果Client Id表明客户端已经连接到这个服务端,那么服务端必须断开原有的客户端连接。
2)服务端必须按照会执行清理会话的过程。
3)服务端必须发送返回码为零的CONNACK报文作为CONNECT报文的确认响应。
4)开始消息分发和保持连接状态监视。
允许客户端在发送CONNECT报文之后,可以不需要等待服务端的CONNACK报文立即发送其它的控制报文。如果服务端拒绝了客户端的CONNECT请求,那么它不能处理客户端在CONNECT报文之后发送的任何数据。
CONNACK – 确认连接请求 报文
服务端发送CONNACK报文响应从客户端收到的CONNECT报文。服务端发送给客户端的第一个报文必须是CONNACK。
如果客户端在合理的时间内没有收到服务端的CONNACK报文,客户端应该关闭网络连接。
CONNACK 的固定头中的 剩余长度为2。(byte2 = 0x02)
# 抓包情况
MQ Telemetry Transport Protocol, Connect Ack
Header Flags: 0x20, Message Type: Connect Ack
Msg Len: 2
Acknowledge Flags: 0x00
0000 000. = Reserved: Not set
.... ...0 = Session Present: Not set
Return Code: Connection Accepted (0)
0040 20 02 00 00 ...
CONNACK 的 可变报头
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
---|---|---|---|---|---|---|---|---|
连接确认标志 | ||||||||
byte 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | SP |
连接返回码 | ||||||||
byte 2 | X | X | X | X | X | X | X | X |
连接确认标志 Connect Acknowledge Flags
连接确认标志 Connect Acknowledge Flags 只用了第0位。
byte1[7:1] 是保留位且必须设置为0;第0 (也叫 SP)位(byte1[0]) 是当前会话(Session Present)标志。
如果服务端收到清理会话(Clean Session)标志为1的连接,除了将CONNACK报文中的返回码设置为0之外,还必须将CONNACK报文中的当前会话设置(Session Present)标志为0 。
如果服务端收到一个Clean Session为0的连接,当前会话标志的值取决于服务端是否已经保存了ClientId对应客户端的会话状态。
- 如果服务端已经保存了会话状态,它必须将CONNACK报文中的SP设置为1。
- 如果服务端没有已保存的会话状态,它必须将CONNACK报文中的SP设置为0;还需要将CONNACK报文中的返回码设置为0。(此时代表连接成功)
SP使服务端和客户端在是否有已存储的会话状态上保持一致。
一旦完成了会话的初始化设置,已经保存会话状态的客户端将期望服务端维持它存储的会话状态。如果客户端从服务端收到的当前的值与预期的不同,客户端可以选择继续这个会话或者断开连接。客户端可以丢弃客户端和服务端之间的会话状态,方法是,断开连接,将清理会话标志设置为1,再次连接,然后再次断开连接。
如果服务端发送了一个包含非零返回码的CONNACK报文,它必须将当前会话标志设置为0
连接返回码 Connect Return code
连接返回码 Connect Return code 是 :连接返回码字段使用一个字节的无符号值,在下表中列出。
值 | 返回码响应 | 描述 |
---|---|---|
0 | 0x00连接已接受 | 连接已被服务端接受 |
1 | 0x01连接已拒绝,不支持的协议版本 | 服务端不支持客户端请求的MQTT协议级别 |
2 | 0x02连接已拒绝,不合格的客户端标识符 | 客户端标识符是正确的UTF-8编码,但服务端不允许使用 |
3 | 0x03连接已拒绝,服务端不可用 | 网络连接已建立,但MQTT服务不可用 |
4 | 0x04连接已拒绝,无效的用户名或密码 | 用户名或密码的数据格式无效 |
5 | 0x05连接已拒绝,未授权 | 客户端未被授权连接到此服务器 |
6-255 | 保留 |
如果服务端收到一个合法的CONNECT报文,但出于某些原因无法处理它,服务端应该尝试发送一个包含非零返回码(表格中的某一个)的CONNACK报文。如果服务端发送了一个包含非零返回码的CONNACK报文,那么它必须关闭网络连接。
CONNACK 的 有效载荷
无。
DISCONNECT –断开连接
DISCONNECT报文是客户端发给服务端的最后一个控制报文。表示客户端正常断开连接。
DISCONNECT
数据包没有可变头(Variable header)和消息体(Payload),那么,DISCONNECT
报文的全部内容(共2个字节)就是 : 0xe0 0x00