四次挥手详细步骤

客户端和服务端双方都可以主动断开连接,通常先关闭连接的一方称为主动方,后关闭连接的一方称为被动方。
TCP四次挥手 - 图1image.gif
可以看到,四次挥手过程只涉及了两种报文,分别是 FIN 和 ACK:

  • FIN 就是结束连接的意思,谁发出 FIN 报文,就表示它将不会再发送任何数据,关闭这一方向上的传输通道;
  • ACK 就是确认的意思,用来通知对方:你方的发送通道已经关闭;

1、 client执行close(),向server发送FIN+seq,client的tcp连接由ESTABLISHED进入FIN_WAIT_1状态,等待server返回ACK ,如果一段时间收不到,则重新发送FIN(重发次数由 tcp_orphan_retries 参数控制,默认值是0,特指8次

client—->server:我已经没有数据要发送了,你那边还有数据要发送吗?

  • 如果 FIN_WAIT1 状态连接很多,我们就需要考虑降低 tcp_orphan_retries 的值,当重传次数超过 tcp_orphan_retries 时,连接就会直接关闭掉。
  • 对于普遍正常情况时,调低 tcp_orphan_retries 就已经可以了。如果遇到恶意攻击,FIN 报文根本无法发送出去,这由 TCP 两个特性导致的:
  • 首先,TCP 必须保证报文是有序发送的,FIN 报文也不例外,当发送缓冲区还有数据没有发送时,FIN 报文也不能提前发送。
  • 其次,TCP 有流量控制功能,当接收方接收窗口为 0 时,发送方就不能再发送数据。所以,当攻击者下载大文件时,就可以通过接收窗口设为 0 ,这就会使得 FIN 报文都无法发送出去,那么连接会一直处于 FIN_WAIT1 状态。
  • 解决这种问题的方法,是调整 tcp_max_orphans 参数,它定义了「孤儿连接」的最大数量
  • 当进程调用了 close 函数关闭连接,此时连接就会是「孤儿连接」,因为它无法在发送和接收数据。Linux 系统为了防止孤儿连接过多,导致系统资源长时间被占用,就提供了 tcp_max_orphans 参数。如果孤儿连接数量大于它,新增的孤儿连接将不再走四次挥手,而是直接发送 RST 复位报文强制关闭。

2、 server收到FIN后,向client发送ACK表示确认,由ESTABLISHED进入CLOSE_WAIT状态。表示server在等待进程调用 close 函数关闭连接。

server—>client:我知道你已经没有数据要发送了,但我还要再确认一下我是不是还有数据要给你。

3、client收到server的ACK后,进入FIN_WAIT_2状态。表示client的发送通道已经关闭,接下来将等待对方发送 FIN 报文,关闭对方的发送通道。
4、当server进入 CLOSE_WAIT 时,server还会继续处理数据,等到进程的 read 函数返回 0 后,应用程序就会调用 close 函数,进而触发内核向client发送 FIN 报文,此时server的连接状态变为 LAST_ACK。

server—>client:好了,这下我也没有东西要给你了,你可以终结连接了。此时服务器不确认客户端是否收到信息,继续保持连接。

4、client收到server的FIN后,向server发送ACK,同时进入TIME_WAIT状态,并启动TIME_WAIT定时器,超时时间设为2MSL(linux下是60s)。
5、server收到ACK,执行close(), 从LAST_ACK进入CLOSED状态。如果没有收到ACK,则会重发 FIN 报文(重发次数由tcp_orphan_retries 参数控制)
6、客户端在2MSL时间内没收到对端的任何响应,TIME_WAIT超时,进入CLOSED状态。
在客户端的TIME_WAIT状态持续2MSL的时间后,如果客户端没有收到服务器发来的任何消息,则客户端会终结连接。如果最后一步客户端发给服务器的ack丢失,服务器会重发FIN给客户端,然后客户端再重发最后的ack给服务器。注意如果客户端此时不处于time_wait的话,客户端会发送RST给server,server会将此包解释为一个错误(在java中会抛出connection reset的SocketException)

client—>server: 我知道了,你也终结连接吧

客户端状态:ESTABLISHED——FIN_WAIT_1——FIN_WAIT_2——TIME_WAIT——CLOSED
服务端状态:ESTABLISHED——CLOSE_WAIT——LAST_ACK——CLOSED
TCP四次挥手 - 图3image.gif

为什么断开连接需要四次挥手,三次,五次不行吗?

发送FIN的一方就是主动关闭(客户端),而另一方则为被动关闭(服务器)。
当一方发送了FIN,则表示在这一方不再会有数据的发送。
其中当被动关闭方受到对方的FIN时,此时往往可能还有数据需要发送过去,因此无法立即发送FIN(也就是无法将FIN与ACK合并发送),
而是在等待自己的数据发送完毕后再单独发送FIN,因此整个过程需要四次交互。


  1. 那可能有人会有疑问,在tcp连接握手时为何ACK是和SYN一起发送,这里ACK却没有和FIN一起发送呢。原因是因为tcp是全双工模式,接收到FIN时意味将没有数据再发来,但是还是可以继续发送数据。<br />握手,挥手过程中各状态介绍:<br />3次握手过程状态: <br />LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。 <br />SYN_SENT: 当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。(发送端)<br />SYN_RCVD: 这个状态与SYN_SENT遥想呼应这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个 ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。(服务器端)<br />ESTABLISHED:这个容易理解了,表示连接已经建立了。<br />4次挥手过程状态:(可参考下图)<br /> <br />FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKETESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)<br />FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)<br />TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)<br />CLOSING(比较少见): 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。<br />CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)<br />LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)<br />CLOSED: 表示连接中断。<br />原文:[https://blog.csdn.net/xifeijian/article/details/12777187](https://blog.csdn.net/xifeijian/article/details/12777187)

TCP进行断开连接的目标是:回收资源、终止数据传输。由于TCP是全双工的,需要Peer两端分别各自拆除自己通向Peer对端的方向的通信信道。这样需要四次挥手来分别拆除通信信道,就比较清晰明了了。
1)Client 发送一个FIN包来告诉 Server 我已经没数据需要发给 Server了。
2)Server 收到后回复一个 ACK 确认包说我知道了。
3)然后 server 在自己也没数据发送给client后,Server 也发送一个 FIN 包给 Client 告诉 Client 我也已经没数据发给client 了。
4)Client 收到后,就会回复一个 ACK 确认包说我知道了。
到此,四次挥手,这个TCP连接就可以完全拆除了

