在谈TIME_WAIT之前,我们先回顾一下TCP的四次挥手。来上一张熟悉的图。
1. TCP四次挥手
TCP终止连接时,主机1(主动关闭方)会先发送FIN包,主机2进入CLOSE_WAIT状态,并且回复一个ACK。此时主机2的应用程序会通过read函数获取到EOF(End Of File),主机2可以调用close或者shutdown函数发起主动关闭操作,调用close或者shutdown函数会触发tcp协议栈(内核)向主机1发送FIN包。主机1收到FIN之后,会返回一个ACK应答,之后主机1会进入一个为期2MSL的TIME_WAIT状态。主机2收到ACK后,正常关闭。
在这篇文章中,我们对四次挥手不进行详细介绍,只跟大家聊聊TIME_WAIT。如果对四次挥手想深入的了解,可以关注小编,之后的文章会给大家详细谈谈。
2. TIME_WAIT的时间长度
首先先介绍一个概念:MSL。(最长分节生命期 maximum segment lifetime)MSL指明了TCP报文在Internet上最长生存时间。
TIME_WAIT状态的时间长度定义为2MSL。在Linux系统中,有一个硬编码的字段,名称为TCP_TIMEWAIT_LEN,其值为固定的60秒。
在过了这段时间之后,主机1就会自动关闭.
只有主动关闭socket连接的一端,才会有TIME_WAIT这个状态
3. TIME_WAIT的作用以及其带来的影响
当时设计 TIME_WAIT 考虑到两个方面:
- 经过2MSL的时间,足以让两个方向上的分组都被丢弃,使得原来的分组在网络上自然消失。
想象一下这种场景,当一个TCP连接关闭时,还可能会有一些分组会留存在网络中,当新的TCP连接建立起来时,如果和前面TCP连接的四元组是一致的,这些留存在网络中的包,就可能会给新的连接带来困惑。注意,这种情况指的是新TCP连接和旧TCP连接的四元组(源地址,源端口,目的地址,目的端口)一致的情况。如果新旧TCP连接的四元组不一致,则无需考虑这个问题。
划重点了。现代Linux对上述这种情况进行了优化,加入了时间戳,新建立的TCP的时间戳总比之前建立的时间戳大。再次考虑上述那种场景,当留存在网络中的包(迷失报文)发到新建立起来的TCP的一端时,会由于时间戳小,导致接收端放弃该数据包。所以,迷失报文的问题(考虑一)也就迎刃而解。故你可以认为2MSL和时间戳是迷失报文问题的双重保证。
此外还有一个序列号的机制,也对其这个问题进行了保障。新连接的SYN ,一定比 TIME_WAIT 老连接的末序列号大,这样迷失报文到达新连接的一端时,由于序列号比当前TCP连接的初始化序列号小,而被抛弃。
- 为了使被动关闭方正常关闭。假设主机1没有维护TIME_WAIT状态,直接关闭,当主机2发送FIN包到主机1时,由于主机1已经close了,故主机1只能回复一个RST包,从而导致主机2出现错误。
可以sysctl开启对TCP时间戳的支持。Linux系统默认开启对TCP时间戳的支持。(net.ipv4.tcp_timestamps=1)
TIME_WAIT主要影响有两种:
对内存资源的占用,这个linux已经进行了优化,基本上可以忽略。说个数据,假设一个服务器有4万个连接进入TIME_WAIT状态时,大概可能会占用不到10M的内存。
对端口资源的占用。一个TCP连接至少消耗一个本地端口。端口资源也是有限的,一般可以开启的端口为32768~61000,也可以通过设置net.ipv4.ip_local_port_range指定。如果处于TIME_WAIT的TCP连接过多,会占用大量的端口资源。
4. 如何优雅的规避TIME_WAIT带来的副作用
一种暴力的解决方式:net.ipv4.tcp_max_tw_buckets
该系统值的作用是:当系统中TIME_WAIT连接一旦超过这个值时,系统就会将所有的TIME_WAIT连接状态重置,并且只打印出警告信息。我们可以使用sysctl命令,将系统值调小。
这种方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不建议使用。
一种较为优雅的解决方式:so_linger的设置
我们可以通过设置套接字来控制TCP的一些特征。通过下面函数便可以设置套接字选项。(UNIX API提供的一个方法)
int setsockopt(int sockfd, int level, int optname, const void *optval,socklen_t optlen);
so_linger是一个套接字选项。其作用是:通过设置该套接字选项,来调控调用close或者shutdown关闭连接时候的行为。
先分析一下他的数据结构。如下所示。
struct linger {
int l_onoff;//开关
int l_linger;//超时时间
}
当l_onoff设置为0时,则会关闭本套接字选项。(默认如此)
当l_onoff设置为非0时,且l_linger为0,则表明关闭连接时(调用了close或者shutdown),会立即发送一个RST给对端,TCP会直接关闭,不会经过四次挥手。(意味着跳过了TIME_WAIT)
当l_onoff设置为非0时,且l_linger为非0,则表明关闭连接时(调用了close或者shutdown),close会阻塞直至数据超时或者设置的l_linger时间超时。
综上所述,第二种情况可以跳过TIME_WAIT,但是可能会导致数据传输不完全的现象,不值得提倡。
一种更加安全的解决方式:net.ipv4.tcp_tw_reuse
开启该选项时,对应的TIME_WAIT状态的连接创建时间超过1s后才可以被复用。
有着局限性,只适用于连接发起方(c/s模型中的客户端)
使用这个选项的前提是,需要打开对TCP时间戳的支持(文章前半部分提到过),即net.ipv4.tcp_timestamps=1(默认即为1)
这种方案会在随后的文章中详细探讨,此处大概了解一下。
最佳解决方式:SO_REUSEADDR 套接字选项
这个套接字的作用是:通过设置SO_REUSEADDR 套接字,通知操作系统内核,新建的TCP连接完全可以复用TIME_WAIT状态的连接。允许启动绑定在一个端口,即使之前存在一个和该端口一 样的连接。其实SO_REUSEADDR还有一些其他的用处后续会有专门一章节进行讲解。具体设置方法如下代码所示。
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
划重点了。服务端程序都应该设置SO_REUSEADDR套接字选项,来实现服务端程序在极短的时间内复用同一个端口启动。
往期教程:
结束语:The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it.
引用至《UNIX网络编程》,翻译一哈:存在即合理,我们要勇于面对,不要逃避。