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
状态。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 | struct linger { |
其取值和处理如下:
- 设置 l_onoff 为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close()调用会立即返回给调用者,如果可能将会传输任何未发送的数据;
- 设置 l_onoff 为非0,l_linger为0,当调用close()的时候,TCP连接会立即断开。send buffer中未被发送的数据将被丢弃,并向对方发送一个RST信息。值得注意的是,由于这种方式,不是以4次握手方式结束TCP连接,所以,TCP连接将不会进入TIME_WAIT状态,这样会导致新建立的可能和旧连接的数据造成混乱。这种关闭方式称为“强制”或“失效”关闭。通常会看到“Connection reset by peer”之类的错误;
- 设置 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_WAIT
或TIME_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种:
- 保证由客户端主动发起关闭
- 关闭的时候使用RST方式(set SO_LINGER)
- 对处于TIME_WAIT状态的TPC允许重用(set SO_REUSEADDR)