淺談 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參數


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM