TCP 四次挥手过程
客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
服务器收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。
注意点
主动关闭连接的,才有 TIME_WAIT 状态
TIME_WAIT 是「主动关闭方」断开连接时的最后一个状态,该状态会持续 2*MSL(Maximum Segment Lifetime) 时长,之后进入 CLOSED 状态。
MSL 指的是 TCP 协议中任何报文在网络上最大的生存时间,任何超过这个时间的数据都将被丢弃。
RFC 793 规定 MSL 为 2 分钟
但是在实际实现的时候会有所不同:Linux effectively hard-codes the TIME_WAIT length to about 60 seconds in the kernel source via TCP_TIMEWAIT_LEN (60*HZ), with the comment “about 60 seconds”. - 对应 implied MSL 为 30s
One very common pitfall is to look at net.ipv4.tcp_fin_timeout and think it controls TIME_WAIT. It does not. Current kernel documentation says tcp_fin_timeout is for orphaned connections in FIN_WAIT_2, and tcp(7) likewise describes it as waiting for a final FIN before forcibly closing that socket.
用处
设计 TIME_WAIT 状态的原因
防止历史连接中的数据,被后面相同四元组的连接错误的接收
为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的
保证「被动关闭连接」的一方,能被正确的关闭
如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。
假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSED 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。
为了防止这种情况出现,客户端必须等待足够长的时间,确保服务端能够收到 ACK,如果服务端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。
客户端在收到服务端重传的 FIN 报文时,TIME_WAIT 状态的等待时间,会重置回 2MSL。
Linux
Linux 的 TIME_WAIT 时长约 60s(在内核参数 TCP_TIMEWAIT_LEN 中配置)
可以使用如下命令查看系统中处于 TIME_WAIT 的连接及其剩余时间
ss -tan state time-wait -o
也可以筛选某一端口(方便检查某一特定的服务)
ss -tan state time-wait -o '( sport = :12580 or dport = :12580 )'
TIME_WAIT 导致的问题
在 Linux 操作系统下,TIME_WAIT 状态的持续时间是 60 秒,这意味着这 60 秒内,客户端一直会占用着这个端口。要知道,端口资源也是有限的,一般可以开启的端口为 32768~60999,只有 28232 个
可以通过参数 sysctl net.ipv4.ip_local_port_range 来配置这个范围
如果客户端(主动关闭连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务器发起连接了,但是被使用的本地端口,还是可以继续对另外一个远程服务器/端口发起连接的(只要求五元组唯一)。
net.ipv4.tcp_tw_reuse & net.ipv4.tcp_tw_recycle
Linux 操作系统提供了两个可以系统参数来快速回收处于 TIME_WAIT 状态的连接(这两个参数都是默认关闭的)
net.ipv4.tcp_tw_reuse:如果开启该选项的话,客户端(连接发起方) 在调用 connect() 函数时,如果内核选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于 TIME_WAIT 状态,如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒,那么就会重用这个连接,然后就可以正常使用该端口了。所以该选项只适用于连接发起方。
net.ipv4.tcp_tw_recycle:如果开启该选项的话,允许处于 TIME_WAIT 状态的连接被快速回收,该参数在 NAT 的网络下是不安全的!
要使得上面这两个参数生效,有一个前提条件,就是要打开 TCP 时间戳,即 net.ipv4.tcp_timestamps=1
默认关闭 —— 事实上,默认应该是 tcp_tw_reuse = 2 即默认对 loopback 启用,但对外部连接不生效
使用这两个参数的风险:
PAWS 是单连接内机制,TIME_WAIT 解决的是跨连接代际旧包问题
风险一:RST
过程如下:
客户端向一个还没有被服务端监听的端口发起了 HTTP 请求,接着服务端就会回 RST 报文给对方,很可惜的是 RST 报文被网络阻塞了。
由于客户端迟迟没有收到 TCP 第二次握手,于是重发了 SYN 包,与此同时服务端已经开启了服务,监听了对应的端口。于是接下来,客户端和服务端就进行了 TCP 三次握手、数据传输(HTTP应答-响应)、四次挥手。
因为客户端开启了 tcp_tw_reuse,于是快速复用 TIME_WAIT 状态的端口,又与服务端建立了一个与刚才相同的四元组的连接。
接着,前面被网络延迟 RST 报文这时抵达了客户端,而且 RST 报文的序列号在客户端的接收窗口内,由于防回绕序列号算法不会防止过期的 RST,所以 RST 报文会被客户端接受了,于是客户端的连接就断开了。
上面这个场景就是开启 tcp_tw_reuse 风险,因为快速复用 TIME_WAIT 状态的端口,导致新连接可能被回绕序列号的 RST 报文断开了,而如果不跳过 TIME_WAIT 状态,而是停留 2MSL 时长,那么这个 RST 报文就不会出现下一个新的连接。
风险二:第四次挥手的 ACK 报文丢失
开启 tcp_tw_reuse 来快速复用 TIME_WAIT 状态的连接,如果第四次挥手的 ACK 报文丢失了,服务端会触发超时重传,重传第三次挥手报文,处于 syn_sent 状态的客户端收到服务端重传第三次挥手报文,则会回 RST 给服务端。
如果 TIME_WAIT 状态被快速复用后,刚好第四次挥手的 ACK 报文丢失了,此时客户端复用了 TIME_WAIT 状态的端口并发送 SYN 报文,此时对端认为目前正处于 LAST_ACK 状态,此时会回复确认号与服务端上一次发送 ACK 报文一样的 ACK 报文(Challenge ACK),而不是确认收到 SYN 报文,此时处于 SYN_SENT 的客户端收到 ACK 发现不是自己期望收到的确认号,于是就会回复 RST 报文,服务端收到后,就会断开连接。
进程退出并不会影响 TIME_WAIT
首先,进程退出时,内核会关闭这个进程持有的所有 open file descriptors。 对 socket 也是一样;如果这已经是指向该 socket 的最后一个文件描述符,那么底层对象才进入真正的释放/关闭流程。(反过来,如果这个 socket 还被别的进程或别的 fd 引用着(例如 fork/dup/传给别的进程),那它不会因为当前进程退出就消失。)
对 ESTABLISHED / FIN_WAIT / LAST_ACK 这类 TCP 连接,进程退出后通常只是“应用不再持有它”,内核仍会继续维护这个连接的后续 TCP 关闭过程。Linux 内核文档明确把这类情况称为 orphan socket:应用调用 close 之后,应用已经和 socket 没关系了,但内核还要继续等对端回复,并且它最终可能进入 TIME_WAIT。
已经存在的TIME_WAIT 也不会因为应用退出就立刻被清掉。它本来就是内核为了完成 TCP 关闭语义而保留的状态,不再“属于”某个活着的进程;即使应用已经没了,你仍然可能在 ss / netstat 里看到它。Linux 还专门有 tcp_max_tw_buckets 来限制系统里 TIME_WAIT socket 的总数。
对 监听 socket (LISTEN),如果退出的这个进程持有的是最后一个引用,那监听会跟着消失;但如果这个监听 fd 还被别的进程继承或复制着,它就可能继续存在。
另外
SO_LINGER 会影响关闭行为,但 socket(7) 说明:如果 socket 是作为 exit(2) 的一部分被关闭,它总是在后台 linger,不会让进程退出流程一直阻塞在 close() 上。
orphaned FIN_WAIT2 socket 的寿命还会受 TCP_LINGER2 / /proc/sys/net/ipv4/tcp_fin_timeout 影响;tcp(7) 里写明默认 tcp_fin_timeout 是 60 秒,而 TCP_LINGER2 可以按 socket 覆盖它。
清理处于 TIME_WAIT 的连接
几乎没有办法 —— 除非采用通用的关闭连接的方法(诱导/伪造 RST,参考 如何关闭一个 TCP 连接? )
总结
tcp_tw_reuse 的作用是让客户端快速复用处于 TIME_WAIT 状态的端口,相当于跳过了 TIME_WAIT 状态,这可能会出现这样的两个问题:
历史 RST 报文可能会终止后面相同四元组的连接,因为 PAWS 检查到即使 RST 是过期的,也不会丢弃。
如果第四次挥手的 ACK 报文丢失了,有可能被动关闭连接的一方不能被正常的关闭;
虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。
《UNIX网络编程》一书中却说道:TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。