菜鸟笔记
提升您的技术认知

深入理解TCP三次握手四次挥手

阅读 : 1183

目录

TCP报文格式

 TCP 三次握手

TCP三次握手的疑惑

问题一:为什么是三次握手,不是两,四次呢?

避免历史链接

同步双方的初始序列号

小总结

问题二:第一次握手丢失

问题三:第二次握手丢失

问题四:第三次握手丢失

问题五:每次建立 TCP 连接序号都不一样

问题六:初始序列号 ISN 是如何随机产生的

问题七:TCP 和 UDP 可以同时绑定相同的端口吗

问题八:IP 层会分片,为什么 TCP 层还需要 MSS 

问题九:什么是 SYN 攻击?如何避免 SYN 攻击

问题十:accept发生在三次握手的哪一个阶段?

问题十一:listen 时候参数 backlog 的意义

TCP 四次挥手

TCP 四次挥手的疑惑

问题一:为什么挥手需要四次,而不是三次?

问题二:第一次挥手丢失

问题三:第二次挥手丢失

问题四:第三次挥手丢失

问题五:第四次挥手丢失

问题六:为什么 TIME_WAIT 等待的时间是 2MSL

问题七:为什么需要 TIME_WAIT 状态

保证被动关闭方正确关闭

问题八:TIME_WAIT 过多有什么危害

问题九:如何优化 TIME_WAIT

问题十:如果已经建立了连接,但是客户端的进程崩溃会发生什么

问题十一:如果已经建立了连接,但是客户端突然出现故障了怎么办

总结

我们在编写tcp网络的时候,经常会遇到各种问题而头疼,但我们还是那么的喜欢它,哈哈哈~ 正像那句话“纵你虐我千百遍,我仍待君如初恋”啊。好了我就不废话了,搞起来吧!

这边文章主要深入分析TCP三握四挥,关于TCP其他的知识可以看TCP/IP四层模型详细分析

TCP报文格式

TCP详细报文

 

TCP报文

 在TCP三握四挥中使用的重要:序号,确认号,TCP标识(控制位)。

序号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。

确认号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。

TCP标识:最主要的是ACK,RST,SYN,FIN这四个标志位。

ACK:该位为 1 时,“确认号”的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1。

RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。

SYN:该位为 1 时,表示希望建立连接,并在其“序号”的字段进行序列号初始值的设定。

FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

TCP标记

 TCP 三次握手

TCP是面向连接的,所以在使用TCP之前必须通过三次握手建议连接。三次握手过程图如下:

 三次握手过程图解析:

一开始:客户端和服务端都是 CLOSED 状态。先是服务端主动监听某个端口,并进入 LISTEN 状态

第一次:客户端向服务端建立连接,T把TCP报文中的序号设置为seq,同时设置TCP标记中的SYN位为1,发送报文给服务端客户端进入SYN_SEND状态

第二次:服务端收到SYN报文之后,同意建立新连接,随机初始化自己的序号seq y,把TCP报文中的序号设置为seq y,并把确认号设置为seq x+1,同时设置TCP标记中的SYN位和ACK位为1,然后发送报文给客户端,服务端进入SYN_RCVD 状态

第三次:客户端收到服务端报文之后,会给出一个应答报文。把TCP报文中的确认号设置为seq y+1,同时设置TCP标记中的ACK位为1,然后发送报文发给服务端,客户端进入ESTABLISHED状态;当服务端收到客户端发来的报文之后,状态就进入ESTABLISHED状态;第三次握手可以携带数据(但不建议)

那么这些连接状态我们怎么查看呢,可以通过netstat -napt命令查看,如一下图:

TCP三次握手的疑惑

 对于三次握手我们都会有很多疑问?那么我们现在一一来掰开它们的面纱:

问题一为什么是三次握手,不是两,四次呢?

原因有三:

1. 为了防止旧的重复连接初始化造成混乱(主要原因)

2. 同步双方的初始序列号

3. 避免资源浪费

那么我们下面对三个原因做简单的剖析:

避免历史链接

网络拥堵环境中,如果客户端发起多次SYN建立连接,而且旧SYN报文最新的SYN报文,先到达服务端,服务端会给客户端回一个SYN-ACK报文,客户端收到服务端的报文之后,会根据自己的上下文来判断是不是历史链接(序列号过期或超时),如果是,那么就会给服务端发送RST报文中止这次连接(即重置新建连接),如下图:

