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 状态,至此客户端也完成连接的关闭。
每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
为什么需要四次挥手
再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。
-
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
-
服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。
为什么 TIME_WAIT 等待的时间是 2MSL?
MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
MSL 与 TTL 的区别:MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN:
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
state, about 60 seconds */
如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。
为什么需要 TIME_WAIT 状态
主动发起关闭连接的一方,才会有 TIME-WAIT 状态。
需要 TIME-WAIT 状态,主要是两个原因:
- 防止具有相同「四元组」的「旧」数据包被收到;
- 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
原因一:防止旧连接的数据包
假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?
-
如上图黄色框框服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
-
这时有相同端口的 TCP 连接被复用后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。
所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
原因二:保证连接正确关闭
TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
假设 TIME-WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?
-
如上图红色框框客户端四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSE 状态了,那么服务端则会一直处在 LASE-ACK 状态。
-
当客户端发起建立连接的 SYN 请求报文后,服务端会发送 RST 报文给客户端,连接建立的过程就会被终止。
如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:
- 服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。
服- 务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。
所以客户端在 TIME-WAIT 状态等待 2MSL 时间后,就可以保证双方的连接都可以正常的关闭。
TIME_WAIT 过多有什么危害?
如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器方主动发起的断开请求。
过多的 TIME-WAIT 状态主要的危害有两种:
-
第一是内存资源占用;
-
是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口;
第二个危害是会造成严重的后果的,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过如下参数设置指定
net.ipv4.ip_local_port_range
如果服务端 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。
如何优化 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
如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。
net.ipv4.tcp_tw_reuse = 1
使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即
net.ipv4.tcp_timestamps=1(默认即为 1)
这个时间戳的字段是在 TCP 头部的「选项」里,用于记录 TCP 发送方的当前时间戳和从对端接收到的最新时间戳。
由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。
温馨提醒:net.ipv4.tcp_tw_reuse
要慎用,因为使用了它就必然要打开时间戳的支持 net.ipv4.tcp_timestamps
,当客户端与服务端主机时间不同步时,客户端的发送的消息会被直接拒绝掉。
方式二:net.ipv4.tcp_max_tw_buckets
这个值默认为 18000,当系统中处于 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状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。
如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP 有一个机制是保活机制。这个机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
-
tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
-
tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
-
tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。
这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。
如果开启了 TCP 保活,需要考虑以下几种情况:
第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。