前言
本篇会把连接(connect)、心跳(pingreq/pingresp)、确认(connack)、断开连接(disconnect)和在一起。
connect
像前面所说,mqtt有关字符串部分采用的修改版的utf-8编码,connect可变头部中协议名称、消息体都是采用修改版的utf-8编码。前面基本上可变头部内容不多,下面是一个较为完整的connect消息结构:
| description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|
fixed header/固定头部 |
| | message type(1) | dup flag | qos level | retain |
byte 1 | | 0 | 0 | 0 | 1 | x | x | x | x |
byte 2 | remaining length |
variable header/可变头部 |
protocol name |
byte 1 | length msb (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
byte 2 | length lsb (6) | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 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 | 'i' | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
byte 6 | 's' | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 |
byte 7 | 'd' | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 |
byte 8 | 'p' | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
protocol version number |
byte 9 | version (3) | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
connect flags |
| user name flag | password flag | will retain | will qos | will flag | clean session | reserved |
byte 10 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | x |
keep alive timer |
byte 11 | keep alive msb (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
byte 12 | keep alive lsb (10) | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
payload/消息体 |
client identifier(客户端id) 1-23个字符长度,客户端到服务器的全局唯一标志,如果客户端id超出23个字符长度,服务器需要返回码为2,标识符被拒绝响应的connack消息。 处理qos级别1和2的消息id中,可以使用到。 必填项。 |
will topic will flag值为1,这里便是will topic的内容。qos级别通过will qos字段定义,retain值通过will retain标识,都定义在可变头里面。 |
will message will flag若设为1,这里便是will message定义消息的内容,对应的主题为will topic。如果客户端意外的断开触发服务器publish此消息。 长度有可能为0。 在connect消息中的will message是utf-8编码的,当被服务器发布时则作为二进制的消息体。 |
user name 如果设置user name标识,可以在此读取用户名称。一般可用于身份验证。协议建议用户名为不多于12个字符,不是必须。 |
password 如果设置password标识,便可读取用户密码。建议密码为12个字符或者更少,但不是必须。 |
可变头部
协议名称和协议版本都是固定的。
连接标志(connect flags)
一个字节表示,除了第1位是保留未使用,其它7位都具有不同含义。
业务上很重要,对消息总体流程影响很大,需要牢记。
clean session
0,表示如果订阅的客户机断线了,要保存为其要推送的消息(qos为1和qos为2),若其重新连接时,需将这些消息推送(若客户端长时间不连接,需要设置一个过期值)。
1,断线服务器即清理相关信息,重新连接上来之后,会再次订阅。
will flag
定义了客户端(没有主动发送disconnect消息)出现网络异常导致连接中断的情况下,服务器需要做的一些措施。
简而言之,就是客户端预先定义好,在自己异常断开的情况下,所留下的最后遗愿(last will),也称之为遗嘱(testament)。
这个遗嘱就是一个由客户端预先定义好的主题和对应消息,附加在connect的可变头部中,在客户端连接出现异常的情况下,由服务器主动发布此消息。
只有在will flag位为1时,will qos和will retain才会被读取,此时消息体payload中要出现will topic和will message具体内容,否则,will qos和will retain值会被忽略掉。
will qos
两位表示,和publish消息固定头部的qos level含义一样。这里先掠过,到publish消息再回过头来看看,会更明白些。
若标识了will flag值为1,那么will qos就会生效,否则会被忽略掉。
will retain
如果设置will flag,will retain标志就是有效的,否则它将被忽略。
当客户端意外断开服务器发布其will message之后,服务器是否应该继续保存。这个属性和publish固定头部的retain标志含义一样,这里先掠过。
user name 和 password flag:
用于授权,两者要么为0要么为1,否则都是无效。都为0,表示客户端可自由连接/订阅,都为1,表示连接/订阅需要授权。
payload/消息体
消息体定义的消息顺序(如上表所示),约定俗成,不得更改,否则将可能引起混乱。
若will flag值为0,那么在payload中,client identifer后面就不会存在will topic和will message内容。
若user name和password都为0,意味着payload/消息体中,找不到user name和password的值,就算有,也是无效。标志决定着是否读取与否。
心跳时间(keep alive timer)
以秒为单位,定义服务器端从客户端接收消息的最大时间间隔。一般应用服务会在业务层次检测客户端网络是否连接,不是tcp/ip协议层面的心跳机制(比如开启socket的so_keepalive选项)。
一般来讲,在一个心跳间隔内,客户端发送一个pingreq消息到服务器,服务器返回pingresp消息,完成一次心跳交互,继而等待下一轮。若客户端没有收到心跳反馈,会关闭掉tcp/ip端口连接,离线。
16位两个字节,可看做一个无符号的short类型值。最大值,2^16-1 = 65535秒 = 18小时。最小值可以为0,表示客户端不断开。一般设为几分钟,比如微信心跳周期为300秒。
will message编码
will message在connect payload/息体中,使用utf-8编码。假设内容为“abcd”,大概如下:
| description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|
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 | 'a' (0x61) | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
byte 4 | 'b' (0x62) | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 |
byte 5 | 'c' (0x63) | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 |
byte 6 | 'd' (0x64) | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 |
有一点需要记住,publish的payload/消息体中以二进制编码保存。
某刻客户端异常关闭触发服务器会publish此消息。那么服务器会直接把byte3-byte6之间字符取出,保存为二进制,附加到publish消息体中,大概存储如下:
| description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|
byte 1 | 'a' (0x61) | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
byte 2 | 'b' (0x62) | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 |
byte 3 | 'c' (0x63) | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 |
byte 4 | 'd' (0x64) | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 |
另外,mqtt 3.1协议对will message的说明很容易引起误解,3.1.1草案已经得到修正。
相关说明:
http://mqtt.org/wiki/doku.php/willmessageutf8_support
https://tools.oasis-open.org/issues/browse/mqtt-2
连接异常中断通知机制
connect消息一旦设置在可变头部设置了will flag标记,那就启用了last-will-and-testament特性,此特性很赞。
一旦客户端出现异常中断,便会触发服务器发布will message消息到will topic主题上去,通知will topic订阅者,对方因异常退出。
接收connect后的响应动作
接收到connect消息之后,服务器应该返回一个connack消息作为响应:
- 若客户端绕过connect消息直接发送其它类型消息,服务器应关闭此非法连接
若客户端发送connect之后未收到connact,需要关闭当前连接,然后重新连接
- 相同client id客户端已连接到服务器,先前客户端必须断开连接后,服务器才能完成新的客户端connect连接
客户端发送无效非法connect消息,服务器需要关闭
connack
一个完整的connack消息大致如下:
| description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|
fixed header/固定头部 |
byte 1 | | message type (2) | dup flag | qos flags | retain |
| | 0 | 0 | 1 | 0 | x | x | x | x |
byte 2 | | remaining length (2) |
| | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
variable header/可变头部 |
topic name compression response |
byte 1 | reserved values. not used. | x | x | x | x | x | x | x | x |
connect return code |
byte 2 | return code | | | | | | | | |
可变头部第一个字节为保留,无甚用处。第二个字节为连接握手返回码:
返回值 | 16进制 | 含义 |
0 | 0x00 | connection accepted |
1 | 0x01 | connection refused: unacceptable protocol version |
2 | 0x02 | connection refused: identifier rejected |
3 | 0x03 | connection refused: server unavailable |
4 | 0x04 | connection refused: bad user name or password |
5 | 0x05 | connection refused: not authorized |
6-255 | | reserved for future use |
只有0-5目前被使用到,其他值有待日后使用。一般返回值为0x00,表示连接建立。非法的请求,需要返回相应的数值。
从上面看出,一个connact,四个字节表示。一个正常的connact消息实际内容可能如下:
0x20 0x02 0x00 0x00
若是在私有协议中,两个字节就足够了。
很多时候,客户端和服务器端在没有消息传递时,会一直保持着连接。虽然不能依靠tcp心跳机制(比如so_keepalive选项),业务层面定义心跳机制,会让连接状态检测、控制更为直观。
pingreq
由客户端发送到服务器端,证明自己还在一直连接着呢。两个字节,固定值。
| description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|
fixed header/固定头部 |
byte 1 | | message type (12) | dup flag | qos flags | retain |
| | 1 | 1 | 0 | 0 | x | x | x | x |
byte 2 | | remaining length (0) |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
客户端会在一个心跳周期内发送一条pingreq消息到服务器端。
心跳频率在connect可变头部“keep alive timer”中定义时间,单位为秒,无符号16位short表示。
pingresp
服务器收到pingreq请求之后,会立即响应一个两个字节固定格式的pingresp消息。
| description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|
fixed header/固定头部 |
byte 1 | | message type (13) | dup flag | qos flags | retain |
| | 1 | 1 | 0 | 1 | x | x | x | x |
byte 2 | | remaining length (0) |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
服务器一般若在1.5倍的心跳周期内接收不到客户端发送的pingreq,可考虑关闭客户端的连接描述符。此时的关闭连接的行为和接收到客户端发送disconnect消息的处理行为一致,但对客户端的订阅不会产生影响(不会清除客户端订阅数据),这个需要牢记。
若客户端发送pingreq之后的一个心跳周期内接收不到pingresp消息,可考虑关闭tcp/ip套接字连接。
disconnect
客户端主动发送到服务器端,表明即将关闭tcp/ip连接。此时要求服务器要完整、干净的进行断开处理,不能仅仅类似于关闭连接描述符类似草草处理之。
需要两个字节,值固定:
| description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|
fixed header/固定头部 |
byte 1 | | message type (14) | dup flag | qos flags | retain |
| | 1 | 1 | 1 | 0 | x | x | x | x |
byte 2 | | remaining length (0) |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
服务器要根据先前此客户端在发送connect消息可变头部connect flag中的“clean session flag”所设置值,再次复习一下:
值为0,服务器必须在客户端断开之后继续存储/保持客户端的订阅状态。这些状态包括:
- 存储订阅的消息qos1和qos2消息
- 正在发送消息期间连接丢失导致发送失败的消息
- 以便当客户端重新连接时以上消息可以被重新传递。
值为1,服务器需要立刻清理连接状态数据。
有一点需要牢记,服务器在接收到客户端发送的disconnect消息之后,需要主动关闭tcp/ip连接。