1. 程式人生 > 實用技巧 >淺談 Java Socket 建構函式引數 backlog

淺談 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方法,重複繫結,產生異常!

如下圖所示:

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 的抓包結果如下:

  • 埠號為 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);
        }
    }
}

控制檯發生報錯:

  • 第一個客戶端 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引數