四次挥手能不能变成三次挥手呢?

四次挥手过程中,Server端的ACK确认包能不能和接下来的FIN包合并成一个包呢,这样四次挥手不就变成三次挥手了。

答案是可能的。

TCP是全双工通信,Cliet在自己已经不会在有新的数据要发送给Server后,可以发送FIN信号告知Server,这边已经终止Client到对端Server那边的数据传输。但是,这个时候对端Server可以继续往Client这边发送数据包。于是,两端数据传输的终止在时序上是独立并且可能会相隔比较长的时间,这个时候就必须最少需要2+2 = 4 次挥手来完全终止这个连接。但是,如果Server在收到Client的FIN包后,在也没数据需要发送给Client了,那么对Client的ACK包和Server自己的FIN包就可以合并成为一个包发送过去,这样四次挥手就可以变成三次了(似乎linux协议栈就是这样实现的)

Client和Server同时发起断开连接的FIN包会怎么样呢,TCP状态是怎么转移的?

由上面的”TCP协议状态机 “图可以看出,TCP的Peer端在收到对端的FIN包前发出了FIN包,那么该Peer的状态就变成了FIN_WAIT1,Peer在FIN_WAIT1状态下收到对端Peer对自己FIN包的ACK包的话,那么Peer状态就变成FIN_WAIT2,Peer在FIN_WAIT2下收到对端Peer的FIN包,在确认已经收到了对端Peer全部的Data数据包后,就响应一个ACK给对端Peer,然后自己进入TIME_WAIT状态。
但是如果Peer在FIN_WAIT1状态下首先收到对端Peer的FIN包的话,那么该Peer在确认已经收到了对端Peer全部的Data数据包后,就响应一个ACK给对端Peer,然后自己进入CLOSEING状态,Peer在CLOSEING状态下收到自己的FIN包的ACK包的话,那么就进入TIME WAIT 状态。于是,TCP的Peer两端同时发起FIN包进行断开连接,那么两端Peer可能出现完全一样的状态转移 FIN_WAIT1——>CLOSEING——->TIME_WAIT,也就会Client和Server最后同时进入TIME_WAIT状态。
同时关闭连接的状态转移如下图所示:
TCP四次挥手 - 图5


