之前的文章介绍了 mqtt 控制报文的基本结构,后面的文章会接着之前的主题,继续详细介绍各类控制报文的具体内容。本篇文章介绍了连接建立与保持相关的控制报文结构,包括 CONNECT、CONNACK、DISCONNECT、PINGREQ 和 PINGRESP 控制报文。
CONNECT - 连接服务端
客户端到服务器端的网络连接建立后(TCP),客户端发送给服务端的第一个报文必须是 CONNECT 报文。
在一个网络连接上,客户端只能发送一次 CONNECT 报文,服务端要将收到第二个 CONNECT 报文视为违规报文并断开与客户端的连接。
固定报头
可变报头
CONNECT 报文的可变报头按下列次序包含四个字段:协议名(Protocol Name),协议级别(Protocol
Level),连接标志(Connect Flags)和保持连接(Keep Alive)。
协议名
后四个字节是协议名,为 “MQTT” 的 UTF-8 编码。
协议级别
该字段用来表示客户端使用的协议级别,对于 mqtt 3.1.1 版本,该字段的值为 0x04。如果服务器端发现自己不支持该协议版本,会在 CONNACK 报文中返回 0x01 标识,断开与客户端的连接,后续会在 CONNACK 报文中介绍。
连接标志
连接标志字节包含一些用于指定 mqtt 连接行为的参数。它还指出有效载荷中的字段是否存在。
Reserved 保留位:服务端必须验证 CONNECT 控制报文的保留标志位(第 0 位)是否为 0,如果不为 0 必须断开客户端连接
清除会话
对于客户端,会话状态保存的有:
- 已经发送给服务端,但是还没有完成确认的 QoS1 和 QoS2 级别的消息
- 已从服务端接收,但是还没有完成确认的 QoS2 级别的消息
对于服务器端,会话状态保存的有:
会话是否存在,即使会话状态的其它部分都是空。
客户端的订阅信息。
已经发送给客户端,但是还没有被完成确认的 QoS1 和 QoS2 级别的消息。
已从客户端接收,但是还没有完成确认的 QoS2 级别的消息。
即将传输给客户端的 QoS1 和 QoS2 级别的消息。
可选,准备发送给客户端的 QoS0 级别的消息。
当清除会话位设置为 0 时,即不清除会话
- 服务端必须基于当前会话(使用客户端标识符识别)的状态恢复与客户端的通信。如果没有与这个客户端标识符关联的会话,服务端必须创建一个新的会话。在连接断开之后,当连接断开后,客户端和服务端必须保存会话信息。
- 会话连接断开之后,服务端必须将之后的 QoS1 和 QoS2 级别的消息保存为会话状态的一部分
当清除会话位设置为 1 时,即清除会话
- 客户端和服务端必须丢弃之前的任何会话并开始一个新的会话。会话仅持续和网络连接同样长的时间。与这个会话关联的状态数据不能被任何之后的会话重用。
遗嘱标识 :遗嘱标志(Will Flag)被设置为 1,表示如果连接请求被接受了,遗嘱(Will Message)消息必须被存储在服务端并且与这个网络连接关联。之后网络连接关闭时,服务端必须发布这个遗嘱消息,除非服务端收到 DISCONNECT 报文时删除了这个遗嘱消息 。
一般在如下情况下会发布遗嘱消息:
- 服务端检测到了一个 I/O 错误或者网络故障。
- 客户端在保持连接(Keep Alive)的时间内未能通讯。
- 客户端没有先发送 DISCONNECT 报文直接关闭了网络连接。
- 由于协议错误服务端关闭了网络连接
如果遗嘱消息被启用,连接标志中的 Will QoS 和 Will Retain 字段会被服务端用到,同时有效载荷中必须包含 Will Topic 和 Will Message 字段。
遗嘱 QoS:这两 bit 用于指定发布遗嘱消息时使用的服务质量等级,可以为 0x0,0x1,0x2
遗嘱保留:如果遗嘱消息被发布时需要保留,需要指定这一位的值。
- 如果遗嘱标志(Will Flag)被设置为 0,遗嘱保留(Will Retain)标志也必须设置为0
- 如果遗嘱标志(Will Flag)被设置为 1:
- 如果遗嘱保留(Will Retain)被设置为 0,服务端必须将遗消息当作非保留消息发布
- 如果遗嘱保留(Will Retain)被设置为 1,服务端必须将遗嘱消息当作保留消息发布
用户名标志
- 如果用户名(User Name)标志被设置为 0,有效载荷中不能包含用户名字段
- 如果用户名(User Name)标志被设置为 1,有效载荷中必须包含用户名字段
密码标志
- 如果密码(Password)标志被设置为 0,有效载荷中不能包含密码字段
- 如果密码(Password)标志被设置为 1,有效载荷中必须包含密码字段
- 如果用户名标志被设置为 0,密码标志也必须设置为 0
保存连接
保持连接(Keep Aive)是一个以秒为单位的时间间隔,表示为一个 16 位的字,它是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。客户端负责保证控制报文发送的时间间隔不超过保持连接的值。如果没有任何其它的控制报文可以发送,客户端必须发送一个PINGREQ 报文。
- 不管保持连接的值是多少,客户端任何时候都可以发送 PINGREQ 报文,并且使用 PINGRESP 报文判断网络和服务端的活动状态。
- 如果保持连接的值非零,并且服务端在 1.5 倍的 保持连接时间内没有收到客户端的控制报文,它必须断开客户端的网络连接,认为网络连接已断开。
- 客户端发送了 PINGREQ 报文之后,如果在合理的时间内仍没有收到 PINGRESP 报文,它应该断开到服务端的网络连接。
最后,用一张图总结 CONNECT 报文的可变报头:
有效负载
CONNECT 报文的有效载荷(payload)包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否包含这些字段。如果包含的话,必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码,它们均是 UTF-8 编码的字符串,结构如下:
客户端标识符
服务端使用客户端标识符 (ClientId) 识别客户端。连接服务端的每个客户端都有唯一的客户端标识符(ClientId)。客户端和服务端都必须使用 ClientId 识别两者之间的 MQTT 会话相关的状态 。
客户端标识符 (ClientId) 必须存在而且必须是 CONNECT 报文有效载荷的第一个字段。
遗嘱主题
如果遗嘱标志(Will Flag)被设置为 1,有效载荷的下一个字段是遗嘱主题(Will Topic)。
遗嘱消息
如果遗嘱标志(Will Flag)被设置为 1,有效载荷的下一个字段是遗嘱消息。遗嘱消息定义了将被发布到遗嘱主题的应用消息。
这个字段由一个两字节的长度和遗嘱消息的有效载荷组成,表示为零字节或多个字节序列。长度给出了跟在后面的数据的字节数,不包含长度字段本身占用的两个字节。遗嘱消息被发布到遗嘱主题时,它的有效载荷只包含这个字段的数据部分,不包含开头的两个长度字节。
用户名
如果用户名(User Name)标志被设置为 1,有效载荷的下一个字段就是它,服务端可以将它用于身份验证和授权。
密码
如果密码(Password)标志被设置为 1,有效载荷的下一个字段就是它。密码字段包含一个两字节的长度字段,长度表示二进制数据的字节数(不包含长度字段本身占用的两个字节),后面跟着0到 65535 字节的二进制数据。
CONNACK - 连接确认
服务端发送 CONNACK 报文响应从客户端收到的 CONNECT 报文。服务端发送给客户端的第一个报文必须是CONNACK。如果客户端在合理的时间内没有收到服务端的 CONNACK报文,客户端应该关闭网络连接。合理的时间取决于应用的类型和通信基础设施。
固定报头
剩余长度字段表示 CONNACK 可变报头+有效负载的长度,CONNACK 报文并没有有效负载,因此,剩余长度仅表示可变报头的长度。对于 CONNACK 报文这个值等于 2。
可变报头
第 1 个字节是连接确认标志,位 [7:1] 是保留位且必须设置为 0。第 0 位是当前会话(Session Present)标志。
当前会话标志
如果服务端收到清理会话(CleanSession)标志为 1 的连接,必须将 CONNACK 报文中的当前会话设置(Session Present)标志为 0。
如果服务端收到清理会话 (CleanSession)标志为 0 的连接,当前会话标志的值取决于服务端是否已经保存了 ClientId 对应客户端的会话状态。如果服务端已经保存了会话状态,它必须将 CONNACK 报文中的当前会话标志设置为 1 。如果服务端没有已保存的会话状态,它必须将 CONNACK 报文中的当前会话设置为 0。
客户端可以使用下面的方式来清除会话状态:断开连接,将清理会话标志设置为 1,再次连接,然后再次断开连接。
连接返回码
如果服务端收到一个合法的 CONNECT 报文,但出于某些原因无法处理它,服务端应该尝试发送一个包含非零返回码,并关闭连接。
DISCONNECT - 断开连接
DISCONNECT 报文是客户端发给服务端的最后一个控制报文。表示客户端正常断开连接。简单的功能也意味着该报文结构会非常简单,只有固定报头部分。
固定报头
客户端发送 DISCONNECT 报文之后:
必须关闭网络连接。
不能通过那个网络连接再发送任何控制报文。
服务端在收到 DISCONNECT 报文时:
- 必须丢弃任何与当前连接关联的未发布的遗嘱消息(正常使用 DISCONNECT 不会触发遗嘱)。
- 应该关闭网络连接,如果客户端 还没有这么做。
PINGREQ - 心跳请求
客户端发送 PINGREQ 报文给服务端的。用于:
在没有任何其它控制报文从客户端发给服务的时,告知服务端客户端还活着。
请求服务端发送 响应确认它还活着。
使用网络以确认网络连接没有断开。
固定报头
心跳请求没有可变报头和有效载荷
PINGRESP - 心跳响应
服务端发送 PINGRESP 报文响应客户端的 PINGREQ 报文。表示服务端还活着。
固定报头
心跳响应没有可变报头和有效载荷