如果你仔细研究了 TCP 四次挥手,你会发现主动关闭一方最后的状态是 TIME_WAIT,这个 TIME_WAIT 状态是什么意思呢?
TIME_WAIT 状态,又称为 2MSL 等待状态。只有主动关闭一方才能进入 TIME_WAIT 状态。
MSL(Maximum Segment Lifetime)表示报文段最大生存时间,它表示任何报文段被丢弃前在网络内的最长时间,实际上这个时间和 TTL 有关(TTL 是 IP 协议中的一个概念,表示能够经历的路由器的跳数,这个跳数是有限制的,最大值为 255)。
然而,MSL 却不用跳数,而是时间。不同系统中,MSL 定义的大小不一样,RFC 规定,MSL = 2 分钟,而实际实现中,通常是 30 秒、1 分钟。尽管 MSL 的单位是时间而不是跳数,我们仍然假设:具有最大跳数(255)的报文在网络中存在的时间不可能超过 MSL 秒。
当 TCP 协议进入 TIME_WAIT 状态时,必须要在这个状态停留 2 倍 MSL 的时间。
可以防止连接终止的最后一个 ACK 丢失。假设最后一个 ACK 在到达对端时恰好消失,此时对端已经等待了一个 RTT(报文段往返时间),于是进行重传最后一个 FIN,经过 0.5RTT 后到达对端。
假设在 ip1:port1 和 ip2:port2 建立了一个连接 A,发送完数据后关闭连接 A。如果没有 TIME_WAIT 状态,我们又立即在 ip1:port1 和 ip2:port2 建立了一个连接 B(虽然这种事情概率很小,但是仍然存在)。
很不幸的是,连接 A 它有一个重复的 TCP 段被连接 B 收到了,然而连接 B 并不知道这个 TCP 段是连接 A 中的旧报文,这会造成错误!
如果有了 TIME_WAIT 状态,等待 2MSL,就足以让连接 A 中重复的报文在网络中消逝;另一方面,TCP 协议规定,处于 TIME_WAIT 状态的端口,是无法建立新的连接的。这样就保证了每成功建立一个新连接时,旧连接中的重复 TCP 段都已消逝。
2. TIME_WAIT 状态的影响如果 TCP 处于 TIME_WAIT 状态,会进行 2MSL 时间的等待,在这个时间内,定义此连接的本地端口不能再次使用。比如有一个连接:
(local 192.168.80.130:5050, foreign 192.168.166.107:40891) state: TIME_WAIT那么对于 192.168.80.130 这个主机来说,在 2MSL 时间内,5050 端口都不能再次被使用。
一般来说,主动关闭一方都是客户端,客户端建立连接时的端口号都是由系统自动分配的,这并没什么影响,这个端口被占用了,那就再分配一个其它端口就是了。
但是对于服务器来说,它使用的端口是熟知端口(公开出去的端口),如果它是主动关闭一方,进入了 TIME_WAIT 状态,那么 2MSL 时间内,这个服务器都无法启动。如果再次启动,会提示 Address already in use 的错误。
我们用实验来模拟服务器发起主动关闭。
3. 实验3.1 实验步骤该程序路径是 unp/protocol/tools/tcpserver/serv.c,需要你自己编译一下。
$ ./serv 192.168.80.130 6666该程序路径是 unp/protocol/tools/winclient/tcp_client.cpp,需要你自己用 VS 编译。
tcp_client.exe 192.168.80.130 6666此时在 linux 上执行 netstat -ant 命令,可以看到我们的连接处于 FIN_WAIT2 状态。
如果此时,我们再次启动服务器,就会出错:
反过来,如果客户端也想申请一个处于 2MSL 的本地端口,一样会出问题。但是一般来说客户端使用的本地端口并不是自己指定的,而是系统自动分派的,所以没什么问题。
但是对于服务器来说,如果它的熟知口处于 TIME_WAIT 状态,它将无法再次启动。实际上,现在的 Linux 提供了一种方法,它可能通过给 socket 指定选项 SO_REUSEADDR 允许端口重用。
4. 总结需要注意的是,即使服务器或客户端绑定了处于 2MSL 的端口,RFC 规定它们也不能建立连接。可惜的是,大多系统实现并未遵守这个规定:
- 如果服务器重用了处于 2MSL 端口,它仍然可以接收连接请求并连接成功(这违反了协议)
- 如果客户端重用了处于 2MSL 端口,它建立连接时仍然会失败。(没有违反协议)