本文由作者小林coding分享,来自公号“小林coding”,有修订和改动。
1、引言
说到TCP协议,对于从事即时通讯/IM这方面应用的开发者们来说,再熟悉不过了。随着对TCP理解的越来越深入,很多曾今碰到过但没时间深入探究的TCP技术概念或疑问,现在是时候回头来恶补一下了。
本篇文章,我们就从系统层面深入地探讨一个有趣的TCP技术问题:拔掉网线后,再插上,原本的这条TCP连接还在吗?或者说它还“好”吗?
可能有的人会说:网线都被拔掉了,那说明物理层(也叫实体层)被断开了(关于网络协议分层模型请见《快速理解网络通信协议(上篇)》),那在物理层之上的传输层理应也会断开,所以原本的 TCP 连接就不会存在的了。就好像我们拨打有线电话的时候,如果某一方的电话线被拔了,那么本次通话就彻底断了。
答案真的是这样吗?可能并非你理解的这样哦,一起跟随笔者来深入探讨一下。
2、系列文章
本文是系列文章中的第14篇,本系列文章的大纲如下:
3、比较笼统的答案
3.1 答案
引言里我们说到:有人认为,网线都被拔掉了,那说明物理层被断开,那么物理层之上的传输层肯定也会断开,所以原来的 TCP 连接自然也就不存在了。(PS:计算机网络分层详解请见《史上最通俗计算机网络分层详解》)
上面这个逻辑是有问题的。
问题在于:错误的认为拔掉网线这个动作会影响传输层,事实上并不会影响!
实际上:TCP 连接在 Linux 内核中是一个名为 struct socket 的结构体,该结构体的内容包含 TCP 连接的状态等信息。
所以:当拔掉网线的时候,操作系统并不会变更该结构体的任何内容,所以 TCP 连接的状态也不会发生改变。
3.2 实验验证一下
我做了个小实验:我用 ssh 终端连接了我的云服务器,然后我通过断开 wifi 的方式来模拟拔掉网线的场景,此时查看 TCP 连接的状态没有发生变化,还是处于 ESTABLISHED 状态(如下图所示)。
![](https://img2022.cnblogs.com/blog/1834368/202203/1834368-20220307180322322-971985120.png)
通过上面实验结果可以验证我的结论:拔掉网线这个动作并不会影响 TCP 连接的状态。
不过,这个答案还是有点笼统。实际上,我们应该在更具体的场景中来看待这个问题,答案才更准确一些。
这个具体场景就是:
- 1)当拔掉网线后,有数据传输时;
- 2)当拔掉网线后,没有数据传输时。
针对上面这两种具体的场景,我来更具体地来分析一下。我们继续往下阅读。
4、具体场景1:拔掉网线后,有数据传输时
4.1 数据传输过程中,恰好又把网线插回去了
如果是客户端被拔掉网线后,服务端向客户端发送的数据报文会得不到任何的响应,在等待一定时长后,服务端就会触发TCP协议的超时重传机制(详见:《TCP/IP详解 - 第21章·TCP的超时与重传》),然而此时重传并不能得到响应的数据报文。
如果在服务端重传报文的过程中,客户端恰好把网线插回去了,由于拔掉网线并不会改变客户端的 TCP 连接状态,并且还是处于 ESTABLISHED 状态,所以这时客户端是可以正常接收服务端发来的数据报文的,然后客户端就会回 ACK 响应报文。
此时:客户端和服务端的 TCP 连接将依然存在且工作状态不会受到影响,给应用层的感觉就像什么事情都没有发生。。。
4.2 数据传输过程中,网线一直没有插回去
上面这种情况下,如果在服务端TCP协议重传报文的过程中,客户端一直没有将网线插回去,那么服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题。然后就会通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开。
接下来,如果客户端再插回网线,如果客户端向服务端发送了数据,由于服务端已经没有与客户端匹配的 TCP 连接信息了,因此服务端内核就会回复 RST 报文,客户端收到后就会释放该 TCP 连接。
此时:客户端和服务端的 TCP 连接已经明确被断开,原本的这个连接也就不存在了。
4.3 刨根问底:TCP数据报文到底重传几次?
本着知其然更应知其所以然的精神,我们来刨根问底一下:TCP 的数据报文到底有重传几次呢?
在 Linux 系统中,提供了一个叫 tcp_retries2 配置项,默认值是 15(如下图所示)。
![](https://img2022.cnblogs.com/blog/1834368/202203/1834368-20220307180334180-1018483456.png)
如上图所示:这个内核参数是控制 TCP 连接建立的情况下,超时重传的最大次数。
不过 tcp_retries2 设置了 15 次,并不代表 TCP 超时重传了 15 次才会通知应用程序终止该 TCP 连接,内核还会基于“最大超时时间”来判定。
每一轮的超时时间都是倍数增长的,比如第一次触发超时重传是在 2s 后,第二次则是在 4s 后,第三次则是 8s 后,以此类推。
![](https://img2022.cnblogs.com/blog/1834368/202203/1834368-20220307180344159-497734485.png)
内核会根据 tcp_retries2 设置的值,计算出一个最大超时时间。
在重传报文且一直没有收到对方响应的情况时,先达到“最大重传次数”或者“最大超时时间”这两个的其中一个条件后,就会停止重传,然后就会断开 TCP 连接。
PS:有关TCP超时重传机制的详细情况,可以阅读《浅析TCP协议中的疑难杂症(下篇)》。
5、具体场景2:拔掉网线后,有数据传输时
5.1 场景分析
针对拔掉网线后,没有数据传输的场景,还得具体看看是否开启了 TCP KeepAlive 机制 (详见《彻底搞懂TCP协议层的KeepAlive保活机制》)。
1)如果没有开启 TCP KeepAlive 机制:
在客户端拔掉网线后,并且双方都没有进行数据传输,那么客户端和服务端的 TCP 连接将会一直保持存在。
2)如果开启了 TCP KeepAlive 机制:
在客户端拔掉网线后,即使双方都没有进行数据传输,在持续一段时间后,TCP 就会发送KeepAlive探测报文。
根据KeepAlive探测报文响应情况,会有以下两种可能:
- 1)如果对端正常工作:当探测报文被对端收到并正常响应, TCP 保活时间将被重置,等待下一个 TCP 保活时间的到来;
- 2)如果对端主机崩溃或对端由于其他原因导致报文不可达:当探测报文发送给对端后,石沉大海、没有响应,连续几次,达到保活探测次数后,TCP 会报告该连接已经死亡。
所以:TCP 保活机制可以在双方没有数据交互的情况,通过TCP KeepAlive 机制的探测报文,来确定对方的 TCP 连接是否存活。
5.2 刨根问底:TCP KeepAlive 机制具体是什么样的?
TCP KeepAlive 机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文。该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔。
以下是 Linux 中的默认值:
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
解释一下:
- 1)tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制;
- 2)tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
- 3)tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个“死亡”连接。
计算公式是:
![](https://img2022.cnblogs.com/blog/1834368/202203/1834368-20220307180357136-1935839809.png)
注意:应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。
PS:关于TCP协议的KeepAlive 机制详见《彻底搞懂TCP协议层的KeepAlive保活机制》、《一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等》。
5.3 刨根问底:TCP KeepAlive 机制的探测时间也太长了吧?
没错,确实有点长。
TCP KeepAlive 机制是 TCP 层(内核态) 实现的,它是给所有基于 TCP 传输协议的程序一个兜底的方案。
实际上:我们通常在应用层自己实现一套探测机制,可以在较短的时间内,探测到对方是否存活。
比如:一般Web 服务器都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒,Web 服务软件就会启动一个定时器,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。
![](https://img2022.cnblogs.com/blog/1834368/202203/1834368-20220307180406856-1121389897.png)
再比如:IM、消息推送系统里的心跳机制,通过应用层的心跳机制(由客户端发出,服务端回复响应包),来灵活控制和探测长连接的健康度。
《为何基于TCP协议的移动端IM仍然需要心跳保活机制?》这篇文章解释了IM这类应用中应用层心跳保活的必要性,有兴趣可以读一读。
如果对应用层心跳的具体应用没什么概念,可以看看微信的这两篇文章:
- 《微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)》
- 《移动端IM实践:实现Android版微信的智能心跳机制》
下面有几个针对im这类应用的心跳实现代码,可以具体感受学习一下:
- 《正确理解IM长连接的心跳及重连机制,并动手实现(有完整IM源码)》
- 《一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》
- 《自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)》
- 《手把手教你用Netty实现网络通信程序的心跳机制、断线重连机制》
6、本文小结
下面简单总结一下文中的内容,本文开头的问题并不是简单一句话能够准确说清楚的,需要分情况对待。
也就是:客户端拔掉网线后,并不会直接影响 TCP 的连接状态。所以拔掉网线后,TCP 连接是否还会存在,关键要看拔掉网线之后,有没有进行数据传输。
1)有数据传输的情况:
在客户端拔掉网线后:如果服务端发送了数据报文,那么在服务端重传次数没有达到最大值之前,客户端恰好插回网线的话,那么双方原本的 TCP 连接还是能存在并正常工作,就好像什么事情都没有发生。
在客户端拔掉网线后:如果服务端发送了数据报文,在客户端插回网线之前,服务端重传次数达到了最大值时,服务端就会断开 TCP 连接。等到客户端插回网线后,向服务端发送了数据,因为服务端已经断开了与客户端相同四元组的 TCP 连接,所以就会回 RST 报文,客户端收到后就会断开 TCP 连接。至此, 双方的 TCP 连接都断开了。
2)没有数据传输的情况:
- a. 如果双方都没有开启 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,那么客户端和服务端的 TCP 连接状态将会一直保持存在;
- b. 如果双方都开启了 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,TCP keepalive 机制会探测到对方的 TCP 连接没有存活,于是就会断开 TCP 连接。而如果在 TCP 探测期间,客户端插回了网线,那么双方原本的 TCP 连接还是能正常存在。
除了客户端拔掉网线的场景,还有客户端“宕机和杀死进程”的两种场景。
第一个场景:客户端宕机这件事跟拔掉网线是一样无法被服务端的感知的,所以如果在没有数据传输,并且没有开启 TCP keepalive 机制时,,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。
所以:我们可以得知一个点——在没有使用 TCP 保活机制,且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态时,并不代表另一方的 TCP 连接还一定是正常的。
第二个场景:杀死客户端的进程后,客户端的内核就会向服务端发送 FIN 报文,与客户端进行四次挥手(见《跟着动画来学TCP三次握手和四次挥手》)。
所以:即使没有开启 TCP KeepAlive,且双方也没有数据交互的情况下,如果其中一方的进程发生了崩溃,这个过程操作系统是可以感知的到的,于是就会发送 FIN 报文给对方,然后与对方进行 TCP 四次挥手。
7、参考资料
[1] TCP/IP详解 - 第21章·TCP的超时与重传
[2] 通俗易懂-深入理解TCP协议(上):理论基础
[3] 网络编程懒人入门(三):快速理解TCP协议一篇就够
[4] 脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手
[5] 脑残式网络编程入门(七):面视必备,史上最通俗计算机网络分层详解
[6] 技术大牛陈硕的分享:由浅入深,网络编程学习经验干货总结
[7] 网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?
[8] 不为人知的网络编程(十):深入操作系统,从内核理解网络包的接收过程(Linux篇)
[9] 为何基于TCP协议的移动端IM仍然需要心跳保活机制?
[10] 一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等
[11] Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?
(本文同步发布于:http://www.52im.net/thread-3846-1-1.html)