为什么客户端要进行TIME_WAIT等一段时间才关闭连接,而不是直接关闭?

超时设置是 2*MSL,RFC793定义了MSL为2分钟,Linux设置成了30s
要说明TIME_WAIT的问题,需要解答以下几个问题:

  • Peer两端,哪一端会进入TIME_WAIT呢?为什么?相信大家都知道,TCP主动关闭连接的那一方会最后进入TIME_WAIT。那么怎么界定主动关闭方呢?是否主动关闭是由FIN包的先后决定的,就是在自己没收到对端Peer的FIN包之前自己发出了FIN包,那么自己就是主动关闭连接的那一方。对于疑症(4) 中描述的情况,那么Peer两边都是主动关闭的一方,两边都会进入TIME_WAIT。为什么是主动关闭的一方进行TIME_WAIT呢,被动关闭的进入TIME_WAIT可以不呢?我们来看看TCP四次挥手可以简单分为下面三个过程过程一.主动关闭方发送FIN;
    过程二.被动关闭方收到主动关闭方的FIN后发送该FIN的ACK,被动关闭方发送FIN;
    过程三.主动关闭方收到被动关闭方的FIN后发送该FIN的ACK,被动关闭方等待自己FIN的ACK问题就在过程三中,据TCP协议规范,不对ACK进行ACK,如果主动关闭方不进入TIME_WAIT,那么主动关闭方在发送完ACK就走了的话,如果最后发送的ACK在路由过程中丢掉了,最后没能到被动关闭方,这个时候被动关闭方没收到自己FIN的ACK就不能关闭连接,接着被动关闭方会超时重发FIN包,但是这个时候已经没有对端会给该FIN回ACK,被动关闭方就无法正常关闭连接了,所以主动关闭方需要进入TIME_WAIT以便能够重发丢掉的被动关闭方FIN的ACK。
  • TIME_WAIT状态是用来解决或避免什么问题呢?

