MQTT 协议 -- CONNECT & CONNACK

概述

今天来学习MQTT协议中关于connect部分,connect是很重要的部分,因为它是Client 与MQTT Broker通信的基础,并且提供了很多很有用的特性,很多场景中都可以用到这些特性。

还是理论结合着实践来讲吧,否则担心小伙伴们看了睡觉。~~~

前面已经讲过了,MQTT是一种基于发布订阅的消息传输协议,所以MQTT发布客户端可以发布消息到1个或多个订阅客户端。这个模式很像电视或者收音机的广播,电台发布节目,千家万户的观众接收节目。所有的消息都是发给MQTT Broker,MQTT Broker再将消息转发给它的订阅者。在这个过程中,需要注意以下几点:

  • 所有客户端都有一个唯一Id,这个Id只是一个标记,不是客户地址。发布端发布消息时,只能发给某个topic,而不能将消息发给某个地址或Id。
  • 客户端的Id不能重复。如果有一个客户端Client A连到MQTT Broker,如果之后又有一个客户端Client B连到MQTT Broker,此时MQTT Broker会断开Client A的连接。因为MQTT 客户端有自动连接功能,Client A断开连接之后,尝试重新连接到MQTT Broker,此时Client B又会断开,之后Client B又进行重连,然后两个Client就会进入断开-连接-断开的死循环。
  • MQTT Broker负责接收消息,并进行过滤,将消息转发给订阅了相关主题的Client。
  • Publisher和Subscriber没有直接的关联。他们都只与MQTT Broker进行连接。
  • 客户端既可以发送消息,也可以接收消息。
  • 通常情况下,MQTT Broker是不存储消息的。

现在应该对MQTT Client和Broker有一个比较清楚的认识了。我们来讨论MQTT Connect的格式吧。


CONNECT

Fixed Header

MQTT的固定头部包含了首字节和可变长度。其中首字节的高4位(bit7bit4)用于表示报文类型,1表示connect。其它标记字节(bit3bit0)都为0,如下表。

Bit 7 6 5 4 3 2 1 0
Byte 1 0 0 0 1 0 0 0 0
Byte 2... Remaining Length
Variable Header

在Connect报文中,可变头部包含10个字节,如下表:

Byte Description bit7 bit6 bit5 bi4 bit3 bit2 bit1 bit0
Byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
Byte 2 Length 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
Byte 7 Level(4) 0 0 0 0 0 1 0 0
Byte 8 Connect Flag User Name Password Will Retain Will QoS Will QoS Will Flag Clean Session Reserved
Byte 9 Keep Alive MSB 0 0 0 0 0 0 0 0
Byte 10 Keep Alive MSB 0 1 1 0 0 0 0 0

可变头部的内容包含Protocol Name, Protocol Level, Connect Flags以及Keep Alive时间。下面分别介绍:

Protocol Name: 字节1-6,这部分内容是固定的,其中字节1和字节2表示协议名称长度,其内容是0x04。字节3-字节6表示协议名称"MQTT"的UTF-8编码。

Protocol Level: 字节7,表示协议等级,MQTT 3.1.1协议版本的协议等级是4。

Connect Flag: 字节8,连接标记,每一位都表示一个标记,Bit0是保留标记。从Bit1~Bit7,分别表示Clean Session, Will Flag等内容。这些标记确定了Payload是否包含对应的信息。例如,如果Bit7和Bit6的值都为1,那么表示此次连接的Payload中包含User Name和Password。后边会分别介绍各个标记的作用。

Keep Alive: 字节9和字节10,客户端与服务器心跳间隔,高字节在前,低字节在后。单位是S,默认是60S。关于Keep Alive,需要注意的事项包括:

  • 当客户端和服务器之间没有消息传输时,客户端会每隔60S(keep alive值)向MQTT Broker发送PINGREQ数据报文。服务器需要回复PINGRESP数据报文。
  • 如果客户端在发送PINGREQ数据包一段时间后没有收到PINGRESP数据包,客户端会断开连接。
  • 如果Keep Alive的值设置为大于0(假设60S),在没有数据交互的情况下,服务器如果在超过1.5倍Keep Alive时间(90S)后没有收到PINGREQ数据包,则服务器会断开与当前客户端的连接。
  • Keep Alive可以设置为0,那么客户端不会发送PINGREQ数据包,服务器也不会因为没有收到PINGREQ而断开客户端连接。

