TCP三次握手及四次挥手过程中的异常处理

本文详细介绍了TCP连接建立(三次握手)和关闭(四次挥手)过程中可能出现的消息丢失情况及处理机制,包括客户端和服务端的重传策略和超时时间。此外,还讨论了尝试连接不存在的IP或端口时的网络行为,以及在连接状态下进程或主机异常宕机时的TCP连接状态变化。重点解析了keepalive选项在保持连接活跃和检测连接故障中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 消息丢失的情况:

总的原则:
ACK不会重传,SYN和FIN报文段有最大重传次数。无论是SYN还是FIN,达到最大重传次数后对端若仍无响应则直接进入CLOSED状态。

1.1 三次握手过程的消息丢失:

正常的三次握手的流程:

请添加图片描述

1.1.1 第一次握手消息丢失:(SYN)

正常情况下,当客户端想要与服务器建立TCP连接时,首先要发送第一个SYN报文段,然后将本端的TCP状态置为 SYN_SENT。

在这之后,如果客户端没有收到服务端的 SYN+ACK 响应,就会触发超时重传机制。

超时时间判断: 不同版本的操作系统可能超时时间不同,一般为 1秒 或 3秒,由内核配置,修改需要重新编译内核。

重传次数: 由内核参数 tcp_syn_retries 配置,默认值为5,可手动修改。

重传周期: 按指数退避计算重传周期,第一次超时重传是 1秒,第二次超时重传是 2秒,第三次超时重传是 4秒,第四次超时重传是 8秒,第五次超时重传是 16秒,以类类推,直到达到最大重传次数后,客户端不再重传SYN报文段,断开TCP连接。 此时 connect会返回 -1,并设置 errno 为 ETIMEOUT。

[ETIMEOUT] Connection establishment timed out without establishing a connection.

如果达到默认最大重传次数后断开TCP连接,总耗时为:
1+2+4+8+16+32 = 63秒,大约 1分钟。

关于TCP内核参数的修改:

所有TCP/IP参数(注意是TCP/IP协议族的所有参数,不止TCP参数)都位于 /proc/sys/net 目录下,注意此目录下的内容修改都是临时的,任何修改在系统重启后都会丢失。
例如对 tcp_syn_retries 参数的修改:

echo 5 > /proc/sys/net/ipv4/syn_timeout_retries

1.1.2 第二次握手消息丢失:(SYN+ACK)

服务端在收到客户端的第一次握手SYN报文段后,将TCP状态置为 SYN_RECV状态,并发送SYN+ACK报文段给客户端。

如果第二次握手的报文段丢失,服务端会发起重传;客户端由于收不到SYN+ACK,无法判断是第一次握手的SYN报文段丢失,还是第二次握手的SYN+ACK报文段丢失,所以客户端也会发起重传。

客户端的重传过程如 1.1 节所述;服务端的 SYN+ACK重传次数由参数 tcp_synack_retries 配置。

1.1.3 第三次握手消息丢失:(ACK)

客户端在收到服务端的第二次握手SYN+ACK报文段后,将TCP状态置为 ESTABLISH 状态,并发送ACK报文段给服务端。

如果第三次握手的ACK报文段丢失,则服务会重传SYN+ACK报文段,直到收到ACK响应或者达到最大重传次数。

注意: 第三次握手的ACK报文段没有重传,当ACK丢失,只能依靠服务端重传SYN+ACK报文段。

如果ACK彻底丢失,客户端的TCP状态是 ESTABLISH,服务端的TCP状态是 SYN_RCVD,此时如果收到客户端的TCP数据,则:。。。。。。。。。。。。。


1.2 四次挥手过程的消息丢失:

正常的四次挥手流程:

请添加图片描述

1.2.1 第一次挥手消息丢失:(FIN)

当客户端(主动关闭方)调用close()函数后,就会发送FIN报文给服务端(确切的说,调用close将会时fd的引用计数减1,当fd的引用计数为0时,触发TCP四次挥手关闭TCP连接),随后客户端将TCP状态置为 FIN_WAIT_1 的状态。

如果第一次挥手的FIN报文段丢失,则客户端会开启重传流程,重传次数由内核参数 tcp_orphan_retries 控制。

当第一次挥手的FIN报文段达到最大重传次数后,客户端将停止重传,直接进入 CLOSE 状态。

1.2.2 第二次挥手消息丢失:(ACK)

当服务端收到第一次挥手的FIN报文段后,将本端TCP状态置为 CLOSE_WAIT,并向客户端回复第二次挥手的ACK报文段。

上面已经提到,ACK报文段是不会重传的,所以如果ACK丢失,客户端会重传FIN报文,直到收到ACK报文,或达到FIN的最大重传次数。

