MQTT协议基础(3):MQTT协议中的连接
MQTT协议中连接相关知识包括CONNECT数据包、CONNACK数据包、DISCONNECT数据包等
建立连接的过程
建立连接的过程中,如果允许Client接入,则回复一个CONNACK包,该CONNACK包的返回码为0.
如果不允许接入,则回复一个返回码不为0的CONNAK包,之后断开底层TCP连接
CONNECT数据包
数据包格式
固定头
基本样式
关键信息:CONNECT数据包类型为1.
可变头
可变头由四部分组成:协议名称,协议版本,连接标识, Keepalive
- 协议名称
协议名称是一个UTF-8编码字符串,在MQTT协议中会有两个字节的前缀,用于标志字符串的长度。
协议名称的值固定为MQTT,加上前缀共有6个字节。
- 协议版本
长度为1个字节,是一个无符号整数,MQTT3.1.1的版本号为4.
- 连接标识长度为1个字节,字节中不同的位用于标识不同的连接选项。
各位代表的含义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17User Name flag
标识消息体中是否可以包含用户名字段。置0,不能包含。置1,必须包含。
Password Flag
标识消息体中是否可以包含密码字段。置0,不能包含。置1,必须包含。如果用户标志置0.密码也必须置0
Will Retain
标识遗嘱消息被发布时是否需要保留,置0,不保留。置1,保留。
Will QoS
标识遗嘱消息的QoS等级
Will Flag
标识是否使用遗嘱消息,置1,使用遗嘱消息,同时需要保证消息体中包含Will Topic 和 Will Message字段。置0,不使用
Clean Session
会话清除标识。标识是否建立一个持久化连接(带有状态的保存的连接)。置0,持久化连接。 置1,非持久化连接。- Keeplive
以秒为单位,十六位。client和broker之间在这个时间间隔内至少要有一次消息交互,否则会认为两者之间的连接已经断开。
总览
消息体
消息体包含五个字段:客户端标识符、遗愿主题、遗愿QoS、遗愿消息、用户名、密码。
其中客户端标识符是必须的,剩余四个依据可变头中是否指定来确认是否包含相应字段。
这些可变字段,有一个两字节的前缀来标识字段的长度。其基本字段形式如下:
- 客户端标识应唯一,大多时候可以选择唯一硬件标志等。MQTT协议要求Client连接时必须带上Client Identifier。但是也可以接收标识符为空的CONNECT数据包,这是Broker会为Client分配一个内部唯一的标识符,如果需要持久性连接,必须自己为Client设置一个唯一的标识符。
- 用户名&密码&遗愿消息&遗愿主题&遗愿QoS,的消息是在可变头中
用户名标志位置1,密码标志位置1, 遗愿标志位置1时,包含这些字段。
CONNACK数据包
当Broker收到Client的CONNECT数据包后,检查并校验CONNECT数据包的内容,然后回复一个CONNACK的数据包。
固定头
注意的点:报文类型为2,由于消息体为空,剩余长度就是可变头的长度,为固定值2个字节。
可变头
可变头中分为两个大类:连接确认标志&连接返回码
连接确认标志
这个字节高七位均为保留位,置0. 第0位是会话存在标识位。这个位的设置遵循如下规则
1
2
3
4
5
6
7
8
9
10如果服务端收到清理会话(CleanSession)标志为1的连接
除了将CONNACK报文中的返回码设置为0之外,还必须将CONNACK报文中的当前会话设置(Session Present)标志为0
如果服务端收到一个CleanSession为0的连接
当前会话标志的值取决于服务端是否已经保存了ClientId对应客户端的会话状态。
如果服务端已经保存了会话状态,它必须将CONNACK报文中的当前会话标志设置为1。
如果服务端没有已保存的会话状态,它必须将 CONNACK报文中的当前会话设置为0。还需要将CONNACK报文中的返回码设置为0
另外返回码设置为0,意味着连接成功情况。当连接不成功时,返回码不为0的情况时。
它必须将 CONNACK报文中的当前会话设置为0连接返回码
具有如下几种情况
关闭连接
两种情况:Client主动关闭&&Broker主动关闭。
Client主动关闭
Client需要主动发送一个DISCONNECT数据包。该数据包只有固定头,没有可变头和消息体。
固定头格式如下
发布该数据包之后,主动断开底层tcp连接,不必等待Broker的回复。
发送这个数据包的目的是什么?
因为Broker需要判断Client是否是正常的断开连接,当Broker收到此数据包的时候,会认为是Client正常断开的,它会丢弃当前连接指定的遗愿消息。如果检测到tcp连接断开,但没有收到此数据包,那么就会向遗愿主题发布遗愿消息
Broker主动关闭连接
MQTT协议规定Broker在没有收到DISCONNECT数据包之前都应该保持连接,只有Broker在Keeplive时间间隔里没有收到Client的任何MQTT协议数据包才会主动关闭连接。
Broker关闭连接不需要发送任何数据包,直接断开底层TCP连接.
相关代码分析
代码分析,参考书中的node.js下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//引入mqtt库
var mqtt= require('mqtt')
//建立连接
var client= mqtt.connect('mqtt://mqtt.eclipse.org', {
clientId:"mqtt_client_id",
clean:false
})
//捕获返回码&当前会话标志
client.on('connect', function (connack){
console.log('return code: ${connack.returnCode}, sessionPresent: ${connack.sessionPresent}')
client.end
})
当第一次运行时,返回
1
2
return code: 0, sessionPresent: false
//因为是第一次建立连接,所以false
再次运行,返回
1
2
return code: 0, sessionPresent: true
//已经存有状态
通过查阅c++的mqtt.h头文件。发现类似函数如下:
client的class 具有三种connect成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//构造函数声明
Client(Network& network, unsigned int command_timeout_ms = 30000);
/** MQTT Connect - send an MQTT connect packet down the network and wait for a Connack
* The nework object must be connected to the network endpoint before calling this
* Default connect options are used
* @return success code -
*/
int connect();
/** MQTT Connect - send an MQTT connect packet down the network and wait for a Connack
* The nework object must be connected to the network endpoint before calling this
* @param options - connect options
* @return success code -
*/
int connect(MQTTPacket_connectData& options);
/** MQTT Connect - send an MQTT connect packet down the network and wait for a Connack
* The nework object must be connected to the network endpoint before calling this
* @param options - connect options
* @param connackData - connack data to be returned
* @return success code -
*/
int connect(MQTTPacket_connectData& options, connackData& data);
可以看到第三种connect函数返回connack信息。再查询connack数据格式&MQTTPacket_connectData数据格式。
1
2
3
4
5
6
7
struct connackData
{
int rc;
bool sessionPresent;
};
MQTTPacket_connectData未找到定义,先略过。
根据上面的信息可以仿写c++代码进行client的连接
1
2
3
4
5
6
7
8
9
10
11
12
//引入库
//建立连接
Client c;
MQTTPacket_connectData data;
connackData conack;
c.conect(data, conack);
//查看返回状态
cout<<conack.rc<<end; //返回码
cout<<conack.sessionPresent<<endl; //当前会话标识
一点实际技巧:
在进行连接时,若两个设备碰巧申请了同一个客户端标识符。当这两个客户端同时去连接时,会造成什么样的事故呢?
在MQTT协议中,若两个Client使用相同的Client Identifier进行连接时,如果第二个连接成功,Broker会关闭与第一个Client的连接。
由于我们使用的MQTT库实现了断线重连功能,所以下线设备会尝试重新连接,结果就是这两个Client交替将对方顶下线,在实际使用中,若检测到某一个Client不停的上线下线,就有可能是这种原因造成的。