我们来测试一下Keep Alive,观察PINGREQ和PINGRESP数据包。在命令行终端输入以下命令,会得到相应结果:

$ mosquitto_sub -t topic001 -k 5 -d
Client mosqsub|16532-SCNWCL012 sending CONNECT
Client mosqsub|16532-SCNWCL012 received CONNACK (0)
Client mosqsub|16532-SCNWCL012 sending SUBSCRIBE (Mid: 1, Topic: topic001, QoS: 0)
Client mosqsub|16532-SCNWCL012 received SUBACK
Subscribed (mid: 1): 0
Client mosqsub|16532-SCNWCL012 sending PINGREQ
Client mosqsub|16532-SCNWCL012 received PINGRESP
Client mosqsub|16532-SCNWCL012 sending PINGREQ
Client mosqsub|16532-SCNWCL012 received PINGRESP
Client mosqsub|16532-SCNWCL012 sending PINGREQ
Client mosqsub|16532-SCNWCL012 received PINGRESP

上面命令中,-t topic001表示订阅topic为topic001的主题,使用 -k 5表示设置keep alive时间间隔为5S,-d表示启用Debug模式。从输出结果可以看到每隔5S,客户端会发送PINGREQ,并收到从服务器返回的PINGRESP。

Connect Flag

因为Connect Flag的内容比较多,所以单独用一小节来介绍。

Reserved

第8字节的Bit0。保留位,必须为0。

Clean Session

第8字节的Bit1,用于表示是否需要清除Session,如果值为0,表示保留Session,如果为1,表示清除Session。

Client和Broker可以存储一些Session状态信息,用于消息的可靠传输。我们知道MQTT是通过定义QoS等级来保证消息的可靠传输的,所以Session状态信息中最重要的就是QoS消息的状态。以Broker为例,对于QoS为1或者2的消息,如果Broker无法成功投递消息到Client A,那么消息状态会保留下来,当下次Client A重新连接时,服务器会根据Session状态,重新投递之前失败的消息。

所以,如果客户端发布或订阅某个topic,并且设置了QoS > 0,那么Clean Session必须设置为0。

Will Flag

第8字节的Bit2,用于标记是否发送遗愿消息。

遗愿消息是指当客户端非正常断开时,客户端希望发送一条消息给一个指定的topic,通知对方自己掉线了。遗愿消息的一个应用场景是设备掉线提醒,当设备掉线后,订阅方(通常是后台服务器)就知道设备已经掉线了。

当Will Flag设置为1时,表示需要设置遗愿消息,那么Broker会存储遗愿消息并且在客户端异常掉线之后,发送遗愿消息到指定的topic。

如果Will Flag设置为1,那么必须要有Will Topic(遗愿主题)和Will Message(遗愿消息),并且Will QoS和Will Retain也会被读取。遗愿消息也可以设置QoS的,这样可以确保遗愿消息的可靠传递。

那么什么情况下Broker会发送Will Message呢?当以下任何一种情况发生时,Broker会发送遗愿消息。

  • 网络异常
  • 服务器在Keep Alive超时后没有收到PINGREQ
  • 客户端在关闭连接之前,没有先发送DISCONNECT
  • 服务器因为协议错误关闭客户端连接

需要注意的是,如果客户端正常关闭连接,在关闭连接之前发送了DISCONNECT,遗愿消息是不会被发送的。当客户端发送DISCONNECT请求后,遗愿消息会被Broker删除。

Will QoS

第8字节的Bit3和Bit4,用来设置遗愿消息的QoS,因为QoS有3个值,0,1和2,所以用两位来表示,高位在前,低位在后。

Will Retain

第8字节的Bit5,用于标记是否保留遗愿消息。