这就是三次握手可以避免历史链接的过程,而两次握手是无法避免的。为什么TCP两次握手无法避免呢?

结论:因为两次握手被动发起方(服务端)没有中间状态给主动发起方(客户端)来阻止历史连接,导致被动发起方可能建立一个历史连接,造成资源浪费。

两次握手的话,在客户端发起建立连接,服务端收到SYN报文之后,服务端就进入了ESTABLISHED状态(意味着服务端可以接受数据了),但是客户端还在SYN_SEND状态中,当它收到服务端的ACK报文的时候,会通过自身的上下文判断这个链接是不是历史链接,如果是历史链接,那么客户端就会发送RTS报文给服务端来断开连接,而服务端是ESTABLISHED状态,它可以发送数据,它并不知道这是一个历史链接,直到收到客户端发来的RTS报文。这就导致了服务端在第一次握手状态转化为ESTABLISHED之后,客户端发来的RTS报文之前,发送一些数据,而浪费了资源。如果客户端发生很多这样的情况,那么就会建立很多这样冗余的无效链接,造成很多资源浪费。

 因此,要解决这个问题,最好在服务端发送数据之前(即建立好连接之前),阻止历史链接,就可以避免资源浪费,所以要进行三次握手

同步双方的初始序列号

序号是确保TCP协议可靠性通信的关键因素,它可以去除重复的数据,可以根据数据包的序列号按序接收,可以根据ACK知道那些数据包给接收了

可见序号在TCP连接中的重要性,客户端在建立连接发送SYN报文中会携带自己的序号给服务端,服务端收到之后,会给出应答并把自己的序号也携带给客户端,客户端收到之后,也做出相应的应答,这样就确定了双方之间的序号而到达可靠传输。

而有的同学就会问,那第二次握手应该是两步(即应答客户端SYN报文和发送自己的序号),那不就是四次握手了吗?是的。但没有任何意义,而且还浪费了网络资源(因为每个包都会有报头),所以就成了三次握手了。

小总结

「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号

「四次握手」:三次握手就是最少次数建立可靠连接,所以不需要使用更多的通信次数

问题二:第一次握手丢失

客户端和服务端建立TCP连接,会先发一个SYN报文,客户端进入SYN_SEND状态,在这之后客户端一直没有收到服务端的SYN-ACK报文,那么客户端就会触发超时重传(不同的linux内核超时时间,而且不可配)机制,重新发SYN报文。

那客户端会一直重传一下吗?答案是当然不会。客户端重传次数会根据内核参数tcp_syn_retries来定。一般默认是5。而且每增加一次重传次数,超时重传时间就是上次超时重传时间的2倍

直到tcp_syn_retries参数次的次数用完,如果还没有回应就会自动断开TCP连接,不再发SYN报文

问题三:第二次握手丢失

在服务端收到客户端的SYN报文之后,给客户端发送SYN-ACK报文时丢失(即第二次握手),那么服务端进入SYN_RECV状态,此时客户端的状态是SYN_SEND。

第二次握手是由SYN-ACK报文,即对客户端的SYN报文应答和服务端自己的SYN报文,由于客户端一直没有收到自己发送的SYN报文应答,所以它会触发超时重传机制; 而且服务端发送的SYN报文也没有得到客户端的应答,所以服务端也会触发超时重传机制,重传SYN-ACK报文。

而SYN-ACK报文重传次数也有限制,它是有内核参数tcp_synack_retries来决定的,一般默认是5

问题四:第三次握手丢失

当客户端说到服务端的SYN-ACK报文之后,会给服务端发送一个ACK报文(即第三次握手),并却客户端进入ESTABLISHED状态。由于服务端一直没有收到客户端的ACK报文,所以就会触发超时重传机制,重传SYN-ACK报文。直到成功或者次数使用完。

注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文

问题五:每次建立 TCP 连接序号都不一样

主要原因:

防止历史报文被下一个相同四元组的连接接收(主要原因)

为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收

