淺談 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 報文重置連線。