Will Retain这个标记也很有用,如果设置成1,当客户端掉线后,之后所有新的订阅者订阅Will Topic时,都能收到遗愿消息。QoS > 0只能报纸之前订阅过的订阅者收到消息,Will Retain能确保新的订阅者也接收到消息。

User Name Flag

用于标记Payload中是否包含User Name信息。如果设置成1,payload中必须包含User Name信息。

Password Flag

用于标记Payload中是否包含Password信息。如果设置成1,payload中必须包含Password信息。需要注意的是,如果User Name Flag设置成0,Password Flag必须设置成0。但是可以只包含用户名,不包含密码,所以User Name Flag设置成1时,Password Flag也可以设置成0.

Keep Alive

可变头中第9和第10字节用来表示Keep Alive时间。这个时间是MQTT的心跳时间,单位是秒,默认值是60S。在Client和Broker没有数据交互的情况下,Client需要发送PINGREQ给Broker,Broker回复PINGRESP,用于检测客户端是否在线。关于Keep Alive,需要注意一下几点:

  • 客户端负责发送心跳PINGREQ,服务端只管在接收到心跳时回复PINGRESP。
  • 客户端在发送PINGREQ一段时间后,未收到回复,客户端将关闭连接。
  • 服务端在Keep Alive的1.5被时间之后,没有收到客户端的任何数据,包括PINGREQ,也会关闭连接。
  • Keep Alive可以设置为0,表示不启用心跳机制,那么客户端,服务器都不会因为未收到心跳或回复而关闭连接。
示例

我们来看一个CONNECT报文的可变头的例子,加深理解。

描述 7 6 5 4 3 2 1 0
字节1 类型长度高位 (0) 0 0 0 0 0 0 0 0
字节2 类型长度低位 (4) 0 0 0 0 0 1 0 0
字节3 'M' 0 1 0 0 1 1 0 1
字节4 'Q' 0 1 0 1 0 0 0 1
字节5 'T' 0 1 0 1 0 1 0 0
字节6 'T' 0 1 0 1 0 1 0 0
字7 Level (4)
字节8 见备注 1 1 0 0 1 1 0 0
Keep Alive
字节9 Keep Alive高位 0 0 0 0 0 0 0 0
字节10 Keep Alive低位 0 0 0 0 1 0 1 0

以上字节8,分别表示 User Name Flag (1),Password Flag (1),Will Retain (0),Will QoS (01),Will Flag (1),Clean Session (1),Reserved (0)

Payload

介绍完可变头,我们来看消息体。

CONNECT协议是包含消息体的,其内容分别是Client Id/[Will Topic]/[Will Message]/[User Name]/[Password]。

以上类型顺序不能变化,除了Client Id之外,其它内容都是可选的,只有Connect Flag中对应的值为1时,其Payload中才包含相关内容。如只有可变头中CONNECT Flag部分,其Will Flag为1时,Payload中才会出现Will Topic和Will Message。

对于所有Payload中的内容,前两位是长度,后面是数据内容。我们后边会通过Wire Shark抓包来验证这一点。

Client Id

Client Id是客户端的唯一标识,这个Id不能重复,如果两个客户端使用了相同的Id,那么就会出现互相踢对方的现象,如果你的客户端一直在断开-连接-断开这样的进行循环,就要考虑是否是Client Id重复了。关于Client Id,需要注意以下事项:

  • Client Id要唯一,并且最好能有一定意义并且可读,如我们公司是使用<客户端类型>_<设备编号>作为Client Id。如前面提到的,虽然不能直接给Client Id发消息,但是在问题排查时,Client Id还是有用的。如EMQX的后台管理工具,可以直接根据Client Id判断客户端是否在线,以及追踪某个Client Id发送和接收的所有消息。这对于在线排查问题很有帮助。
  • Client Id可以为空,如果为空,服务器会自动分配一个Id,之后也会一直使用分配的Id,直到网络断开。但是生产环境中,不建议这样使用。
  • Client Id是和Session关联的,所以如果你的项目中使用到了QoS > 0,那么不能使用随机的Client Id。
Will Topic & Will Message

