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 報文重置連接。