0%

MQTT协议中连接

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
    17
    User 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
//引入库
#include<mqtt.h>

//建立连接
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不停的上线下线,就有可能是这种原因造成的。