假如每次建立TCP连接,客服端和服务端初始化序号都是从0开始。如果在客户端发送数据包的时候网络堵塞了,而服务端进行了重启,就会发送RTS报文断开连接。然后客户端和服务端又建立的一个新的连接而且有相同的四元组(源地址、源端口、目的地址、目的端口),建立好之后,上次给网络堵塞的数据包刚好到达服务端,而且序号也刚好在服务端的收发窗口内,那么数据就会给正常接收,而制成数据错乱。如下图:

 结论是:如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。

每次建立新连接时,如果客户端和服务端的初始化序号都不一样很大程度上历史报文数据的序号不在对方接收窗口内,被丢弃,从而到达避免历史报文数据被接收的情况。

问题六:初始序列号 ISN 是如何随机产生的

在第五个问题的时候,我们收到初始化序号的不一样的重要性。那么它是怎么随机产生的呢?

始 ISN 是基于时钟的,每 4 微 + 1,转一圈要 4.55 个小时

RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。

M 是一个计时器,这个计时器每隔 4 微秒加 1。
F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

但是序列号的上限值是 4GB,会发生回绕的情况。如果在短时间内发送回绕,那么也会发生历史报文问题。

为了解决这个问题就需要使用TCP时间戳来解决,我们通过tcp_timestamps 参数来控制TCP时间戳的使用,默认是开启的。这样TCP头部就有时间戳选项,该选项即便于精确计算 RTT(时间延迟),又能防止序列号回绕(PAWS)。

防回绕序列号算法要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包

结论:客户端和服务端的初始化序列号都是随机生成,能很大程度上避免历史报文被下一个相同四元组的连接接收,然后又引入时间戳的机制,从而完全避免了历史报文被接收的问题

问题七TCP 和 UDP 可以同时绑定相同的端口吗

答案是可以的。因为我们在IP包头就知道该包是TCP/UDP包,进而送到不同的模块中处理,在TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。

结论:TCP/UDP在内核中是两个完全独立的模块,而端口号的作用,是为了区分同一个主机上不同应用程序的数据包。所以可以同时绑定相同的端口。

问题八:IP 层会分片,为什么 TCP 层还需要 MSS 

分析问题之前先认识MTU和MSS,如下图:

MTU:一个网络包的最大长度,以太网中一般为 1500 字节

MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度

如果整个TCP报文都交给IP层进行分片,那么当在IP层的数据包超过MTU大小时,那么IP层会将数据分片成若干片,而且每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 传输层。但是这存在隐患,就是当有一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。

而 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。所以当TCP 报文的某一片丢失后,就会发生超时重传,而却是重传整个TCP报文。这是非常不理想的。

所以为了到达更好的传输效率,TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,IP层数据包就不会大于MTU了,也不进行分片。而如果此时TCP分片丢失了,那么TCP发生超时重传,而却重传也是以MSS 为单位,这样就不用重传所有分片了。

结论:TCP 层有MSS,在TCP数据包要在IP层进行分片,而却出现分片丢失的情况下,可以提高重传的效率。

问题九:什么是 SYN 攻击?如何避免 SYN 攻击

SYN攻击的基础是依靠TCP建立连接时三次握手的设计。即攻击者短时间伪造不同 IP 地址的 SYN 报文,向服务端发送该SYN报文,是服务端进入到SYN_RECV状态,但服务端回应的SYN-ACK报文,无法发送到客户端(因为IP是未知的),而得不到ACK报文应答,慢慢的服务端的半连接队列就会给占满了,使其无法正常提供服务。

查看SYN攻击指令:

netstat -ant|grep SYN_RECV|wc -l

避免 SYN 攻击的办法:

  • 缩短超时(SYN Timeout) 时间 
  • 增加最大半连接数(SYN队列)  通过设置net.ipv4.tcp_max_syn_backlog
  • 过滤网关防护
  • SYN cookies技术 , 通过设置net.ipv4.tcp_syncookies = 1
  • 减少syn+ack重传次数  通过设置tcp_synack_retries

问题十:accept发生在三次握手的哪一个阶段?

accept发生在三次握手完成之后调用。因为accpet接口就是从全连接队列(Accept 队列)取出连接的,所以肯定是连接建立完成,可以相互传输数据时调用。

问题十一:listen 时候参数 backlog 的意义

//socketfd 为 socketfd 文件描述符
//backlog,这参数在历史版本有一定的变化.在早期 backlog 是 SYN 队列大小,在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度
int listen (int socketfd, int backlog)

