前面介绍到四次挥手的时候有讲到,主动断开连接的那一端需要等待 2 个 MSL 才能最终释放这个连接。一般而言,主动断开连接的都是客户端,如果是服务端程序重启或者出现 bug 崩溃,这时服务端会主动断开连接,如下图所示
因为要等待 2 个 MSL 才能最终释放连接,所以如果这个时候程序马上启动,就会出现Address already in use错误。要过 1 分钟以后才可以启动成功。如果你写了一个 web 服务器,崩溃以后被脚本自动拉起失败,需要等一分钟才正常,运维可能要骂娘了。
下面来写一段简单的代码演示这个场景是如何产生的。
public class ReuseAddress {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
// setReuseAddress 必须在 bind 函数调用之前执行
serverSocket.setReuseAddress(false);
serverSocket.bind(new InetSocketAddress(8080));
System.out.println("reuse address: " + serverSocket.getReuseAddress());
while (true) {
Socket socket = serverSocket.accept();
System.out.println("incoming socket..");
OutputStream out = socket.getOutputStream();
out.write("Hello\n".getBytes());
out.close();
}
}
}
这段代码的功能是启动一个 TCP 服务器,客户端连上来就返回了一个 "Hello\n" 回去。
使用 javac 编译 class 文件javac ReuseAddress.java;,然后用 java 命令运行java -cp . ReuseAddress。使用 nc 命令连接 8080 端口nc localhost 8080,应该会马上收到服务端返回的"Hello\n"字符串。现在 kill 这个进程,马上重启这个程序就可以看到程序启动失败,报 socket bind 失败,堆栈如下:
Exception in thread "main" java.net.BindException: 地址已在使用 (Bind failed)
at java.net.PlainSocketImpl.socketBind(Native Method)
at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
at java.net.ServerSocket.bind(ServerSocket.java:375)
at java.net.ServerSocket.bind(ServerSocket.java:329)
at ReuseAddress.main(ReuseAddress.java:18)
将代码修改为serverSocket.setReuseAddress(false);,再次重复上面的测试过程,再也不会出现上述异常了。
0x02 为什么需要 SO_REUSEADDR 参数
服务端主动断开连接以后,需要等 2 个 MSL 以后才最终释放这个连接,重启以后要绑定同一个端口,默认情况下,操作系统的实现都会阻止新的监听套接字绑定到这个端口上。
我们都知道 TCP 连接由四元组唯一确定。形式如下
{local-ip-address:local-port , foreign-ip-address:foreign-port}
一个典型的例子如下图