假设这么一种情况,客户端和服务端建立了tcp连接后,客户端向服务端发送了一些数据,然后就不再发送了,tcp连接的两端在过了很长一段时间后都没有任何数据交换,这个时候tcp连接是处于空闲状态的,也就是说,因为双方一直没有交换数据,这个tcp连接就会一直处于空闲,可以持续数小时,数天,数月,甚至更久,双方的连接依然会保持着。
如图1所示,client和server是通过这条通信线路建立tcp连接的,当建立完tcp连接之后双方就再也没有交换任何数据,这意味着这条线路中间的路由器R1和R2可以崩溃或重启,只要两端的主机没有重启,关机或崩溃,则这条连接仍然会保持
(仔细体会一下)。
举个例子,比如说:假设数据是单向流动的,client负责发送数据,而server负责接收数据,client发了几次数据后,就罢工啥也不干了,也不关闭连接。但是server过了一段时间发现client没发数据过来,又不清楚client到底是个什么情况,不确定client是挂了,还是活着,server这边并不会主动关闭连接(万一什么时候server这边主动关闭连接了,而client就马上发数据过来了呢?),因此server只能保持着这个连接跟client干耗着,server表示很无奈。
所以很多时候(当client很久没发数据了),server是很希望知道client是否崩溃,关机或重启的。因此tcp提供了保活计时器,它每隔一段时间,也就是超时后,server会给client发送保活探查报文检测这个连接是不是空闲时间太长了,client收到后会回复一个响应,然后server通过这个响应判断client是否有异常,如果有就主动关闭连接。
保活计时器并不是tcp规范中的一部分,Host Requirements RFC提供了3个不使用保活计时器的理由:
1.在出现短暂差错的情况下,这可能会使一个非常好的tcp连接终止(后面会说原因)
2.它会耗费不必要的带宽
3.在按流量计费的情况下会花费更多的钱(跟我们用手机流量上网一个道理)
但即便是保活计时器缺点很多,很多实现还是提供了保活计时器。
比如在图1中,client和server之间的通信线路是比较稳定的,但是中间出现了临时的网络故障,比如当R1路由器发生崩溃正在重启时,而server恰好在这个时间点发送了保活探查报文,当这个保活探查报文传输到R1时,发现R1崩溃了,那么tcp会误认为client主机已经崩溃,并终止client和server之间的连接
,而事实上并非如此。
也就是说,原本client和server之间保持着一个比较活跃的tcp连接,但是由于中间出了小插曲(R1崩溃),导致这个活跃的连接被终止,这是保活计时器非常严重的缺点。
关于保活计时器的保活功能可以开启Keep Alive选项来实现的,client和server都可以设置,但通常都是由server来设置。如果client也想了解server的情况也可以开启Keep Alive选项,但是在这里我们假设server开启了Keep Alive选项。
如果client和server之间的tcp连接空闲了2小时,则server的保活计时器超时,会向client发送一个探查报文,然后客户端通常处于以下几种状态:
1. 此时client运行正常,收到server的探测报文后就发送了响应报文,因此server通过响应报文知道了client此时是正常的,然后就会将保活计时器复位。
2. 此时client已经崩溃,并且关机或正在重启,不管是在哪一种情况,client都没有发送响应报文,server会每隔75秒发送一个探查报文,总共发10次,如果server一个都没收到则认为client挂了,就会关闭tcp连接(说明:因为tcp/ip详解年代实在是太过久远了,不知道保活计时器是否还是75秒,大家在看的时候注意一下就行)。
3. 此时client已经重启并正常运行了(client重启时关闭了之前的tcp连接),这时client收到server的探测报文,则会发送一个RST报文,然后服务器收到RST报文会关闭这个连接。
4. client运行正常,但是server不可达(和ICMP差错报告中的主机不可达一个道理)。这种情况和第二种是一样的,因为服务器不能区分对端异常与中间链路异常。
而对于第2,3,4种情况,server的应用进程将会收到tcp的差错报告报文,比如第3种情况,server的应用进程就收到了RST报文,表示连接复位的错误信息。第2种情况,应该是连接超时的错误信息。第4种情况看起来是和第2种类似,但是得根据ICMP差错报告报文返回什么错误信息,来确定具体的错误信息。
如果tcp没有提供这种保活计时器功能,也没有应用层的定时器,则client将永远无法知道对端已经崩溃或崩溃并重启,因此大多数时候,我们写服务器程序的时候,会在应用层实现一个 Keep Alive,而不使用 TCP 提供的,比如常见的 HTTP 协议中就实现了这个功能,还有 SSH 协议也实现了自己的 keep alive 功能。
SO_KEEAPLIVE套接字选项的原理在前面2小节已经介绍过了,这里只介绍如何使用SO_KEEAPLIVE套接字选项实现保活计时器功能。
假设server的套接字设置了SO_KEEAPLIVE选项后, 如果client和server之间的tcp连接没有数据交换,空闲了2小时,那么server的保活计时器就会超时,并向client发送一个保活探测报文(keep alive probe):
1 . 如果client已经重启并正常运行(client重启时关闭了之前的tcp连接),这时client收到server的探测报文,则会发送一个RST报文,然后服务器收到RST报文会关闭这个连接,并返回ECONNRESET错误。
2 . 如果client没有发送响应报文,server会每隔75秒发送一个探查报文,总共发10次,如果server没有收到任何响应则认为client挂了并关闭tcp连接,并返回ETIMEOUT错误。
3. 如果server收到一个ICMP差错报文响应时(常见的错误,比如host unreachable),那就返回相应的错误,说明client没有挂掉,只是不可达,这种情况下会返回EHOSTUNREACH错误。
关于实验:
相信大家也知道了,保活计时器的超时时间设置为2小时,时间有点长,因此这个实验就交给大家来完成了。