SO_REUSEADDR用于设置套接字的地址重用。当一个套接字关闭后,它的端口可能会在一段时间内处于TIME_WAIT状态,此时无法立即再次绑定相同的地址和端口。使用SO_REUSEADDR选项可以允许新的套接字立即绑定到相同的地址和端口,即使之前的套接字仍处于TIME_WAIT状态。
TIME_WAIT状态的产生
客户端和服务器都可以主动发起关闭连接,上图是客户端主动发起的TCP连接关闭。首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。在time_wait的状态下,定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。
MSL:Maximum Segment Lifetime,也就是最大报文生存时间 。
MSL到底是多长时间,可以通过下面的命令来查询,不同的操作系统时间不一样:
$ sysctl -a | grep "ipv4.tcp_fin_timeout"
sysctl: unable to open directory "/proc/sys/fs/binfmt_misc/"
net.ipv4.tcp_fin_timeout = 60
time_wait至少需要持续2MSL时长,这2个MSL中的第一个MSL是为了等自己发出去的最后一个ACK从网络中消失,而第二MSL是为了等在对端收到ACK之前的一刹那可能重传的FIN报文从网络中消失。如果time_wait时间是一个MSL,而time_wait结束后使用了相同的IP和Port建立了新的TCP连接,由于旧连接重传的FIN报文还没有在网络中消失,因此会干扰到新的TCP连接。
SO_REUSEADDR的使用
可以使用Socket类的setReuseAddress()方法来设置SO_REUSEADDR选项的值。示例代码如下:
ServerSocket serverSocket1 = new ServerSocket();
serverSocket1.setReuseAddress(false);
serverSocket1.bind(new InetSocketAddress(8099));
serverSocket1.close();
使用SO_REUSEADDR选项时有两点需要注意:
-
必须在调用bind方法之前使用setReuseAddress方法来打开SO_REUSEADDR选项。因此,要想使用SO_REUSEADDR选项,就不能通过Socket类的构造方法来绑定端口。
-
必须将绑定同一个端口的所有的Socket对象的SO_REUSEADDR选项都打开才能起作用。
SO_REUSEADDR异常的演示
Socket服务端代码如下:
package com.morris.socket;import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;/*** Socket客户端,演示SO_REUSEADDR** @see java.net.SocketOptions*/
public class ReuseAddressServerDemo {public static void main(String[] args) throws IOException {ServerSocket serverSocket1 = new ServerSocket();System.out.println(serverSocket1.getReuseAddress());// serverSocket1.setReuseAddress(false);serverSocket1.bind(new InetSocketAddress(8099));Socket socket = serverSocket1.accept();socket.getOutputStream().write("hello".getBytes(StandardCharsets.UTF_8));socket.close();}
}
Socket客户端代码如下:
package com.morris.socket;import java.io.IOException;
import java.net.Socket;/*** Socket客户端,演示SO_REUSEADDR** @see java.net.SocketOptions*/
public class ReuseAddressClientDemo {public static void main(String[] args) throws IOException {// 这里只建立连接,不发送数据,也不接受数据,也不关闭连接,这样服务端才会出现TIME_WAITnew Socket("172.24.104.61", 8099);System.in.read();}
}
当客户端与服务端建立连接后,服务端发完数据就会立即关闭,此时会进入TIME_WAIT状态:
$ netstat -anotp| grep 8099
tcp6 0 0 172.24.104.61:8099 172.24.104.61:54542 FIN_WAIT2 - timewait (57.97/0/0)
我们可以看到四元组172.24.104.61:8099,172.24.104.61:54542
代表的这个链接已经进入FIN_WAIT2
状态,而且还需要等待57.97s
才能进入CLOSE状态。
当SO_REUSEADDR
选项为true,也就是serverSocket1.setReuseAddress(true)
,也可以不进行设置,因为默认就是true,允许地址重用,此时再次启动ReuseAddressServerDemo程序,能够成功启动不会抛出端口被占用的异常。
当SO_REUSEADDR
选项为false,也就是serverSocket1.setReuseAddress(false)
,不允许地址重用,此时再次启动ReuseAddressServerDemo程序,就会抛出下面的异常:
Exception in thread "main" java.net.BindException: Address already in useat java.base/sun.nio.ch.Net.bind0(Native Method)at java.base/sun.nio.ch.Net.bind(Net.java:555)at java.base/sun.nio.ch.Net.bind(Net.java:544)at java.base/sun.nio.ch.NioSocketImpl.bind(NioSocketImpl.java:640)at java.base/java.net.ServerSocket.bind(ServerSocket.java:392)at java.base/java.net.ServerSocket.bind(ServerSocket.java:340)at com.morris.socket.ReuseAddressServerDemo.main(ReuseAddressServerDemo.java:20)
这时只有等四元组172.24.104.61:8099,172.24.104.61:54542
代表的这个链接进入CLOSE状态后才能再次启动ReuseAddressServerDemo程序。
SO_REUSEADDR的注意事项与使用场景
在使用SO_REUSEADDR选项时,需要注意以下几点:
-
SO_REUSEADDR选项必须在调用bind()函数之前设置,否则设置不会生效。
-
在使用SO_REUSEADDR选项时,需要确保不同的套接字使用相同的协议、地址和端口组合。
-
使用SO_REUSEADDR选项不能使不同的线程或进程监听相同的端口,实现端口服用。
使用场景:
-
服务器程序重启:当服务器程序需要重启时,为了避免等待操作系统回收端口的时间,可以设置SO_REUSEADDR选项。这样,重启后的服务器程序可以快速绑定到之前使用的端口上,实现快速恢复服务。
-
测试和调试:在开发和测试阶段,可能需要频繁地启动和停止服务器。使用SO_REUSEADDR可以使得测试更加方便,因为不需要担心地址和端口已经被其他进程使用的问题。
time_wait产生的危害以及如何解决
大量的time_wait产生需要的条件:
-
高并发
-
服务器主动关闭连接
如果服务器不主动关闭连接,那么TIME_WAIT就是客户端的事情了
在高并发短连接的TCP服务器上,当服务器处理完请求后立刻主动正常关闭连接。这个场景下会出现大量socket处于time_wait状态。在time_wait的状态下,定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。因此,高并发可以让服务器在短时间范围内同时占用大量端口。如果客户端的并发量持续很高,此时部分客户端就会因为端口已经被占用而显示连接不上。
如何解决大量time_wait产生的危害?
-
调整优化linux内核参数:出现大量TIME_WAIT的情况,一般是服务端没有及时回收端口,可以缩减time_wait时间。
-
服务器不主动关闭连接
-
重用端口:设置套接字选项为SO_REUSEADDR,告诉操作系统,如果端口忙,但占用该端口TCP连接处于TIME_WAIT状态,则该端口可被重用。如果TCP连接处于其他状态,依然返回端口被占用。该选项对服务程序重启非常有用。
-
使用长连接:HTTP请求的头部,connection设置为keep-alive