误区
Keep Alive指定连接最大空闲时间T,当客户端检测到连接空闲时间超过T时,必须向Broker发送心跳报文PINGREQ,Broker收到心跳请求后返回心跳响应PINGRESP。若Broker超过1.5T时间没收到心跳请求则断开连接,并且投递遗嘱消息到订阅方;同样,若客户端超过一定时间仍没收到心跳响应PINGRESP则断开连接。
首先反驳一下这个误区:
为什么MQTT单独再设计一套心跳机制,而不是使用TCP已经有的心跳机制?
MQTT协议是基于TCP的一个应用层协议,理论上TCP协议在丢失连接时会通知上层应用,但是TCP有一个半打开连接的问题(half-open connection)。这里不打算深入分析TCP协议,需要记住的是,在这种状态下一端的TCP连接已经失效,但是另外一端并不知情,它认为连接依然是打开的,它需要很长的时间(一般情况是两个TLS)才能感知到对端连接已经断开了,这种情况在使用移动或者卫星网络的时候尤为常见。
所以,仅仅依赖TCP层的连接状态监测是不够的,于是MQTT协议设计了一套Keep Alive机制。在MQTT建立连接的时,可以传递一个Keep Alive参数,它的单位为秒,占用两个字节(最大值65535),MQTT协议中约定:在 1.5*Keep Alive时间间隔内,如果MQTT代理服务器没有收到来自Client的任何数据包,那么代理服务器Broker认为它和 Client之间的连接已经断开;同样如果Client没有收到来自Broker的任何数据包,那么Client认为它和Broker之间的连接已经断开。
3.12 PINGREQ – 心跳请求
客户端发送PINGREQ报文给服务端的。 用于:
1. 在没有任何其它控制报文从客户端发给服务的时, 告知服务端客户端还活着。
2. 请求服务端发送 响应确认它还活着。
3. 使用网络以确认网络连接没有断开。
保持连接( Keep Alive) 处理中用到这个报文, 详细信息请查看 3.1.2.10节。
在没有任何其它控制报文从客户端发给服务器时,告知服务器端,客户端还活着。这句话的正确理解是:
如果客户端只有下行数据,没有上行数据,此时虽然不存在链路空闲,但是客户端必须按照心跳周期时间,向服务器端发送心跳请求!
举个例子:
如果客户端订阅了一个Qos 0 的消息(没有其他业务,客户端仅订阅主题消息),并且服务器端每间隔一段时间 t (t 小于心跳周期)向客户端发送客户端订阅的主题消息, 这种情况下客户端必须按照向服务器发送心跳请求。
简介
保持连接Keep Alive(心跳时间)
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
byte 9 | 保持连接 Keep Alive MSB | |||||||
byte 10 | 保持连接 Keep Alive LSB |
下面是MQTT关于Keep Alive的官方说明:
如果保持连接(Keep Alive)是一个以秒为单位的时间间隔,表示为一个16位的字,它是指在客户端传输完成一个控制报文的时刻,到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。客户端负责保证控制报文发送的时间间隔不超过保持连接的值。如果没有任何其他的控制报文可以发送,那么客户端必须发送一个PINGREQ的报文。(客户端负责保证控制报文发送的时间间隔,不超过保持连接的值。如果没有任何其他的控制报文可以发送,客户端必须发送一个PINGREQ报文。)
不管保持连接的值是多少,客户端在任何时候都可以发送PINGREQ报文,并且使用PINGRESP报文判断网络和服务端的活动状态。
如果保持连接的值非零,并且服务端在1.5倍的保持连接时间内没有收到客户端的控制报文PINGREQ,它必须断开客户端的网络连接,认为网络已经断开。(如果保持连接的值非零, 并且服务端在一点五倍的保持连接时间内没有收到客户端的控制报文, 它必须断开客户端的网络连接, 认为网络连接已断开。)
客户端发送了PINGREQ报文之后,如果在合理时间内没有收到PINGRESP报文,它应该关闭发哦服务端的网络连接。
保持连接的值为零,表示关闭保持连接功能。这意味着服务端不需要因为客户的不活跃而断开连接。注意不管保持连接的值是多少,任何时候,只要服务端认为客户端不是活跃状态,或者无响应的,可以断开客户端的连接。
非规范评注
保持连接的实际值是由应用指定的, 一般是几分钟。 允许的最大值是18小时12分15秒。
通俗理解:
心跳时间(Keep Alive),是一个以秒为单位的时间间隔,表示为一个16位的字。心跳时间指的是,客户端向代理服务器发送一个心跳包 PINGREQ开始计时,到下一次再发送心跳包 PINGREQ时,这中间的时间是一个完整的心跳时间。客户端发送的心跳包 PINGREQ,在链路空闲状态下连续发送时,每次发送的时间间隔不能超过心跳时间Keep Alive。如果客户端没有订阅,发布等操作时,链路处于空闲状态,那么客户端必须发送一个心跳包 PINGREQ,给代理服务器。
不管心跳时间 Keep Alive的值是多少,客户端在任何时候都可以发送心跳包 PINGREQ报文(但是实际测试中,链路有其他报文时,不会有心跳包),客户端使用心跳响应包PINGRESP是客户端用来判断网络和服务器活动状态的,同时,代理服务器是使用客户端发送的心跳包 PINGREQ,来判断网络状态和客户端的活跃状态。
心跳时间间隔keep Alive的值非零时,代理服务器如果在1.5T内没有收到客户的心跳包 PINGREQ,那么代理服务器有权断开当前的MQTT连接,并认为客户端与自己(MQTT代理服务器)网络已经断开。(这一点需要区分用户场景:
如果代理服务器的MCU程序会踢掉MQTT,那么不管标准的MQTT3.1.1版本如何规定,MQTT连接一定会断开。
另外有的客户MCU程序会设定比较长的心跳时间,也可能会等待两个心跳时间判断是否收到了PINGREQ。
还有就是网络底层的问题,MQTT协议仅保证MQTT这一层的实现,对于下面一层的TCP没有任何约束,TCP链路异常,那么MQTT也可能随之关闭。
不同的用户设置不一定相同,但是主流的设定是1.5倍心跳时间。)
客户端发送了心跳包 PINGREQ报文之后,如果在合理时间内没有收到PINGRESP报文,客户端应该关闭到服务端的 MQTT连接(客户端应该断开 MQTT连接,但实际上客户端不一定会选择关闭与代理服务器的连接)。
如果心跳时间的值为零,表示关闭心跳超时保持连接功能。这意味着服务端不需要因为客户的不活跃而断开连接。注意不管保持连接的值是多少,任何时候,只要服务端认为客户端不是活跃状态,或者无响应的,可以断开客户端的连接。(有待验证,不同的使用场景,客户端,代理服务器选择的策略都不一样)
PINGREQ — 心跳请求
PINGREQ报文固定报头
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
byte 1 | MQTT控制报文类型 (12) | 保留位 | ||||||
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | |
byte 2 | 剩余长度 (0) | |||||||
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
byte 1 | MQTT控制报文类型 (12) | 保留位 | ||||||
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | |
byte 2 | 剩余长度 (0) | |||||||
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
可变报头:PINGREQ报文没有可变报头。
有效载荷:PINGREQ报文没有有效载荷。
响应 :服务端必须发送 PINGRESP报文响应客户端的PINGREQ报文。
PINGREQ 数据包没有可变头(Variable header)和消息体(Payload),包大小是2字节(是TCP payload的内容是2字节)。当 Client 在一个 Keep Alive 时间间隔内没有向 Broker 发送任何数据包,比如 PUBLISH 和 SUBSCRIBE 的时候,它应该向 Broker 发送 PINGREQ 数据包。客户端会在一个心跳周期内发送一条PINGREQ消息到服务器端。心跳频率在CONNECT可变头部“Keep Alive timer”中定义时间,单位为秒,无符号16位short表示。
若客户端发送PINGREQ之后的一个心跳周期内接收不到PINGRESP消息,可考虑关闭TCP/IP套接字连接(仅供参考选择,非强制规范)。
PINGRESP – 心跳响应
服务端发送 PINGRESP报文,响应客户端发送的 PINGREQ报文。 表示服务端还活着。保持连接( Keep Alive) 处理中用到这个报文。
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
byte 1 | MQTT控制报文类型 (13) | 保留位 | ||||||
1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | |
byte 2 | 剩余长度 (0) | |||||||
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
可变报头:PINGRESP报文没有可变报头。
有效载荷:PINGRESP报文没有有效载荷。
PINGRESP 数据包没有可变头(Variable header)和消息体(Payload),包大小是2字节(是TCP payload的内容是2字节)。当 Broker 收到来自 Client 的 PINGREQ 数据包,它应该回复 Client 一个 PINGRESP 数据包。代理服务器Broker,一般若在1.5倍的心跳周期内接收不到客户端发送的PINGREQ,可考虑关闭客户端的连接描述符。此时的关闭连接的行为和接收到客户端发送DISCONNECT消息的处理行为一致,但对客户端的订阅不会产生影响(不会清除客户端订阅数据),这个需要牢记。
若客户端发送PINGREQ之后的一个心跳周期内接收不到PINGRESP消息,可考虑关闭TCP/IP套接字连接(仅供参考选择,非强制规范)。
实际代码具体做法:
在建立连接的时候,客户端可以传递一个 Keep Alive 参数,它的单位为秒,MQTT协议中约定:在 1.5 * Keep Alive 的时间间隔内,如果代理服务器 Broker 没有收到来自客户端 Client 的任何数据包,那么代理服务器 Broker 认为它和客户端 Client 之间的连接已经断开;同样地, 如果客户端 Client 没有收到来自代理服务器 Broker 的任何数据包,那么客户端 Client 认为它和代理服务器 Broker 之间的连接已经断开。
对于 Keep Alive 机制,我们还需要记住以下几点:
问题:链路空闲多久后,客户端才会向代理服务器发送心跳包PINGREQ?
1、如果是Qos大于0的发布,或者是订阅,取消订阅等,在这些报文发送(交互过程)完成之后,经过一个心跳时间Keep Alive,心跳机制才会去发送心跳请求包 PINGREQ。
2、如果客户端只是订阅了,那么就是从客户端没有上行控制报文开始,经过一个心跳周期,客户端需要向服务器端发送PINGREQ,想客户端证明自己还活着。
问题:如果因为网络问题,客户端发送的心跳包PINGREQ,不能发送到移动网络中去,那么代理服务器会怎么处理?
这个要分情况,网络问题本身就很复杂:
1、客户端的底层网络有问题,客户端写入到了TCP缓冲区内,并且也写成功了,但是底层TCP网络有问题,没有成功将 MQTT心跳请求包 PINGREQ发送给代理服务器,这样代理服务器就不回复心跳响应包PINGRESP。这种情况MQTT链路,可能会在客户端,或者服务器端断开,二者都有权利断开连接。
2、网络有问题,客户端已经把心跳包 PINGREQ发送到了移动网络中去,但是客户端没有收到,如果是连续出现这样的情况,MQTT 链路也会断开。
3、如果是发送了TCP重传,对就是客户端的心跳请求包 PINGREQ,发生了TCP重传,
重传成功了:那么客户端的心跳请求包 PINGREQ可能会超时,这需要看代理服务器如何处理;
重传失败了:那么客户端就会发起TCP层的 [ RST ACK ],重置TCP链路,MQTT也就随之断开。
问题:如果因为网络问题,代理服务器没有回复心跳响应PINGRESP,客户端会怎么处理?
如果代码里面有对心跳包的标记,比如全局变量pre_ping, pre_ping_response,ping_flag在每次发送心跳之前,都检查上次发送的心跳请求包 PINGREQ 有没有收到心跳响应包 PINGRESP 。通过变量判断控制,那么客户端 Client 可以自行选择是否断开 MQTT 链路,还是重连 MQTT。