1. TIME_WAIT状态
主动关闭方在收到被动关闭方的FIN
包后并返回ACK
后,会进入TIME_WAIT
状态,TIME_WAIT
状态又称2MSL
状态,每个TCP连接都必须有一个最大报文段生存时间MSL,在网络传输中超过这个时间的报文段将被丢弃。当TCP连接发起一个主动关闭,并发出最后一个ACK时,必须在TIME_WAIT
状态停留两倍MSL时间,在2MSL
等待期间,定义这个连接的插口(客户端IP地址和端口号,服务器IP地址和端口号的四元组)将不能再被使用。2MSL
状态存在有两个理由:
- 1.允许老的重复报文分组在网络中消逝。
- 2.保证TCP全双工连接的正确关闭。
第一个理由是假如我们在192.168.1.1:5000
和39.106.170.184:6000
建立一个TCP连接,一段时间后我们关闭这个连接,再基于相同插口建立一个新的TCP连接,这个新的连接称为前一个连接的化身。老的报文很有可能由于某些原因迟到了,那么新的TCP连接很有可能会将这个迟到的报文认为是新的连接的报文,而导致数据错乱。为了防止这种情况的发生TCP连接必须让TIME_WAIT状态持续2MSL
,在此期间将不能基于这个插口建立新的化身,让它有足够的时间使迟到的报文段被丢弃。
第二个理由是因为如果主动关闭方最终的ACK
丢失,那么服务器将会重新发送那个FIN
,以允许主动关闭方重新发送那个ACK
。要是主动关闭方不维护2MSL
状态,那么主动关闭将会不得不响应一个RST
报文段,而服务器将会把它解释为一个错误,导致TCP连接没有办法完成全双工的关闭,而进入半关闭状态。
1.1 为什么是维持2MSL
- 一个
MSL
是确保主动关闭方最后的ACK
能够到达对端。 - 一个
MSL
是确保被动关闭方重发的FIN
能够被主动关闭方收到。
2.处理TIME_WAIT状态
一个web服务器最大的端口数量是65535个,如果这个服务器作为客户端不停的和服务端不停的创建短连接,就会导致有大量的TCP进入TIME_WAIT
状态。
当服务端是主动关闭方,因为有TIME_WAIT
状态的存在,在重启程序的时候可能会出现java.net.BindException: Address already in use
的错误,这是因为这个端口TIME_WAIT
状态需要等待2MSL
。在RFC793
中规定MSL
的时间为2min,在实际使用中一般是30s或者1min,在高并发的情况下毫无疑问,这将造成大量连接无法建立的问题,那么有什么方法可以处理这些问题那?
2.1 SO_REUSEADDR
SO_REUSEADDR
设置为1在TIME_WAIT
时允许套接字端口复用。SO_REUSEADDR
设置为0TIME_WAIT
时不允许允许套接字端口复用。
在Java中可以通过以下代码设置:
ServerSocket serverSocket = new ServerSocket();
// setReuseAddress 必须在 bind 函数调用之前执行
serverSocket.setReuseAddress(true);
SO_REUSEADDR
可以用在以下四种情况下。(摘自《Unix网络编程》卷一,即UNPv1)
- 当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
SO_REUSEADDR
允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可 以测试这种情况。SO_REUSEADDR
允许单个进程绑定相同的端口到多个socket
上,但每个socket
绑定的ip地址不同。这和2很相似,区别请看UNPv1
。SO_REUSEADDR
允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。
所以SO_REUSEADDR
并不是在所有情况下都可以使用的。
2.2 TCP头部时间戳选项(TCP Timestamps Option)
时间戳可选项主要包含4个部分:
- kind:类型
- length:长度
- TimeStamp value:发送方时间戳
- TimeStamp echo reply:回显时间戳
时间戳可选项可以处理序号回绕,判断乱序,更加准确的计算RTT
,在linux中可以通过net.ipv4.tcp_timestamps
设置,一般默认是1
(打开)。
发送方在发送数据时将发送数据的时间X
放到发送方时间戳TSval
中。
接收方在接收到数据的时候将收到的时间X
原封不动的放到回显时间戳TSecr
中,同时将自己发送数据的时间Z
放到发送方时间戳TSval
中,以此类推。
2.3 net.ipv4.tcp_tw_reuse
在linux中tcp_tw_reuse
默认为0,设置为1
后表示客户端允许TIME_WAIT
状态重用。重用的条件是:
tcp_tw_reuse
选项和tcp_timestamps
也必须同时打开;- 开启后收到最后一个包后超过1s。
当开启tcp_tw_reuse
后,主动关闭方收到包的时间戳比存储的时间戳小,则证明收到的包是一个旧的连接的包,直接丢弃。
2.4 net.ipv4.tcp_tw_recycel
在linux中tcp_tw_recycel
默认为0,设置为1后开启(tcp_timestamps
必须同时打开),开启tcp_tw_recycel
后服务器将会缓存每个套接字的最新时间戳。对于新的连接,如果SYN
包中的时间戳小于之前缓存的套接字的时间戳,则直接丢弃,否则允许复用TIME_WAIT
。
这种机制在经过NAT
或者负载均衡后,会发生严重的问题。因为不同请求经过NAT或者负载均衡后IP可能是一样的,就会导致TIME_WAIT
无法复用。所以不推荐打开这个参数。