当Will Flag设置成1时,Payload中就包含Will Topic和Will Message。Will Topic和Will Message已经讲过,这里不再赘述。

User Name & Password

如果这两个Connection Flag被设置成1,那么Payload中就包含User Name和Password。通过User Name和Password可以对Client进行身份验证和授权。身份验证可以决定是否允许客户端连接,授权可以限制客户端允许访问某些资源(比如topic)。这个属于Broker中客户端管理的内容,我们后边会介绍。


CONNACK

当客户端发送一个CONNECT报文后,服务器需要回复一个CONNACK报文。如果客户端发送CONNECT给Broker,在一段时间内没有收到CONNACK,那么客户端需要关闭当前连接。

Fixed Header

CONNACK固定头如下:

Bit 7 6 5 4 3 2 1 0
Byte 1 0 0 1 0 0 0 0 0
Byte 2 0 0 0 0 0 0 1 0

再次回忆一下固定头的格式,第一字节高4位表示协议类型,2表示CONNACK,低4位是某些协议的标记位,对于CONNACK,这是保留字段。

固定头的后边部分是Remaining Length,CONNACK的协议,其内容长度是2,用一个字节表示。

Variable Header

可变头包含两个字节,其格式如下:

Bit 7 6 5 4 3 2 1 0
Byte 1 0 0 0 0 0 0 0 X
Byte 2 X X X X X X X X

第一字节Bit7-Bit1是保留位,Bit 0用于表示Session Present。

第二字节是Return Code。我们分别介绍。

Session Present

Session Present用来表明服务端是否存在Session状态。前边讲过,当Clean Session设置为0时,服务端会保留Session的一些状态,当客户端重新连接时,那么服务器会根据Client Id判断是否存在Session Status,如果存在,该值为1,否则为0。关于Session Present,需要注意以下两点:

  • 当客户端发起连接并且设置Clean Session为1时,不管服务器是否存在Session Status,Session Present总是为0.

  • 如果Broker返回的Return Code不为0,那么Session Present必须为0.

Return Code

CONNACK可变头的第二字节表示Return Code,表明连接的返回状态。0表示成功,其他数字表示异常,Return Code列表如下:

描述
0X00 正常
0X01 服务器不支持协议当前协议版本
0X02 拒绝连接,Client Id没有连接权限
0X03 拒绝连接,服务端不可用
0X04 拒绝连接,用户名密码错误
0X05 拒绝连接,没有授权
0X06-0XFF 保留

上边0X02和0X05的区别,0X02是身份验证拒绝连接,0X05是授权验证失败,拒绝连接。

Payload

CONNACK没有Payload。


测试

讲了这么多,我们现在来抓包测试一下,验证我们上面讲述的内容。

  1. 打开Wire Shark,监听本地环回网卡,过滤条件输入tcp.port==1883

  2. 打开终端,输入以下命令,发送一条消息。

    mosquitto_pub -d -t topic1 --will-qos 2 --will-topic "will_topic" --will-payload "I'm offline!" -u "zengbiaobiao" -P "password" -m "Hello MQTT"
    
  3. 回到Wire Shark,查看CONNECT数据包,如下图。

mqtt-connect.png

当我们查看CONNECT协议时,可以看到其详细数据内容,

第一块区域,显示固定头以及消息长度,Msg Len: 85,之后是协议名称长度以及协议名称。

第二块区域,显示了CONNECT Flags,对应着我们发送消息是设置的参数。

第三块区域,显示了Payload的内容,每一项内容之前,都有两字节用于表示内容长度。


总结

今天介绍了MQTT中CONNECT 以及CONNACK协议类型。CONNECT协议类型包含协议头,可变头和消息体。重点介绍了CONNECT Flags以及连接报文中的一些重要特性,这些特性包括Clean Session, Will Topic, Keep Alive, User/Password等等,这些特性都很适用。

另外我们也介绍了CONNACK,最后通过一个实验进行抓包测试,验证我们所讲的内容。

如果你有


所有文章在Github上同步,你也可以访问我的个人博客点击查看

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容