一、TCP存活(keepalive)检测的背景
对于TCP设计来说,如果一个客户端和服务器端建立连接后,不在进行数据传输,那么这个连接将会一直存在下去,理论上即使中间的路由器崩溃重启、或者中间的网络线路拔掉在插上等等,只要客户端和服务器端的主机没有发生异常,这个TCP连接仍然是处于连接状态的。这样会有两个问题
1、服务器端可能需要内存等资源保存每个连接的状态信息,即使这个连接在实际上已经不能通信而没有存在的必要了。比如客户端崩溃时,服务器可能仍然认为这个连接是有效的而继续占用为这个连接分配的资源
2、防火墙和NAT设备可能会保存一个连接超时的定时器,如果这个TCP连接长时间没有数据传输,定时器超时后,服务器端和客户端实际上已经不能在进行通信。
为了解决上面的两个问题,TCP连接需要一个存活(keepalive)检测机制,定时检测当前的TCP连接是否可用,以刷新防火墙和NAT信息,或者当检测到连接失败的时候释放相关资源。
二、存活检测的两种方式:
1、应用层面的心跳机制(application-managed keepalive)
自定义心跳消息头. 一般客户端主动发送, 服务器接收后进行回应(也可以不回应)。相比传输层的keepalive检测,应用层的检测更灵活且不依赖传输层的协议。 例如最新的ssh就有应用层的存活检测机制。本文不做额外介绍。
2、传输层的存活检测
TCP的存活检测就属于传输层的存活检测功能,相比于应用层的检测,传输层的检测可以提供更通用的检测从而减少应用层的工作量,另外传输层的探测包不需要添加应用层的头,从而减轻了网络负载。传输层的TCP keepalive是接下来介绍的功能。
三、TCP的存活检测
TCP keealive定义于RFC1122,但是并不是要求每个TCP实现都支持keepalive,[RFC1122]给出的原因是(1)在短暂的故障期间,它们可能引起一个良好连接被释放,比如网络中间的路由器崩溃了并且正在重启,keepalive检测可能会认为对端主机崩溃而释放连接,实际上在这种情况下路由器重启完毕后,该TCP连接可以继续通信;(2)需要消耗额外的带宽资源;(3)在以数据包计费的互联网上需要额外花费金钱;但是尽管如此,很多TCP实现还是支持TCP的存活检测。
3.1、TCP存活检测的机制
TCP的存活检测可以在TCP连接的两端同时启用,或者只在一端启用,或者两端都不启用。在TCP的存活检测中有三个参数
keepalive time:存活检测时间间隔
keepalive interval:没有收到ACK响应时存活检测时间间隔
keepalive probes:没有收到ACK响应时存活检测累计次数,因为keepalive依靠探测包的ACK响应判断连接情况,但是不携带数据的ACK没有重传不是可靠的传输,因此需要多次累计探测没有响应才能认为连接失活。
TCP的存活检测流程如下:在TCP存活检测中有一个存活定时器(keepalive timer),当TCP连接的任何一个方向有业务数据传输时就会把这个存活定时器重置为keepalive time,当没有数据传输本端这个定时器超时的时候,TCP就会发送一个探测包(keepalive probe 或者简称为keepalive包),对端接收到这个探测包后就会响应一个ACK,本端接收到这个ACK响应的时候就会重置这个存活定时器为keepalive time。如果本端没有收到这个ACK响应,会以keepalive interval为间隔时间重复发送探测包,如果一直发送了keepalive probes次都没有收到ACK响应,则认为这个连接已经失活。如果中间的某次收到ACK响应则会重置存活定时器为keepalive time,并且停止keepalive interval为间隔的探测包重复发送。
3.2、探测包的包格式
对于探测包的格式,一般是使用本端目前发送窗口的上一个byte做为系列号,比如当前本端接受到的最大连续ACK Number为n,则探测包的seq为n-1;按照RFC1122,探测包中可以不包含数据,也可以包含1byte的无用数据(garbage byte)以兼容老式TCP实现。
3.3、TCP存活检测可以检测到的连接情况
TCP的存活检测可以检测到四种连接情况,在下面的描述中,我们称使用存活选项的那一段为服务器,另一端为客户端。也可以在客户端设置该选项,且没有不允许这样做的理由,但通常设置在服务器。如果连接两端都需要探测对方是否消失,那么就可以在两端同时设置(比如NFS)。
1)客户端主机依旧活跃(up)运行,并且从服务器可到达。从客户端TCP的正常响应,服务器知道对方仍然活跃。服务器的TCP复位存活定时器为keepalive time,如果在keepalive time到期之前,连接上发生应用程序的通信,则TCP会将存活定时器重置为keepalive time。
2) 客户端已经崩溃,或者已经关闭(down),或者正在重启过程中。在这两种情况下,它的TCP都不会响应。服务器没有收到对其发出探测的响应,并且在keepalive interval秒之后超时。服务器将总共发送keepalive probes个这样的探测,每个探测间隔keepalive interval秒。如果没有收到一个响应,它就认为客户端主机已经关闭并终止连接。
3) 客户端曾经崩溃,但已经重启。这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位(在TCP头中置位RST标识),从而引起服务器对连接的终止。
4) 客户端主机活跃运行,但从服务器不可到达。这与状态2类似(但是状态4可能会收到ICMP的错误指示信息),因为TCP无法区别它们两个。它所能表明的仅是未收到对其探测的回复。
服务器不必担心客户端主机被关闭然后重启的情况(这里指的是操作员执行的 正常关闭,而不是主机的崩溃)。当系统被操作员关闭时,所有的应用程序进程(也就是客户端进程)都将被终止,客户端TCP会在连接上发送一个FIN。收到 这个FIN后,服务器TCP向服务器进程报告一个文件结束,以允许服务器检测这种状态。
在第一种状态下,服务器应用程序不知道存活探测是否发生。凡事都是由 TCP层处理的,存活探测对应用程序透明,直到后面2,3,4三种状态发生。在这三种状态下,通过服务器的TCP,返回给服务器应用程序错误信息。(通常 服务器向网络发出一个读请求,等待客户端的数据。如果存活检测返回一个错误信息,则将该信息作为读操作的返回值返回给服务器。)在状态2,错误信息类似于 “连接超时”。状态3则为“连接被对方复位”。第四种状态看起来像连接超时,或者根据是否收到与该连接相关的ICMP错误信息,而可能返回其它的错误信息。
四、TCP存活检测的相关参数设置
在各个系统下三个参数对应如下表,括号中给出的是系统的默认值,其中keepalive probs参数在FreeBSD和Mac OS X下为常量。另外在FreeBSD和Mac OS X这两个系统下还有一个net.inet.tcp.always_keepalive的布尔变量,如果这个变量使能,则所有的TCP连接都会有keepalive功能,即使应用程序没有请求keepalive功能(Linux通过libkeepalive库也可以实现相似功能)。其中Windows系统下的参数在注册表[HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesTcpipParameters]下
keepalive time | keepalive interval | keepalive probs | |
Linxu | net.ipv4.tcp_keepalive_time(7200) | net.ipv4.tcp_keepalive_intvl(75) | net.ipv4.tcp_keepalive_probes(9) |
FreeBSD | net.inet.tcp.keepidle(7200) | net.inet.tcp.keepintvl(75) | 8 |
Mac OS X | net.inet.tcp.keepidle(7200) | net.inet.tcp.keepintvl(75) | 9 |
Windows | KeepAliveTime(7200) | KeepAliveInterval(1) | MaxDataRetries(5) |
RFC1122中规定keepalive time默认不能少于2小时,另外keepalive功能必须默认关闭,除非应用程序请求使用keepalive功能(FreeBSD和Mac OS X系统下net.inet.tcp.always_keepalive设置是违反这个行为的)。
五、wireshark示例
1、通过linux的socket选项,设置keepalive time=5s、keepalive interval=1s、keepalive probes=3。最终如下图所示
client在与server端建立连接后,双发并没有数据进行传输,server每隔5s进行一次keepalive探测,但是No10探测包没有收到client的回复,因此server端又隔了1s重复进行探测,最终发送了三个探测包No10、No11、No12后,server端放弃探测,认定与client端的连接已经断开,因此最后发送一个RST报文后,server端也会释放这个连接。