TIME_WAIT主要是用来解决以下几个问题:
1)上面解释为什么主动关闭方需要进入TIME_WAIT状态中提到的: 主动关闭方需要进入TIME_WAIT以便能够重发丢掉的被动关闭方FIN包的ACK。如果主动关闭方不进入TIME_WAIT,那么在主动关闭方对被动关闭方FIN包的ACK丢失了的时候,被动关闭方由于没收到自己FIN的ACK,会进行重传FIN包,这个FIN包到主动关闭方后,由于这个连接已经不存在于主动关闭方了,这个时候主动关闭方无法识别这个FIN包,协议栈会认为对方疯了,都还没建立连接你给我来个FIN包?,于是回复一个RST包给被动关闭方,被动关闭方就会收到一个错误(我们见的比较多的:connect reset by peer,这里顺便说下 Broken pipe,在收到RST包的时候,还往这个连接写数据,就会收到 Broken pipe错误了),原本应该正常关闭的连接,给我来个错误,很难让人接受。2)防止已经断开的连接1中在链路中残留的FIN包终止掉新的连接2(重用了连接1的所有的5元素(源IP,目的IP,TCP,源端口,目的端口)),这个概率比较低,因为涉及到一个匹配问题,迟到的FIN分段的序列号必须落在连接2的一方的期望序列号范围之内,虽然概率低,但是确实可能发生,因为初始序列号都是随机产生的,并且这个序列号是32位的,会回绕。3)防止链路上已经关闭的连接的残余数据包(a lost duplicate packet or a wandering duplicate packet) 干扰正常的数据包,造成数据流的不正常。这个问题和2)类似。
- TIME_WAIT会带来哪些问题呢?
TIME_WAIT带来的问题注意是源于:一个连接进入TIME_WAIT状态后需要等待2*MSL(一般是1到4分钟)那么长的时间才能断开连接释放连接占用的资源,会造成以下问题
1) 作为服务器,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,占据大量的tuple,严重消耗着服务器的资源。
2) 作为客户端,短时间内大量的短连接,会大量消耗的Client机器的端口,毕竟端口只有65535个,端口被耗尽了,后续就无法在发起新的连接了。
( 由于上面两个问题,作为客户端需要连本机的一个服务的时候,首选UNIX域套接字而不是TCP )
TIME_WAIT很令人头疼,很多问题是由TIME_WAIT造成的,但是TIME_WAIT又不是多余的不能简单将TIME_WAIT去掉,那么怎么来解决或缓解TIME_WAIT问题呢?可以进行TIME_WAIT的快速回收和重用来缓解TIME_WAIT的问题。有没一些清掉TIME_WAIT的技巧呢?

  • TIME_WAIT的快速回收和重用
    • TIME_WAIT快速回收。
      linux下开启TIME_WAIT快速回收需要同时打开tcp_tw_recycle和tcp_timestamps(默认打开)两选项。Linux下快速回收的时间为3.5 * RTO(Retransmission Timeout),而一个RTO时间为200ms至120s。开启快速回收TIME_WAIT,可能会带来(问题一、)中说的三点危险,为了避免这些危险,要求同时满足以下三种情况的新连接要被拒绝掉。[1]. 来自同一个对端Peer的TCP包携带了时间戳。
      [2].之前同一台peer机器(仅仅识别IP地址,因为连接被快速释放了,没了端口信息)的某个TCP数据在MSL秒之内到过本Server
      [3].Peer机器新连接的时间戳小于peer机器上次TCP到来时的时间戳,且差值大于重放窗口戳(TCP_PAWS_WINDOW)
      初看起来正常的数据包同时满足下面3条几乎不可能, 因为机器的时间戳不可能倒流的,出现上述的3点均满足时,一定是老的重复数据包又回来了,丢弃老的SYN包是正常的。到此,似乎启用快速回收就能很大程度缓解TIME_WAIT带来的问题。但是,这里忽略了一个东西就是NAT。。。在一个NAT后面的所有Peer机器在Server看来都是一个机器,NAT后面的那么多Peer机器的系统时间戳很可能不一致,有些快,有些慢。这样,在Server关闭了与系统时间戳快的Client的连接后,在这个连接进入快速回收的时候,同一NAT后面的系统时间戳慢的Client向Server发起连接,这就很有可能同时满足上面的三种情况,造成该连接被Server拒绝掉。所以,在是否开启tcp_tw_recycle需要慎重考虑了
    • TIME_WAIT重用
      linux上比较完美的实现了TIME_WAIT重用问题。只要满足下面两点中的一点,一个TW状态的四元组(即一个socket连接)可以重新被新到来的SYN连接使用
      [1]. 新连接SYN告知的初始序列号比TIME_WAIT老连接的末序列号大
      [2]. 如果开启了tcp_timestamps,并且新到来的连接的时间戳比老连接的时间戳大
      要同时开启tcp_tw_reuse选项和tcp_timestamps 选项才可以开启TIME_WAIT重用,还有一个条件是:重用TIME_WAIT的条件是收到最后一个包后超过1s。细心的同学可能发现TIME_WAIT重用对Server端来说并没解决大量TIME_WAIT造成的资源消耗的问题,因为不管TIME_WAIT连接是否被重用,它依旧占用着系统资源。即便如此,TIME_WAIT重用还是有些用处的,它解决了整机范围拒绝接入的问题,虽然一般一个单独的Client是不可能在MSL内用同一个端口连接同一个服务的,但是如果Client做了bind端口那就是同个端口了。时间戳重用TIME_WAIT连接的机制的前提是IP地址唯一性,得出新请求发起自同一台机器,但是如果是NAT环境下就不能这样保证了,于是在NAT环境下,TIME_WAIT重用还是有风险的。
      有些同学可能会混淆tcp_tw_reuse和SO_REUSEADDR 选项,认为是相关的一个东西,其实他们是两个完全不同的东西,可以说两个半毛钱关系都没。tcp_tw_reuse是内核选项,而SO_REUSEADDR用户态的选项,使用SO_REUSEADDR是告诉内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息, 指明Address already in use”。如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时 SO_REUSEADDR 选项非常有用。但是,使用这个选项就会有(问题二、)中说的三点危险,虽然发生的概率不大。
  • 清掉TIME_WAIT的奇技怪巧
    可以用下面两种方式控制服务器的TIME_WAIT数量
    • 修改tcp_max_tw_buckets
      tcp_max_tw_buckets 控制并发的TIME_WAIT的数量,默认值是180000。如果超过默认值,内核会把多的TIME_WAIT连接清掉,然后在日志里打一个警告。官网文档说这个选项只是为了阻止一些简单的DoS攻击,平常不要人为的降低它。
    • 利用RST包从外部清掉TIME_WAIT链接
      根据TCP规范,收到任何的发送到未侦听端口、已经关闭的连接的数据包、连接处于任何非同步状态(LISTEN, SYS-SENT, SYN-RECEIVED)并且收到的包的ACK在窗口外,或者安全层不匹配,都要回执以RST响应(而收到滑动窗口外的序列号的数据包,都要丢弃这个数据包,并回复一个ACK包),内核收到RST将会产生一个错误并终止该连接。我们可以利用RST包来终止掉处于TIME_WAIT状态的连接,其实这就是所谓的RST攻击了。为了描述方便:假设Client和Server有个连接Connect1,Server主动关闭连接并进入了TIME_WAIT状态,我们来描述一下怎么从外部使得Server的处于 TIME_WAIT状态的连接Connect1提前终止掉。要实现这个RST攻击,首先我们要知道Client在Connect1中的端口port1(一般这个端口是随机的,比较难猜到,这也是RST攻击较难的一个点),利用IP_TRANSPARENT这个socket选项,它可以bind不属于本地的地址,因此可以从任意机器绑定Client地址以及端口port1,然后向Server发起一个连接,Server收到了窗口外的包于是响应一个ACK,这个ACK包会路由到Client处,这个时候99%的可能Client已经释放连接connect1了,这个时候Client收到这个ACK包,会发送一个RST包,server收到RST包然后就释放连接connect1提前终止TIME_WAIT状态了。提前终止TIME_WAIT状态是可能会带来(问题二、)中说的三点危害,具体的危害情况可以看下RFC1337。RFC1337中建议,不要用RST过早的结束TIME_WAIT状态。

