TCP/IP中TIME_WAIT状态详解

TCP的TIME_WAIT状态是什么?谁会有TIME_WAIT状态?TIME_WAIT状态的作用是什么?如何避免大量的TIME_WAIT状态占用过多的系统资源?

TIME_WAIT状态是TCP连接中主动关闭连接的一方会进入的状态,在发出最后一个ACK包之后,主动关闭方进入TIME_WAIT状态,从而确保ACK包到达对端,以及等待网络中之前迷路的数据包完全消失,防止端口被复用的时候收到迷路包从而出现收包错误。

TIME_WAIT状态会持续2MSL(max segment lifetime)的时间,一般1分钟到4分钟。在这段时间内端口不能被重新分配使用。

TIME_WAIT并不会占用过多的系统资源,但是可以通过修改内核参数/etc/sysctl.conf来限制TIME_WAIT数量。

time_wait

TIME_WAIT状态原理

在通信双方建立连接之后,主动关闭连接的一方会进入TIME_WAIT状态。Client端主动关闭连接时,会发送最后一个ACK,然后进入TIME_WAIT状态,再停留2个MSL时间,进入CLOSED状态。

上图以Client端主动关闭连接为例,说明这个过程。

TIME_WAIT状态存在理由

TCP/IP协议这样设计,主要有两个原因:

可靠地实现TCP全双工连接的可靠终止

TCP协议在关闭连接的四次握手过程中,最终ACK是由主动关闭连接的一端(以下简称A端)发出的,如果这个ACK丢失,对方(以下简称B端)将重发最终的FIN,因此A端就必须维护状态信息TIME_WAIT允许它发送最终的ACK。如果A端不维持TIME_WAIT的状态,而是处于CLOSED状态,那么A端将响应RST(reset)分节,B端收到后将此分节解释成一个异常(Java中会抛出connection reset的SocketException)。因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个分节中,任何一个分节丢失的情况,主动关闭连接的A端必须维持TIME_WAIT的状态。

保证此次连接的重复数据段从网络中消失

TCP分节可能由于路由器异常而“迷路”,在“迷路”期间,TCP发送端可能因确认超时而重发这个分节,“迷路”的分节在路由器恢复正常后也会被发送到最终的目的地,这个迟到的“迷路”分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又建立起一个相同的IP和端口之间的“新连接”,这会导致“前一个连接”的迷路重复分组在“前一个连接”终止后到达,从而被“新连接”接收到了。

为了避免以上情况,TCP/IP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,这就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消失。

MSL时间

MSL就是max segment lifetime,这是一个IP数据包能在网络中生存的最长时间,超过这个时间,IP数据包将在网络中消失。MSL在RFC 793中建议是2分钟,而源自Berkeley的TCP实现传统上使用30秒。

地址reuse问题

在写一个unix server程序时,经常需要在命令行重启它,绝大多数时候工作正常,但是某些时候会抛出异常“bind: address already in use”,于是重启失败。

上面这个就是地址reuse问题,就是由于TIME_WAIT状态产生的,我们有以下方案来解决这个问题:

SO_REUSEADDR

这个socket选项通知内核:如果端口忙,但TCP状态位于TIME_WAIT,可以重用端口。

一个socket由相关五元组构成,协议、本地地址、本地端口、远程地址、远程端口。SO_REUSEADDR仅仅表示可以重用本地本地地址、本地端口,整个相关五元组还是唯一确定的。所以,重启后的服务程序有可能收到非期望数据。必须慎重使用SO_REUSEADDR选项。

一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。

SO_REUSEADDR用于对TCP处于TIME_WAIT状态下的socket,才可以重复绑定使用。server程序总是应该在调用bind()之前设置SO_REUSEADDR选项。先调用close()的一方会进入TIME_WAIT状态。

SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。

SO_LINGER

Linux网络编程中,socket的选项很多。其中几个比较重要的选项就包括SO_LINGER。

在默认情况下,当调用close()关闭socket的使用,close()会立即返回,但是,如果send buffer中还有数据,系统会试着先把send buffer中的数据发送出去,SO_LINGER选项则是用来修改这种默认操作的。SO_LINGER是一个socket选项,可以通过setsockopt API进行设置,使用起来比较简单,但其实现机制比较复杂,且字面意思上比较难理解。SO_LINGER的值用如下数据结构表示:

1
2
3
4
struct linger {
int l_onoff //0 = off, nonzero = on(开关)
int l_linger //linger time(延迟时间)
}

其取值和处理如下:

  1. 设置 l_onoff 为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close()调用会立即返回给调用者,如果可能将会传输任何未发送的数据;
  2. 设置 l_onoff 为非0,l_linger为0,当调用close()的时候,TCP连接会立即断开。send buffer中未被发送的数据将被丢弃,并向对方发送一个RST信息。值得注意的是,由于这种方式,不是以4次握手方式结束TCP连接,所以,TCP连接将不会进入TIME_WAIT状态,这样会导致新建立的可能和旧连接的数据造成混乱。这种关闭方式称为“强制”或“失效”关闭。通常会看到“Connection reset by peer”之类的错误;
  3. 设置 l_onoff 为非0,l_linger为非0,在这种情况下,会使得close()返回得到延迟。调用close()去关闭socket的时候,内核将会延迟。也就是说,如果send buffer中还有数据尚未发送,该进程将会被休眠直到一下任何一种情况发生:

a. send buffer中的所有数据都被发送并且得到对方TCP的应答消息;

b.延迟时间消耗完。在延迟时间被消耗完之后,send buffer中的所有数据都将会被丢弃。这种关闭称为“优雅的”关闭。

因此,在正常情况下,在socket调用close()之前设置SO_LINGER超时为0都不是个好的选择。但也有些情况下需要使用SO_LINGER:

  • 如果server返回无效数据或者超时时,SO_LINGER有助于避免卡在CLOSE_WAITTIME_WAIT的状态;
  • 如果必须启动有数千个客户端连接的app,则可以考虑设置SO_LINGER,从而避免数千个socket处于TIME_WAIT状态,从而减少可用端口在服务重启后,新客户端连接收到的影响;

总结

通过上面的讨论,我们知道TIME_WAIT状态是友好的,并不是多余的,TCP要保证在所有可能的情况下使得所有的数据都能够正确送达。当你关闭一个socket时,主动关闭一端的socket将进入TIME_WAIT状态,而被动关闭的一方则进入CLOSED状态,这的确能够保证所有的数据都被传送。

当一个socket关闭的时候,是通过两端四次挥手完成的,当一端调用close()时,就说明本端没有数据要传送了,这好像看来在挥手完成以后,socket就可以处于CLOSED状态了,其实不然,原因是这样安排状态有两个问题,首先我们没有任何机制保证最后的一个ACK能够正常传输,第二,网络仍然可能有残余的数据包,我们也必须能够正常处理。TIME_WAIT状态就是为了解决这两个问题而生的。

服务端为了解决这个TIME_WAIT问题,可选的方式有3种:

  1. 保证由客户端主动发起关闭
  2. 关闭的时候使用RST方式(set SO_LINGER)
  3. 对处于TIME_WAIT状态的TPC允许重用(set SO_REUSEADDR)