部分内容来源:小林coding
TCP四次挥手过程是怎样的
天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥手方式
双方都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥手的过程如下图
1.客户端打算关闭连接,此时会发送一个 TCP 首部 FIN
标志位被置为 1
的报文,也即 FIN
报文,之后客户端进入 FIN_WAIT_1
状态。
2.服务器收到报文后,就向客户端发送 ACK
应答报文,接着服务端进入 CLOSE_WAIT
状态。客户端收到服务器的 ACK
应答报文后,之后进入 FIN_WAIT_2
状态。
3.等待服务器处理完数据后,也向客户端发送 FIN
报文,之后服务端进入 LAST_ACK
状态。
4.客户端收到服务器的 FIN
报文后,回发 ACK
应答报文,之后进入 TIME_WAIT
状态。服务器收到了 ACK
应答报文后,就进入 CLOSE
状态,至此服务器端已经完成连接的关闭。客户端经过 2MSL
一段时间后,自动进入 CLOSE
状态,至此客户端也完成连接的关闭。
你可以看到,每个方向都需要一个 FIN
和一个 ACK
,因此通常被称为四次挥手。
这里一点需要注意是:主动关闭连接的一方才有 TIME_WAIT
状态
为什么挥手需要四次
再来回顾下四次挥手双方发 FIN
包的过程,就能理解为什么需要四次了。
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 - 服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务器端可能还有数据要处理和发送,等服务器端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务器端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,因此是需要四次挥手。
但是在特定情况下,四次挥手是可以变成三次挥手的,具体情况可以看这篇:TCP 四次挥手,可以变成三次吗?
第一次挥手丢失了会发生什么?
当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。
正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT_2 状态。
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文
重发次数由 tcp_orphan_retries 参数控制。
当客户端传输 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,机会在等待一段时间(时间为上一次超时的 2 倍),如果还是没能收到第二次挥手,那么直接进入 close 状态。
第二次挥手丢失了会发生什么
当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。
在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数
这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 FIN_WAIT_2 状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN 报文。
close () 函数与 FIN_WAIT_2 状态的时长:
当使用 close () 函数关闭连接时,该连接就无法再发送和接收数据了。
为了避免 FIN_WAIT_2 状态持续过长时间占用资源,系统通过 tcp_fin_timeout 这个参数来控制处于该状态下连接的持续时长,默认值是 60 秒。也就是说,超过 60 秒还没收到服务端的 FIN 报文,连接就会进行相应处理(比如释放资源等)
shutdown () 函数与 FIN_WAIT_2 状态:
果主动关闭方使用 shutdown () 函数关闭连接,并且指定只关闭发送方向,接收方向并没有关闭,那么主动关闭方仍然可以接收数据。
在这种情况下,如果主动关闭方一直没有收到服务端发送的第三次挥手(FIN 报文),那么主动关闭方的连接就会一直处于 FIN_WAIT_2 状态(不会超时关闭),
因为它还在等待接收服务端的数据,所以不会按照 tcp_fin_timeout 设定的时间去结束这个状态
close()
函数
在网络编程中,close()
函数通常用于关闭一个已经建立的 TCP 连接。当调用 close()
函数时,意味着该连接不再用于发送和接收数据。从 TCP 协议的角度来看,调用 close()
会触发 TCP 连接的关闭流程,即发送一个 FIN(Finish)报文给对方,表示本方已经没有数据要发送了
shutdown()
函数
shutdown()
函数也是用于关闭 TCP 连接的,但它比 close()
函数更加灵活。shutdown()
函数可以指定关闭连接的不同方向,即只关闭发送方向、只关闭接收方向或者同时关闭两个方向
第三次挥手丢失会发生什么
当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。
此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。
服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,
同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的
第四次挥手丢失会发生什么
当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。
在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍的 tcp_orphan_retries 参数控制
为什么需要Time_Wait状态
主动发起关闭连接的一方,才会有 TIME-WAIT 状态
需要 TIME-WAIT 状态,主要是两个原因:
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
- 保证「被动关闭连接」的一方,能被正确的关闭;
原因一:防止历史连接中的数据,被后面相同四元组的连接错误的接收
TIME_WAIT状态保持一定时间,防止错误接收数据
为了能更好的理解这个原因,我们先来了解序列号(SEQ)和初始序列号(ISN)。
- 序列号:是一个头字段,标识了 TCP 发送端到 TCP 接收端的数据流的一字节,因为 TCP 是面向字节流的可靠传输协议,为了确保数据的顺序性和可靠性,TCP 为每个传输方向上的每个字节都赋予了一编号,以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。序列号是一个 32 位的无符号数,因此在到达 4G 之后会循环到 0。
- 初始序列号:在 TCP 建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都有不同的初始序列号。初始序列号与 32 位的序列号数,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时
通过前面我们知道,序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况
这意味着无法根据序列号来判断新老数据。
假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢
- 服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
- 接着,服务端以相同的四元组重新打开了新连接,前面被延迟的 SEQ = 301 这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。
为了防止历史连接中的数据,被后面相同四元组的连接错误的接收
因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃
使得原来连接的数据包在网络中自然消失,再出现的数据包一定都是新建立连接所产生的
原因二:保证「被动关闭连接」的一方,能被正确的关闭
客户端必须等待足够长的时间,确保服务端能够收到 ACK
TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。
假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文
服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer)
这对于一个可靠的协议来说不是一个优雅的终止方式
为了防止这种情况出现,客户端必须等待足够长的时间,确保服务端能够收到 ACK,如果服务端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一来一往刚好两个 MSL 的时间
TIME_WAIT过多会有什么危害?
过多的 TIME-WAIT 状态主要的危害有两种:
- 占用系统资源:比如文件描述符、内存资源、CPU 资源、线程资源等;
- 占用端口资源:端口资源也是有限的,一般可以开启的端口为 32768~61000 ,也可以通过 net.ipv4.ip_local_port_range 参数指定范围。
客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。
客户端的 TIME_WAIT 状态过多
客户端(发起连接方)都是和「目的 IP+ 目的 PORT 」都一样的服务端建立连接的话,当客户端的 TIME_WAIT 状态连接过多的话,就会受端口资源限制
如果占满了所有端口资源,那么就无法再跟「目的 IP+ 目的 PORT」都一样的服务端建立连接了。
不过,即使是在这种场景下,只要连接的是不同的服务端,端口是可以重复使用的,所以客户端还是可以向其他服务端发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源 IP、源端口、目的 IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。
服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多
如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限
因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等
服务器出现大量 TIME_WAIT 状态的原因有哪些?
首先要知道 TIME_WAIT 状态是主动关闭连接方才会出现的状态,所以如果服务器出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务器主动断开了很多 TCP 连接。
问题来了,什么场景下服务器端会主动断开连接呢?
- 第一个场景:HTTP 没有使用长连接
- 第二个场景:HTTP 长连接超时
- 第三个场景:HTTP 长连接的请求数量达到上限
如果已经建立了连接,但是客户端突然出现故障了怎么办?
客户端出现故障指的是客户端的主机发生了宕机,或者断电的场景。发生这种情况的时候,如果服务端一直不会发送数据给客户端,那么服务端是永远无法感知到客户端宕机这个事件的,也就是服务端的 TCP 连接将一直处于 ESTABLISH 状态,占用着系统资源。
为了避免这种情况,TCP 搞了个保活机制。这个机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔发送一个探测报文,该探测报文包含的数据是异常小,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将该错误信息通知给上层应用程序
说一下TCP的保活机制
TCP保活机制的原理:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔发送一个探测报文,该探测报文包含的数据是异常小,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将该错误信息通知给上层应用程序
如果已经建立了连接,但是服务端的进程崩溃会发生什么?
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。
我自己做了个实验,使用 kill -9 来模拟进程崩溃的情况
发现在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手