Socket连接到底是个什么概念?
大家经常提socket,那么,到底什么是一个socket?其实,socket就是一个 五元组,包括:

  1. 源IP
  2. 源端口
  3. 目的IP
  4. 目的端口
  5. 类型:TCP or UDP

这个五元组,即标识了一条可用的连接。注意,有很多人把一个socket定义成四元组,也就是 源IP:源端口 + 目的IP:目的端口,这个定义是不正确的。

例如,如果你的本地出口IP是180.172.35.150,那么你的浏览器在连接某一个Web服务器,例如百度的时候,这条socket连接的四元组可能就是:
[180.172.35.150:45678, tcp, 180.97.33.108:80]
源IP为你的出口IP地址 180.172.35.150,源端口为随机端口 45678,目的IP为百度的某一个负载均衡服务器IP 180.97.33.108,端口为HTTP标准的80端口。
如果这个时候,你再开一个浏览器,访问百度,将会产生一条新的连接:
[180.172.35.150:43678, tcp, 180.97.33.108:80]
这条新的连接的源端口为一个新的随机端口 43678。

TIME_WAIT的出现,目的是为了解决网络的丢包和网络不稳定所带来的问题

第一,防止前一个连接【五元组,我们继续以 180.172.35.150:45678, tcp, 180.97.33.108:80 为例】上延迟的数据包或者丢失重传的数据包,被后面复用的连接【前一个连接关闭后,此时你再次访问百度,新的连接可能还是由180.172.35.150:45678, tcp, 180.97.33.108:80 这个五元组来表示,也就是源端口凑巧还是45678】错误的接收(异常:数据丢了,或者传输太慢了),参见下图:

  • SEQ=3的数据包丢失,重传第一次,没有得到ACK确认
  • 如果没有TIME_WAIT,或者TIME_WAIT时间非常端,那么关闭的连接【180.172.35.150:45678, tcp, 180.97.33.108:80 的状态变为了CLOSED,源端口可被再次利用】,马上被重用【对180.97.33.108:80新建的连接,复用了之前的随机端口45678】,并连续发送SEQ=1,2 的数据包
  • 此时,前面的连接上的SEQ=3的数据包再次重传,同时,seq的序号刚好也是3(这个很重要,不然,SEQ的序号对不上,就会RST掉),此时,前面一个连接上的数据被后面的一个连接错误的接收

