Java Socket:飛鴿傳書的網路套接字
在古代,由於通訊不便利,一些聰明的人就利用鴿子會飛且飛得比較快、會辨認方向的優點,對其進行了馴化,用來進行訊息的傳遞——也就是所謂的“飛鴿傳書”。而在 Java 中,網路套接字(Socket)扮演了同樣的角色。
套接字(Socket)是一個抽象層,應用程式可以通過它傳送或接收資料;就像操作檔案那樣可以開啟、讀寫和關閉。套接字允許應用程式將 I/O 應用於網路中,並與其他應用程式進行通訊。網路套接字是 IP 地址與埠的組合。
01、ping 與 telnet
“老王啊,能不能幫我看一下這個問題呢,明明本地可以進行網路通訊,可等我部署到伺服器上時就通訊不了了,搞了半天也不知道什麼原因,我看程式碼是沒有問題的。”小二的語氣中充滿了沮喪。
“ping 過嗎?或者 telnet 了嗎?”老王頭都沒回,冷冰冰地扔出去了這句話。
“哦,我去試試。”小二心頭掠過一絲愧疚。
ping 與 telnet 這兩個命令,對除錯網路程式有著非常大的幫助。
ping,一種計算機網路工具,用來測試資料包能否透過 IP 協議到達特定主機。ping 會向目標主機發出一個 ICMP 的請求回顯資料包,並等待接收回顯響應資料包。
例如,我們 ping 一下部落格園。截圖如下。
telnet,Internet 遠端登入服務的標準協議和主要方式,可以讓我們坐在家裡的計算機面前,登入到另一臺遠在天涯海角的遠端計算機上。
在 Windows 系統中,telnet 一般是預設安裝的,但未啟用(可以在控制面板中啟用它)。
例如,我們 telnet 一下火(shui)土(mu)社群。截圖如下。
使用 telnet 登入遠端計算機時,需要遠端計算機上執行一個服務,它一直不停地等待那些希望和它進行連線的網路請求;當接收到一個客戶端的網路連線時,它便喚醒正在監聽網路連線請求的伺服器程序,併為兩者建立連線。連線會一直保持,直到某一方中止。
不過,需要注意的是,telnet 在格外重視安全的現代網路技術中並不受到重用。因為 telnet 是一個明文傳輸協議,使用者的所有內容(包括使用者名稱和密碼)都沒有經過加密,安全隱患非常大。
02、Socket 例項
不知道你有沒有體驗一下 telnet 火土社群的那條命令,結果非常有趣。我們也可以通過 Java 的客戶端套接字(Socket)實現,程式碼示例如下。
try (Socket socket = new Socket("bbs.newsmth.net", 23);) {
InputStream is = socket.getInputStream();
Scanner scanner = new Scanner(is, "gbk");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
1)建立套接字連線非常簡單,只需要一行程式碼:
Socket socket = new Socket(host, port)
host 為主機名,port 為埠號(23 為預設的 telnet 埠號)。如果無法確定主機的 IP 地址,則丟擲 UnknownHostException
異常;如果在建立套接字時發生 IO 錯誤,則丟擲 IOException
異常。
需要注意的是,套接字在建立的時候,如果遠端主機不可訪問,這段程式碼就會阻塞很長時間,直到底層作業系統的限制而丟擲異常。所以一般會在套接字建立後設置一個超時時間。
Socket socket = new Socket(...);
socket.setSoTimeout(10000); // 單位為毫秒
2)套接字連線成功後,可以通過 java.net.Socket
類的 getInputStream()
方法獲取輸入流。有了 InputStream
物件後,可以藉助文字掃描器類(Scanner)將其中的內容打印出來。
InputStream is = socket.getInputStream();
Scanner scanner = new Scanner(is, "gbk");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
部分結果(完整結果自己親手實踐一下哦)如下圖所示:
03、ServerSocket 例項
接下來,我們模擬一個遠端服務,通過 java.net.ServerSocket
實現。程式碼示例如下。
try (ServerSocket server = new ServerSocket(8888);
Socket socket = server.accept();
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
Scanner scanner = new Scanner(is)) {
PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "gbk"), true);
pw.println("你好啊,歡迎關注「沉默王二」 公眾號,回覆關鍵字「2048」 領取程式設計師進階必讀資料包");
boolean done = false;
while (!done && scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
if ("2048".equals(line)) {
done = true;
}
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
1)建立伺服器端的套接字也比較簡單,只需要指定一個能夠獨佔的埠號就可以了(0~1023 這些埠都已經被系統預留了)。
ServerSocket server = new ServerSocket(8888);
2)呼叫 ServerSocket 物件的 accept()
等待客戶端套接字的連線請求。一旦監聽到客戶端的套接字請求,就會返回一個表示連線已建立的 Socket 物件,可以從中獲取到輸入流和輸出流。
Socket socket = server.accept();
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
客戶端套接字傳送的所有資訊都會包裹在伺服器端套接字的輸入流中;而伺服器端套接字傳送的所有資訊都會包裹在客戶端套接字的輸出流中。
3)伺服器端可以通過以下程式碼向客戶端傳送訊息。
PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "gbk"), true);
pw.println("你好啊,歡迎關注「沉默王二」 公眾號,回覆關鍵字「2048」 領取程式設計師進階必讀資料包");
4)伺服器端可以通過以下程式碼讀取客戶端傳送過來的訊息。
Scanner scanner = new Scanner(is);
boolean done = false;
while (!done && scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
if ("2048".equals(line)) {
done = true;
}
}
執行該服務後,可以通過 telnet localhost 8888
命令連線該遠端服務,不出所料,你將會看到以下資訊。
PS:可以在當前命令視窗中輸入 2048,服務端收到該訊息後會中斷該套接字連線(當前視窗會顯示“遺失對主機的連線”)。
04、為多個客戶端服務
非常遺憾的是,上面的例子中,伺服器端只能為一個客戶端服務——這不符合伺服器端一對多的要求。
優化方案也非常簡單(你應該也能想得到):伺服器端接收到客戶端的套接字請求時,可以啟動一個執行緒來處理,而主程式繼續等待下一個連線。程式碼示例如下。
try (ServerSocket server = new ServerSocket(8888)) {
while (true) {
Socket socket = server.accept();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 套接字處理程式
}
});
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
執行緒內部(run(){}
方法裡)用來處理套接字,程式碼示例如下:
try {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
Scanner scanner = new Scanner(is);
// 其他程式碼省略
// 向客戶端傳送訊息
// 讀取客戶端傳送過來的訊息
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
伺服器端程式碼優化後重新執行,你就可以通過 telnet 命令測試了。開啟一個命令列視窗輸入 telnet localhost 8888
,再開啟一個新的命令列視窗輸入 telnet localhost 8888
,多個視窗都可以和伺服器端進行通訊,除非伺服器端程式碼中斷執行。
05、最後
如今大多數基於網路的軟體,如瀏覽器、即時通訊工具甚至是 P2P 下載都是基於 Socket 實現的,所以掌握 Java Socket 程式設計還是蠻有必要的。Socket 程式設計也比較有趣,很多初學者都會編寫一兩個基於“客戶端-伺服器端”的小程式來提高自己的程式設計水平,建議你也試一試。