14-TIME_WAIT状态
61.6.7 TIME_WAIT状态
TCP协议中的TIME_WAIT状态在网络编程中常常会引起理解上的混乱。参考图61-4,我们可以看到TCP在这个状态下正在执行一个主动关闭。TIME_WAIT状态的存在主要基于两个目的。
- 实现可靠的连接终止。
- 让老的重复的报文段在网络中过期失效,这样在建立新的连接时将不再接收它们。
TIME_WAIT状态区别于其他状态的地方在于:导致从该状态迁移到其他状态(到CLOSED状态)的事件是超时。这个超时时间为2倍的MSL(2MSL),这里的MSL(报文最大生存时间)是TCP报文在网络中的最大生存时间。
IP首部中有一个8位的生存时间字段(TTL),如果在报文从源主机到目的主机间传递时,在规定的跳数(经过的路由器)内报文没有到达目的地,那么该字段用来确保所有的IP报文最终都会被丢弃。MSL是IP报文在超过TTL限制前可在网络中生存的最大估计时间。由于TTL只有8位,因此允许最大跳数为255跳。通常,IP报文在完成整个转发过程中需要的跳数比这个最大值要小很多。当路由器出现几种特定类型的异常(例如,路由器配置问题)导致报文在网络中循环直到超过了TTL限制,此时IP报文就会遇到这个限制。
BSD的套接字实现假设MSL为30秒,而Linux遵循了BSD规范。因而,Linux上的TIME_WAIT状态将持续60秒。但是,RFC 1122建议MSL的值为2分钟,因此在遵循了这个建议的实现中,TIME_WAIT状态将持续4分钟。
通过观察图61-6,现在我们可以理解TIME_WAIT状态的第一个目的了——确保能可靠地终止连接。在这个图中,我们可以看到在终止TCP连接时有4个报文需要交换。其中最后一个ACK报文是从执行主动关闭的一方发往执行被动关闭的一方。现在假设这个ACK在网络中被丢弃了,如果发生了这种情况,那么执行TCP被动关闭的一方最终会重传它的FIN报文。让执行TCP主动关闭的一方保持在TIME_WAIT状态一段时间,可以确保它在这种情况下可以重新发送最后的ACK确认报文。如果执行主动关闭的一方已经不存在了,那么——由于它不再持有关于连接的任何状态信息——TCP协议将针对对端重发的FIN发送一个RST(重置)给执行被动关闭的一方以作为响应。而这个RST会被解释为一个错误。(这就解释了为何TIME_WAIT状态的持续时间为2倍的MSL:1个MSL时间留给最后的 ACK 确认报文到达对端 TCP 结点,另一个 MSL 时间留给必须发送的FIN报文。)
执行被动关闭的一方并不需要一个功能上相当于TIME_WAIT的状态。因为在连接终止时,被动关闭的一方是作为发起者开始进行最后的报文交换。在发送了FIN报文后,这个TCP结点将等待对端发来的ACK确认,如果在ACK到达之前超时了就重传FIN。
要理解TIME_WAIT状态的第二个目的——确保老的重复的报文在网络中过期失效——我们必须记住TCP协议采用的重传算法意味着可能会生成重复的报文,并且根据路由的选择,这些重复的报文可能会在连接已经终止后才到达。假设我们在两个套接字地址之间有一条TCP连接,比如说204.152.189.116端口21(FTP服务的端口),以及200.0.0.1端口50000。同时假设这条连接已经关闭了,而之后使用同样的IP和端口重新建立新的连接。这可以看做是原来连接的新化身。在这种情况下,TCP必须确保上一次连接中老的重复报文不会在新的连接中被当成合法数据接收。当有TCP结点处于 TIME_WAIT 状态时是无法通过该结点创建新的连接的,这样就阻止了新连接的建立。
在网络论坛中常会看到的一个问题是如何关闭TIME_WAIT状态,因为当重新启动的服务器进程尝试将套接字绑定到处于 TIME_WAIT 状态的地址上时,会导致出现EADDRINUSE的错误(“地址已使用”)。尽管的确有办法可以关闭TIME_WAIT状态(参见[Stevens et al., 2004]),并且也有办法可以让 TCP 结点从TIME_WAIT状态中过早地终止(参见[Snader, 2000]),但还是应该避免这么做。因为这么做会阻碍TIME_WAIT状态所提供的可靠性保证。在61.10节中,我们会看到SO_REUSEADDR套接字选项,这个选项可用来避免常常会遇到的EADDRINUSE错误,同时仍然允许TIME_WAIT状态提供其可靠性保证。