TCP四次挥手 - 图6image.gif
第二,确保连接方能在时间范围内,关闭自己的连接。其实,也是因为丢包造成的,参见下图:

  • 主动关闭方关闭了连接,发送了FIN;
  • 被动关闭方回复ACK同时也执行关闭动作,发送FIN包;此时,被动关闭的一方进入LAST_ACK状态
  • 主动关闭的一方回去了ACK,主动关闭一方进入TIME_WAIT状态;
  • 但是最后的ACK丢失,被动关闭的一方还继续停留在LAST_ACK状态
  • 此时,如果没有TIME_WAIT的存在,或者说,停留在TIME_WAIT上的时间很短,则主动关闭的一方很快就进入了CLOSED状态,也即是说,如果此时新建一个连接,源随机端口如果被复用,在connect发送SYN包后,由于被动方仍认为这条连接【五元组】还在等待ACK,但是却收到了SYN,则被动方会回复RST
  • 造成主动创建连接的一方,由于收到了RST,则连接无法成功

TCP四次挥手 - 图8image.gif
所以,你看到了,TIME_WAIT的存在是很重要的,如果强制忽略TIME_WAIT,还是有很高的机率,造成数据粗乱,或者短暂性的连接失败。
参考:
高性能网络 | 你所不知道的TIME_WAIT和CLOSE_WAIT 作者 大房 微信公众号 FangTalk
https://mp.weixin.qq.com/s/Jt2ss3SsE_iGJp5oW8QDSw


TCP/IP协议就是这样设计的,是不可避免的。主要有两个原因:
1)可靠地实现TCP全双工连接的终止
TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN,因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。如果A端不维持TIME_WAIT状态,而是处于CLOSED 状态,那么A端将响应RST分节,B端收到后将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。
因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个分节任何一个分节的丢失情况,主动关闭连接的A端必须维持TIME_WAIT状态 。

2)允许老的重复分节在网络中消逝
TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。为了避免这个情况,TCP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消逝。


  1. 为了保证客户端发送的最后一个 ACK 报文能够到达服务端。若未成功到达,则服务端超时重传 FIN+ACK 报文段,客户端再重传 ACK,并重新计时。
  2. 防止已失效的连接请求报文段出现在本连接中。TIME-WAIT 持续 2MSL 可使本连接持续的时间内所产生的所有报文段都从网络中消失,这样可使下次连接中不会出现旧的连接报文段。

如果在处于 TIME_WAIT 状态的客户端返回 ACK 丢失,会发生什么呢?服务端由于没有接收到 ACK 号,可能会重新发送一次 FIN 。这个时候如果客户端不等待一段时间,直接关闭连接,那么通信中对应的端口号也会释放出来了,这时候如果正好有应用程序创建套接字,又恰好分配了这同一个端口号,接着服务器的 FIN 包又刚好发到。本来是要关闭之前的连接,但是由于端口号相同,这个 FIN 就开始断开这个刚刚建立的新连接。这所以需要等待一段时间,就是为了防止这种误操作。

为什么TIME_WAIT状态需要经过2MSL?

MSL是Maximum Segment Lifetime,译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将在网络中消失。

如果服务端没有收到客户端的ACK,就会超时重传第三步的FIN, 那么客户端再次接到重传的FIN,会再次发送ACK
所以等待时间应该是
客户端->服务端的 ACK消息最大存活时间(MSL) + 服务端->客户端的 FIN消息的最大存活时间(MSL)。
所以就是2MSL

image.png

关闭的连接的方式有两种

分别是 RST 报文关闭和 FIN 报文关闭。
如果进程异常退出了,内核就会发送 RST 报文来关闭,它可以不走四次挥手流程,是一个暴力关闭连接的方式。
安全关闭连接的方式必须通过四次挥手,它由进程调用 closeshutdown 函数发起 FIN 报文(shutdown 参数须传入 SHUT_WR 或者 SHUT_RDWR 才会发送 FIN)。

调用 close 函数 和 shutdown 函数有什么区别?

调用了 close 函数意味着完全断开连接,完全断开不仅指无法传输数据,而且也不能发送数据。此时,调用了 close 函数的一方的连接叫做「孤儿连接」,如果你用 netstat -p 命令,会发现连接对应的进程名为空。
使用 close 函数关闭连接是不优雅的。于是,就出现了一种优雅关闭连接的 shutdown 函数,它可以控制只关闭一个方向的连接: