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

article/2025/10/1 17:58:50

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.csdn.net/ilini/article/details/118565606
https://zhuanlan.zhihu.com/p/390939380
https://blog.csdn.net/tdy353021560/article/details/38368845


http://chatgpt.dhexx.cn/article/XbxMgACF.shtml

相关文章

HTTPS 中 TLS 和 TCP 能同时握手吗?

HTTPS 中 TLS 和 TCP 能同时握手吗? 大家好,我是小林。 有位读者在面试的时候,碰到这么个问题: 面试官跟他说 HTTPS 中的 TLS 握手过程可以同时进行三次握手,然后读者之前看我的文章是说「先进行 TCP 三次握手&#…

TCP优化一:TCP 三次握手的优化

TCP 三次握手的性能提升 TCP 是面向连接的、可靠的、双向传输的传输层通信协议,所以在传输数据之前需要经过三次握手才能建立连接。 那么,三次握手的过程在一个 HTTP 请求的平均时间占比 10% 以上,在网络状态不佳、高并发或者遭遇 SYN 攻击…

TCP握手过程和挥手过程

TCP报文首部 源端口和目的端口,各占2个字节,分别写入源端口和目的端口;序号,占4个字节,TCP连接中传送的字节流中的每个字节都按顺序编号。例如,一段报文的序号字段值是 301 ,而携带的数据共有1…

TCP三次握手

TCP协议是传输层协议,是一种面向连接的传输控制协议,可以控制流量的传输。是一种可靠的传输,能够保证数据的完整性,有效性和有序性。 1.TCP建立连接(三次握手) 第一次握手:PC1发送SYN请求&…

TCP握手与挥手详解(附有图)

为什么不是4次握手 首先我们知道TCP是3次握手与4次挥手,为什么不是4次握手呢,因为其中握手请求同步过程中并不需要数据传输因此将两次合并为一次了。 我们需要掌握哪些标志量 SYN:请求同步标志,为1的时候为有效 ACK&#xff1…

为什么TCP需要握手

一、TCP握手流程 二、为什么不是4次握手 TCP的每次请求都是成对的,原则上应该是四次 【Client to Server】第一次SYN,seqx【Server to Client】第二次ACK,seqy,ackx1(没有携带数据的ACK不消耗序列号)【Se…

tcp_tw_recycle引起的TCP握手失败

背景 测试环境的一台Nginx服务器,最近一直被前端同事吐槽网络有问题,经常出现访问HTTP请求时超时,哪怕是静态文件也经常超时。 刚开始以为是公司网络抽风了,也就没放在心上,但持续了一个星期,而且复现率很…

TCP握手过程(正解版)

参考文章 Why do we need a 3-way handshake? Why not just 2-way https://blog.csdn.net/qq_36903042/article/details/102656641 https://blog.csdn.net/qq_36903042/article/details/102513465 大部分网络博客的错误解读 首先需要声明的是, 百度搜索到的大…

网络协议 (五) TCP握手建立连接

一、握手策略 为了可以准确的将数据准确无误地送达目标主机,所有基于 TCP 实现的协议,都需要先完成 TCP 协议的三次握手策略。 1. 首先我们需要了解一下图中提到的几个标志符: 1.序号seq seq 是TCP通信过程中,某一个传输方向上字…

TCP 握手没成功怎么办?

大家好,我是小林。 之前收到个读者的问题,对于 TCP 三次握手和四次挥手的一些疑问: 第一次握手,如果客户端发送的SYN一直都传不到被服务器,那么客户端是一直重发SYN到永久吗?客户端停止重发SYN的时机是什么…

深入理解TCP三次握手

一、TCP 包头格式 首先,TCP报文是TCP层传输的数据单元,也称为报文段,下面就是TCP包头格式: 接下来我们来看看每个字段的含义: 源端口和端口字号: TCP源端口:源计算机上应用程序端的端口号&…

TCP的三次握手及四次挥手详解

三次握手 三次握手过程: (1)第一次握手:Client将标志位SYN置为1(表示要发起一个连接),随机产生一个值seqJ,并将该数据包发送给Server,Client进入SYN_SENT状态&#xff0c…

C语言中main函数参数使用

在C99标准中定义main函数两种正确的写法 int main(void); int main(int argc, char* argv[]);常见的不标准写法 void main() main()这里主要说明带参数的main函数如何使用 int main(int argc, char* argv[]) {int i;for (i0; i<argc; i)printf("%d: %s\r\n", i…

C语言main函数参数、返回值

C语言main函数返回值&#xff1a; main函数的返回值&#xff0c;用于说明程序的退出状态。如果返回0&#xff0c;则代表程序正常退出&#xff1b;返回其他数字的含义则由系统决定&#xff0c;通常&#xff0c;返回非零代表程序异常退出&#xff0c;即使程序运行结果正确也仍需修…

main 函数的参数说明

C/C语言中的main函数&#xff0c;经常带有参数argc&#xff0c;argv&#xff0c;如下&#xff1a; int main(int argc, char** argv)int main(int argc, char* argv[])这两个参数的作用是什么呢&#xff1f;argc 是指命令行输入参数的个数&#xff0c;argv存储了所有的命令行参…

C++ main函数及main函数的参数

C main函数及main函数的参数 1、main函数的几种形式 int main() int main(int argc) int main(int argc,char** argv)//int main(int argc,char* argv[])2、argc表示命令行参数的个数、argv表示命令行参数的值 &#xff08;1&#xff09;写个小代码&#xff0c;用命令行运行…

带参数的main函数

支持C语言的系统允许main函数有两个参数 int main(int argc,char *argv[]){//argc表示从命令行传入的参数的个数&#xff1b;//argv表示从命令行传入的字符串数组&#xff1b; } 回显命令行参数 #include<stdio.h> int main(int argc,char *argv[]){int i;for(i0;i<…

main主函数参数解析

默认的main函数参数 int main(int argc, char *argv[]) {// 主函数的代码逻辑return 0; }其中&#xff0c;int 是主函数的返回值类型&#xff0c;主函数执行完后会返回一个整数值给操作系统&#xff0c;通常返回值为 0 表示程序正常结束&#xff0c;非 0 的返回值表示程序运行…

C语言main函数参数

常见的C语言的main函数都是不带参数的。因此main 后的括号都是空括号。实际上&#xff0c;main函数可以带参数&#xff0c;这个参数可以认为是main函数的形式参数。C语言规定main函数的参数只能有两个&#xff0c;习惯上这两个参数写为argc和argv。因此&#xff0c;main函数的函…

【C语言】main函数的参数

我们先看看主函数main的参数列表 #include <stdio.h>int main(int argc, char *argv[]) {return 0; }argc是一个整型变量&#xff0c;存储的是主函数的参数个数argv[]是一个字符型指针数组&#xff0c;其中存储的是主函数的参数字符串&#xff0c;是一个参数列表 注意:…