在listen 中backlog 是 accept 队列大小,但上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)

TCP 四次挥手

在客户端和服务端断开连接的时候会发生四次挥手的过程,如下图:

 四次挥手过程图解析:

第一次:主动关闭端(A)先发送连接释放报文段,此时会将TCP中的TCP标记为的FIN标记位置为1,并发送出FIN报文,然后A就进行FIN_WAIT_1状态。注:FIN报文段即使不携带数据也要消耗一个序列。

第二次:被动关闭端(B)在接收到A方的FIN报文后,会向A方发送ACK应答报文,并进入CLOSED_WAIT 状态

注:TCP服务器这时会通知高层应用进程,从A到B这个方向的连接就断开了,这时TCP连接处于半关闭(half-close)状态;但B到A这个方向的连接并没有断,B任然可以向A发送数据。

第三次:当A收到B发来的ACK报文之后,A的状态变为FIN_WAIT_2,继续等待B发送连接释放报文(FIN);B会一直发送数据,直到B把数据发送完,B就会向A发送FIN报文,进入LAST_ACK状态

第四次:A收到B发送的FIN报文之后,的状态变为TIME_WAIT,然后回一个ACK报文给B;当B收到ACK报文之后,状态进入CLOSED。而A会等待2MSL时间长度进入CLOSED状态

TCP 四次挥手的疑惑

问题一:为什么挥手需要四次,而不是三次?

主要原因:

1. 关闭连接时,主动关闭端向被动关闭端发送 FIN 报文,这只代表主动关闭端不再发送数据了但是还能接收数据

2. 被动关闭端收到主动关闭端的 FIN 报文之后,会先回一个 ACK 应答报文,而被动关闭端可能还有数据需要处理和发送,等被动关闭端不再发送数据时,才发送 FIN 报文给主动关闭端来表示同意现在关闭连接

问题二:第一次挥手丢失

第一次挥手,主动关闭端向被动关闭端发送FIN报文,进入到FIN_WAIT_1状态。如果主动关闭端一直没有收到被动关闭端的ACK应答报文,那么主动关闭端就会触发超时重传机制,重传FIN报文,重传次数通过tcp_orphan_retries 参数控制。如果重传次数超过tcp_orphan_retries ,就不在发送FIN报文,而直接关闭连接,进入CLOSED状态

问题三:第二次挥手丢失

第二次挥手就是被动关闭端收到主动关闭端发来的FIN报文,并发送ACK报文,被动关闭端进入CLOSE_WAIT 状态。由于ACK报文是不会进行重传的,所以还是主动关闭端等待超时重传,重传FIN报文,直到成功或者重传次数使用完。

注意!如果第二次挥手主动关闭端收到被动关闭端的ACK报文了,主动关闭端进入了FIN_WAIT2 状态,在等待被动关闭端发来FIN报文。但是对于使用close 函数关闭的连接,由于无法再发送数据,所以FIN_WAIT2 状态不会持续太久,持续时间由tcp_fin_timeout参数来控制,默认是60s,如果超过这个数据还没有收到被动关闭端FIN报文,主动关闭端会直接关闭连接,进入CLOSED状态

问题四:第三次挥手丢失

第三次挥手就是被动关闭端收到主动关闭端发来的FIN报文,并分别发送了ACK报文和FIN报文,同时被动关闭端进入 LAST_ACK 状态。但一直没有收到主动关闭端的ACK报文,那么被动关闭端就会触发超时重传机制,直到重传收到主动关闭端的ACK报文或者重传次数(使用tcp_orphan_retries 参数控制)使用完。

问题五:第四次挥手丢失

第四次挥手就是主动关闭端收到被动关闭端的FIN报文之后,进入TIME_WAIT 状态(TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态),然后被动关闭端一直处于LAST_ACK 状态。由于被动关闭端一直没有收到ACK报文,所以会触发超时重传。

问题六:为什么 TIME_WAIT 等待的时间是 2MSL

MSL 是 Maximum Segment Lifetime,报文最大生存时间,超过这个时间报文将被丢弃。因为TCP报文是基于IP协议上的,而IP头中有一个TTL字段,即IP数据报可以经过的最大路由数,每经过一处路由器减1,直到为0将数据报丢弃,同时发送ICMP报文回源主机。

MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。TTL默认值是64,MSL默认值是30秒(在Linux 系统停留在 TIME_WAIT 的时间固定为60 秒(2MSL)),意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文在网络中消失。

TIME_WAIT为什么是2MSL呢?在网络中发送数据包,当这些发送的数据包被接收之后会向对方回应一个数据包,所以需要等待一个来回的时间,即2MSL。这也说明了至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。如果时间太长了,性价比并不高,因为发生这种情况的概率几乎为万分之一。

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时

问题七:为什么需要 TIME_WAIT 状态

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。而需要这个状态的主要是两个原因:

防止历史连接中的数据,被后面相同四元组的连接错误的接收

保证被动关闭连接的一方,能被正确的关闭

避免历史链接数据

假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?

 因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的

保证被动关闭方正确关闭

TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。

假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSED 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。

服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。

为了防止这种情况出现,客户端必须等待足够长的时间确保对端收到 ACK,如果对端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。

 但是你可能会说重新发送的 ACK 还是有可能丢失啊,没错,但 TCP 已经等待了那么长的时间了,已经算仁至义尽了

问题八:TIME_WAIT 过多有什么危害

主要原因:

内存资源占用

对端口资源的占用,一个 TCP 连接至少消耗发起连接方的一个本地端口

第二个危害是会造成很严重的后果的,因为端口资源是有限的。般可以开启的端口为 32768~61000,也可以通过参数net.ipv4.ip_local_port_range来设置。

发起连接方受端口资源限制:

客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就 65536 个,被占满就会导致无法创建新的连接

被动连接方受系统资源限制:

由于一个四元组表示 TCP 连接,理论上服务端可以建立很多连接,因为服务端只监听一个端口,不会因为 TCP 连接过多而导致端口资源受限。但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等

所以如果发起连接方有太多TIME_WAIT 状态的连接,会占满了所有端口资源,则会导致无法创建新连接。

问题九:如何优化 TIME_WAIT

方法:

设置 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 

net.ipv4.tcp_max_tw_buckets

程序中使用 SO_LINGER ,应用强制使用 RST 关闭

net.ipv4.tcp_tw_reuse 和 tcp_timestamps

结论:如果这两个参数开启之后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用

tcp_tw_reuse 功能只能用连接发起方,因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用,即tcp_tw_reuse设置为1。但使用这个参数必须设置tcp_timestamps为1(即打开对 TCP 时间戳的支持)。

这个时间戳的字段是在 TCP 头部的「选项」里,它由一共 8 个字节表示时间戳,其中第一个 4 字节字段用来保存发送该数据包的时间,第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。

由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃

net.ipv4.tcp_max_tw_buckets 

设置这个参数时,就是说当TIME_WAIT 状态超过这个参数值,会这个TIME_WAIT 状态的连接直接重置,比较暴力

程序中使用 SO_LINGER

我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为

struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));

如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。

但这为跨越TIME_WAIT状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。

前面介绍的方法都是试图越过 TIME_WAIT状态的,这样其实不太好。虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。所以不要试图避免这个状态,而是应该弄清楚它。

如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT
 

问题十:如果已经建立了连接,但是客户端的进程崩溃会发生什么

如果客户端的进程崩溃了,服务端会发送 FIN 报文,与客户端进行四次挥手。

问题十一:如果已经建立了连接,但是客户端突然出现故障了怎么办

TCP 有一个机制是保活机制。这个机制的原理是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

net.ipv4.tcp_keepalive_time=7200 

//7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制

net.ipv4.tcp_keepalive_intvl=75

//表示每次检测间隔 75 秒

net.ipv4.tcp_keepalive_probes=9

//表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接

所以发现一个死亡连接的计算是:

tcp_keepalive_time + (tcp_keepalive_intvl * cp_keepalive_probes)

注意,应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。

如果开启了 TCP 保活,需要考虑以下几种情况:

1. 对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

2. 对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。

3. 是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。

TCP 保活的这个机制检测的时间是有点长,我们可以自己在应用层实现一个心跳机制

总结

这篇文章主要讲解TCP三次握手和四次挥手的过程和所遇到的问题,希望对你有帮助~感谢阅读~