另外还有一种情况需要注意:
如果客户端成功接收到第二次挥手的ACK报文,则客户端的TCP状态置为 FIN_WAIT_2 ,此时客户端开始等待服务端的第三次挥手FIN报文。
FIN_WAIT_2状态不会持续太久,内核参数 tcp_fin_timeout 控制其时长,默认是 60秒。

对于调用close()关闭的连接,如果在 60秒 后还没有收到第三次挥手的FIN报文,客户端连接会直接进入 CLOSE 状态。

1.2.3 第三次挥手消息丢失:(FIN)

当服务端收到客户端的第二次挥手的FIN报文后,内核会自动的回复ACK,并置TCP状态为 CLOSE_WAIT,顾名思义,CLOSE_WAIT状态表示内核在等待应用进程调用clsoe()函数关闭连接(wait to close)。

此时,内核没有权利替代进程关闭连接,必须由进程主动调用close函数来触发服务端发送FIN报文。

当服务端调用close()函数后,TCP状态将从 CLOSE_WAIT 状态转入 LAST_ACK 状态,如果在超时时间内未收到客户端的第四次挥手 ACK报文,则重传FIN报文,最大重传次数由内核参数 tcp_orphan_retries 控制。

CLOSE_WAIT状态的最大持续时间是多少:
CLOSE_WAIT状态表示的就是内核在等待应用进程调用close()函数来关闭连接,至于应用进程什么时候close,这完全取决于程序。所以,从理论上来讲,只要被动关闭端不断电,进程不退出,那么CLOSE_WAIT状态就会一直持续下午,CLOSE_WAIT的最大持续时间从理论上来讲可以达到无限长

1.2.4 第四次挥手消息丢失:(ACK)

当客户端收到服务端的第三次挥手FIN报文后,TCP状态由 FIN_WAIT_2 转入 TIME_WAIT,并回复第四次挥手ACK报文给服务端。

如果第四次挥手ACK报文丢失,ACK报文不会重传,服务端在超时未收到ACK后会重传FIN,直至成功收到ACK会达到最大重传次数。

TIME_WAIT的最大持续时间默认为 60秒(2*MSL),超时后TCP状态转为 CLOSED。


2. 尝试连接IP不存在或端口存在的目的主机:

2.1 结论:

2.1.1 当试图连接一个IP不存在的主机时:

  1. 如果尝试连接的IP在局域网内,则会发送N次ARP请求,向局域网内请求获取目的主机的MAC地址,且本地主机不能发出TCP握手消息;
  2. 如果尝试连接的IP在局域网外,则会将TCP握手消息通过网关路由发出到局域网外,但因为最终找不到目的地,会触发TCP的超时重传,直至达到最大重传次数后关闭TCP连接。

2.1.2 当试图连接一个IP存在但端口不存在的主机时:

  1. 不论尝试连接的主机是在局域网内还是局域网外,当目的主机收到源主机的TCP握手消息后,判断这个端口上并没有应用进程,内核协议栈会立即回复RST复位报文段,发送端在收到RST后关闭TCP连接。
  2. 如果目的主机设置了防火墙策略,限制他人将消息发送到不对外暴露的端口,那么这种情况目的主机会将收到的TCP握手消息直接丢弃,源主机在超时后进行重传,直至达到最大重传次数后关闭TCP连接。

2.2 原理:

  1. 为什么在连接局域网内不存在的IP时会一直重发ARP请求,而没有发出SYN报文:
      当连接一个局域网内的IP时,主机会先到本地ARP表中查询是否有目的IP的MAC地址,如有则返回,如没有则向局域网广播一条ARP请求,请求获取目的IP的MAC地址。
      当局域网内的其他主机收到ARP请求后,判断请求中的IP是否为自己的IP,如果是则向请求方发送ARQ响应,捎带上自己的MAC地址;如果不是自己的IP则直接将收到的ARP请求丢弃。
      当源主机在本地的ARP表中没有找到与目的IP一致的缓存记录时,此时会将SYN报文放进 unresolved_queue 队列中缓存起来,直至收到有效的ARP响应后,才会将SYN报文从缓存去列中取出并打上MAC头发给目的主机。如果始终得不到ARP响应,则SYN握手消息将一直不会发出。
      注意 ARP协议本身是没有重传机制的,之所以会重复发送ARP请求,是因为触发了TCP三次握手的重传机制,而每一次重传SYN都会对应的向局域网内发出一条ARP请求。
  2. RST报文:
      TCP正常情况下断开连接是用四次挥手,这是正常时候的优雅做法。但异常情况下,收发双方因为某种异常情况无法完成四次挥手,所以就需要一个机制来强行关闭连接
      RST就是用于这种情况,一般用来异常的关闭一条连接。 通过在TCP头中将 RST标志位置为1,在接收端收到后就会关闭连接,此时一般会看到 “connection reset” 或 “connection refused” 的报错。

