Java Socket:飛鴿傳書的網路套接字

  • 2019 年 10 月 3 日
  • 筆記

在古代,由於通訊不便利,一些聰明的人就利用鴿子會飛且飛得比較快、會辨認方向的優點,對其進行了馴化,用來進行消息的傳遞——也就是所謂的“飛鴿傳書”。而在 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 編程也比較有趣,很多初學者都會編寫一兩個基於“客戶端-伺服器端”的小程式來提高自己的編程水平,建議你也試一試。