浅谈 Java Socket 构造函数参数 backlog

ServerSocket API

API:java.net.ServerSocket 1.0

  • ServerSocket(int port, int backlog)
    创建一个监听端口的服务器套接字
  • ServerSocket() 1.4
    创建一个未绑定的服务器套接字
  • void bind(SocketAddress endpoint, int backlog) 1.4
    把服务器套接字绑定到指定套接字地址上

注意
bind 方法常常在调用无参构造函数 ServerSocket() 之后使用。如果 bind 和其他有参构造函数一起使用,会产生报错。以下错误示范:

ServerSocket serverSocket = new ServerSocket(8081, 2); // 使用有参构造函数,创建一个监听端口的服务器套接字
serverSocket.bind(new InetSocketAddress(8081), 1); // 这里再调用bind方法,重复绑定,产生异常!

如下图所示:
SocketException-already bound

backlog 参数

backlog 参数是套接字上请求的最大挂起连接数。它的确切语义是特定于实现的。
backlog 是请求的 incoming 连接队列的最大长度。

创建 ServerSocket 并绑定端口:

Socket 服务端代码

public class SocketServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(8080), 1);
        System.in.read(); // 不让服务端关闭
    }
}

这里使用 telnet 127.0.0.1 8080 打开两个 Windows Telnet 客户端,根据 WireShark 的抓包结果如下:

backlog

  • 端口号为 5608 的第一个 Telnet 客户端,经过三次握手,顺利地和服务器建立的连接并保持了连接
  • 端口号为 5620 的第二个 Telnet 客户端,首先发送了第一次握手报文(SYN),但是服务器因为设置了 backlog 为 2,因此直接给客户端返回 (RST) 报文。客户端尝试重传 (报文内容和第一次握手时的报文一模一样),尝试2次后收到的仍然是RST 报文,就不了了之。

如果改用 Java 客户端,代码如下:

public class Clients {

    public static void main(String[] args) throws IOException {
        Socket[] clients = new Socket[2];
        for (int i = 1; i <= clients.length; i++) {
            clients[i-1] = new Socket("127.0.0.1", 8080);
            System.out.println("client connection:" + i);
        }
    }
}

控制台发生报错:
Connection refused

  • 第一个客户端 Socket 创建成功,但是第二个客户端的 Socket 被拒绝连接。

因此,在这种情况下,能够成功创建客户端套接字的个数,刚好就是创建 ServerSocket 时候指定的 backlog 的数量。

用 accept 返回 Socket 对象

API:java.net.ServerSocket 1.0

  • Socket accept()
    等待连接。该方法阻塞(即使之空闲)当前线程直到建立连接为知。该方法返回一个 Socket 对象,程序可以通过这个对象与连接中的客户端进行通信。

我们改造一下 ServerSocket,在 while 循环调用 ServerSocket#accept 方法。

public class SocketServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(8080), 1);
        int acceptCount = 0;
        while (true) {
            Socket clientSocket = serverSocket.accept();
            InetSocketAddress remote = (InetSocketAddress) clientSocket.getRemoteSocketAddress();
            System.out.println(remote.getPort());
            ++acceptCount;
            System.out.println("当前客户端连接数:" + acceptCount);
        }
    }
}

客户端我们也改一下,变成并发量 100 的连接请求

public class Clients {

    public static void main(String[] args) throws IOException {
        Socket[] clients = new Socket[100];

        for (int i = 1; i <= clients.length; i++) {
            final int index = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        clients[index-1] = new Socket("127.0.0.1", 8080);
                        System.out.println("client connection:" + index);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }, String.valueOf(i)).start();
        }
        System.in.read();
    }
}

经过实验,backlog=1 时, 一次运行结果如下:

多次执行,拒绝连接的数量存在波动。

给服务端加上阻塞

上一个实验中,我们使用 accept 来返回 Socket 对象。我们把套接字从 sync_queue 转移到 accept_queue,这样就可以接收更多的连接了。

但是,如果我们用 sleep 来模拟接收到连接后的收发消息,业务处理的延迟,实验结果又会不同。

带延迟的 SocketServer

public class SocketServer {

    public static void main(String[] args) throws IOException, InterruptedException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(8080), 1);
        int acceptCount = 0;
        while (true) {
            Socket clientSocket = serverSocket.accept();
            InetSocketAddress remote = (InetSocketAddress) clientSocket.getRemoteSocketAddress();
            System.out.println(remote.getPort());
            ++acceptCount;
            System.out.println("当前客户端连接数:" + acceptCount);
            Thread.sleep(2000); // 加入延迟时间
        }
    }
}

同步客户端

public class SyncClients {

    private static Socket[] clients = new Socket[100];
    public static void main(String[] args) throws Exception {
        for (int i = 1; i <= clients.length; i++) {
            clients[i-1] = new Socket("127.0.0.1", 8080);
            System.out.println("client connection:" + i);
        }
    }
}

同步连接客户端,套接字是一个接着一个连接的。先完成一组三次握手,再进行第二组三次握手,以此类推。
第一次连接从 sync_queue 转移到 accept_queue,
第二次连接进入到 sync_quque,
第三次连接因为 backlog=1 的缘故,被拒绝连接了,客户端抛出异常。
结果如图所示:

异步客户端

public class Clients {
    static Socket[] clients = new Socket[3];
    public static void main(String[] args) throws IOException {

        for (int i = 0; i < clients.length; i++) {
            final int index = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        clients[index] = new Socket("127.0.0.1", 8080);
                        System.out.println("client connection:" + index);
                        Thread.sleep(10000);
                    } catch (IOException | InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, String.valueOf(i)).start();
        }
        System.in.read();
    }
}

测试结果并没有像我预料的那样,第三次连接失败

这里我先留个坑,跟网上查询的 sync_queue 理论有些不符合的样子。如果有熟悉底层的大佬可以指点一二。

小贴士

  • 如果 SyncClients 中没有加入 System.in.read() 代码,客户端程序会停止运行,客户端主动给服务器端发送 RST 报文重置连接。

参考博客

浅谈tcp socket的backlog参数

原文地址:https://www.cnblogs.com/kendoziyu/p/java-serversocket-backlog.html