3. 在连接状态下的进程/主机异常宕机:

3.1 没有开启keepalive选项,且没有数据交互的情况:

一道面试题:
一个TCP连接,没有打开keepalive选项,没有数据交互,现在一端突然掉电和一端的进程crash了,请问这两种情况有什么区别?

  1. 第一种情况:主机掉电:
    对端主机崩溃对于服务端而言是无法感知的 ,此时又没有开启keepalive,又没有数据交互,则服务端的tcp连接会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。
  2. 第二种情况:主机正常,网络进程crash:
    即使是在没有开启keepalive,且没有数据交互的情况下,如果其中一方的进程发生了崩溃,这个过程操作系统是可以感知的到的,于是就会 发送FIN报文给对端,双方进行TCP四次挥手关闭连接。
    注意:在进程异常crash时,系统在检测到进程crash后会发送FIN给对端,注意这里发送的 FIN,走正常的四次挥手关闭流程,不是发送RST直接复位连接。

3.2 没有开启keepalive选项,但有数据交互的情况:

(1)如果是主机正常,网络进程crash,无论是否有数据交互,处理方式都是一样的,即发生进程crash一端的主机系统发送FIN报文,进行TCP四次挥手流程关闭连接。

(2)如果是主机异常宕机,此时可细分为两种情况:

  • 客户端主机宕机,又迅速重启;
  • 客户端主机宕机,一直没有重启。

在客户端主机宕机后,服务端向客户端发送的业务数据报文会得不到ACK响应,在一定时长后,服务端会触发 超时重传 机制,重传未被响应的业务数据报文。

  • 如果在服务端重传业务数据的过程中,客户端的主机重启完成,此时如果客户端的网络进程没有重启,则客户端主机收到服务端的业务数据后判断没有进程监听这个端口,会向服务端回复RST报文重置连接;
  • 如果客户端迅速重启,且主机上有新的进程重新绑定了这个端口,这种情况应该相当于新连接上收到旧连接的报文来处理?
  • 如果客户端主机宕机后,一直没有重启,则服务端的重传报文达到一定阈值后,内核就会判断这条tcp连接出现问题,然后通知应用进程连接不可用(send返回-1 ?)

tcp_retries2

在Linux系统中,通过 tcp_retries2 配置参数控制报文的最大重传次数,默认值为15,每次重传的时间以指数退避的方式计算。当达到最大重传次数后,tcp就会停止重传。


3.3 已开启keepalive选项的情况:

TCP的keepalive保活机制默认是关闭的,要想使用,必须通过设置套接字选项的方式手动开启:

bool bKeepAlive = TRUE;
int nRet = setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (char*)&bKeepAlive, sizeof(bKeepAlive));
if(nRet == SOCKET_ERROR) {
    return FALSE;
}

关于keepalive机制的默认配置:

net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9

如果一条tcp连接在连续7200秒的时间内(2小时)都没有数据报文,则会触发保活检测,每次发送探测报文的时间间隔是75秒,最多发送9轮。
所以最大时间是:
7200 + 75*9 = 2小时11分15秒

保活机制的探测结果有三种情况:

  1. 对端程序正常运行,且中间网络通畅,此时keepalive探测报文能够顺利到达对端,对端在收到报文后回复ACK,重置tcp保活时间;
  2. 对端主机正常,但网络进程崩溃 (正常情况下发生这种问题时,对端主机的操作系统内核能够检测到网络进程崩溃,此时内核会自动发起四次挥手关闭连接,但是如果FIN报文段丢失并,在达到FIN最大重传次数后TCP进入CLOSED状态,就需要依赖keepalive保活机制了),在对端主机收到keepalive检测报文后,判断目的端口没有应用进程监听,则回复一条RST报文关闭tcp连接;
  3. 对端主机宕机,且未重启,中间网络正常,对端主机崩溃对于服务端来说是无法感知的,此时发送的keepalive报文石沉大海,没有主机对其响应,需要达到最大检测次数后关闭连接;
  4. 对端主机宕机,并已重启,不论网络进程是否重启,在收到keepalive检测报文后,回复RST重置连接;
  5. 中间网络出现问题,这种情况下无论对端主机、网络进程是否正常,keepalive报文都不可达,需要等待达到最大检测次数后关闭tcp连接。

对于正常四次挥手关闭,tcp在收到FIN报文后,会返回可读事件,应用程序调用read后返回0,说明对方已经close;

对于keepalive超时的情况,当tcp检测到对端已不可达,会返回一个可读事件,应用程序调用recv会返回-1,errno被置为 ETIMEOUT,方便应用程序及时清理不可用的连接。


参考内容:

https://blog.youkuaiyun.com/ilini/article/details/118565606
https://zhuanlan.zhihu.com/p/390939380
https://blog.youkuaiyun.com/tdy353021560/article/details/38368845

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值