TIME_WAIT状态是TCP连接中主动关闭连接的一方会进入的状态,在发出最后一个ACK包之后,主动关闭方进入TIME_WAIT状态,从而确保:
- ACK包到达对端
- 等待网络中之前迷路的数据包完全消失,防止端口被复用的时候收到迷路包从而出现收包错误
TIME_WAIT状态会持续2MSL(max segment lifetime)的时间,一般1分钟到4分钟。在这段时间内端口不能被重新分配使用。
TIME_WAIT并不会占用过多的系统资源,但是可以通过修改内核参数/etc/sysctl.conf
来限制TIME_WAIT数量。
四次挥手过程
先来了解TCP四次挥手的过程:
第一次:主机 1(可以是客户端,也可以是服务器端),设置 Sequence Number 和 Acknowledgment Number,向主机 2 发送一个 FIN 报文段;此时,主机1进入 FIN_WAIT_1 状态;这表示主机 1 没有数据要发送给主机 2 了;
第二次:主机 2 收到了主机 1 发送的 FIN 报文段,向主机 1 回一个 ACK 报文段,Acknowledgment Number 为 Sequence Number 加 1 ;主机 1 进入 FIN_WAIT_2 状态;主机 2 告诉主机 1,我“同意”你的关闭请求;
第三次:主机 2 向主机 1 发送FIN报文段,请求关闭连接,同时主机 2 进入LAST_ACK 状态;
第四次:主机 1 收到主机 2 发送的 FIN报文段,向主机2发送 ACK 报文段,然后主机1进入 TIME_WAIT 状态;主机 2 收到主机 1 的ACK报文段以后,就关闭连接;此时,主机1等待 2 * MSL 后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
注: MSL是指Max Segment Lifetime
,即一个IP数据包能在网络中生存的最长时间,超过这个时间,IP数据包将在网络中消失。每种TCP协议的实现方法均要指定一个合适的MSL值,如RFC1122给出的建议值为2分钟,又如Berkeley体系的TCP实现通常选择30秒作为MSL值。这意味着TIME_WAIT的典型持续时间为1-4
分钟。
TCP四次挥手过程中通信双方状态解析
FIN_WAIT_1
其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:- FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)
FIN_WAIT_2
实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)CLOSE_WAIT
表示在等待关闭。当对方close一个SOCKET后发送FIN报文给你,你自然会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)LAST_ACK
被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED状态了。(被动方)TIME_WAIT
表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED状态了。如果FIN WAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)CLOSED
表示SOCKET连接已中断
为什么会有TIME_WAIT状态
可靠地实现TCP全双工连接的可靠终止
TCP协议在关闭连接的四次握手过程中,最终ACK是由主动关闭连接的一端发出的,如果这个ACK丢失,被动方将重发最终的FIN,因此主机1就必须维护状态信息TIME_WAIT 允许它发送最终的ACK。如果主机1不维持TIME_WAIT的状态,而是处于CLOSED状态,那么主机1将响应RST(reset)数据包,主机2收到后将此数据报解释成一个异常(Java中会抛出connection reset
的SocketException)。因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个数据包任何一个分节丢失的情况,主动关闭连接的主机1必须维持TIME_WAIT的状态。保证此次连接的重复数据段从网络中消失
TCP数据包可能由于路由器异常而“迷路”,在“迷路”期间,TCP发送端可能因确认超时而重发这个分节,“迷路”的分节在路由器恢复正常后也会被发送到最终的目的地,这个迟到的“迷路”数据包到达时可能会引起问题。在关闭“前一个连接”之后,马上又建立起一个相同的IP和端口之间的“新连接”,这会导致“前一个连接”的迷路重复分组在“前一个连接”终止后到达,从而被“新连接”接收到了。为了避免以上情况,TCP/IP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,这就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消失。
出现太多TIME_WAIT危害
在高并发短连接的TCP服务器上,当服务器处理完请求后会立刻按照主动正常关闭连接。这个场景下,会出现大量socket处于TIMEWAIT状态。如果客户端的并发量持续很高,此时部分客户端就会显示连接不上。
解释下这个场景。主动正常关闭TCP连接,都会出现TIMEWAIT。为什么我们要关注这个高并发短连接呢?有两个方面需要注意:
① 高并发可以让服务器在短时间范围内同时占用大量端口,而端口有个0~65535的范围,并不是很多,刨除系统和其他服务要用的,剩下的就更少了。
② 在这个场景中,短连接表示“业务处理+传输数据的时间 远远小于 TIMEWAIT超时的时间”的连接。 这里有个相对长短的概念,比如,取一个web页面,1秒钟的http短连接处理完业务,在关闭连接之后,这个业务用过的端口会停留在TIMEWAIT状态几分钟,而这几分钟,其他HTTP请求来临的时候是无法占用此端口的。单用这个业务计算服务器的利用率会发现,服务器干正经事的时间和端口(资源)被挂着无法被使用的时间的比例是
1 :几百
,服务器资源严重浪费。
说个题外话,从这个意义出发来考虑服务器性能调优的话,长连接业务的服务就不需要考虑TIMEWAIT状态。同时,假如你对服务器业务场景非常熟悉,你会发现,在实际业务场景中,一般长连接对应的业务的并发量并不会很高.
综合这两个方面,持续的到达一定量的高并发短连接,会使服务器因端口资源不足而拒绝为一部分客户服务。
TIME_WAIT太多怎么解决
修改/etc/sysctl.conf
:
1 | 表示开启重用 |
保存后sysctl -p
生效
地址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选项,可以通过set sockopt 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)