1. 程式人生 > 實用技巧 >Java 實現簡單的 Socket 通訊

Java 實現簡單的 Socket 通訊

Java socket 封裝了傳輸層的實現細節,開發人員可以基於 socket 實現應用層。本文介紹了 Java socket 簡單用法。

傳輸層協議

傳輸層包含了兩種協議,分別是 TCP (Transfer Control Protocol,傳輸控制協議) 和 UDP (User Datagram Protocol,使用者資料報協議)。

TCP 是一種面向連線,可靠的傳輸協議。通訊雙方在“傳送-接收”資料之前需要先建立 TCP 連線。所謂連線,指的是各種裝置、線路,或網路中進行通訊的應用程式為了相互傳遞訊息而建立的專有、虛擬的通訊線路。連線一旦建立,進行通訊的應用程式只使用該虛擬的通訊線路傳送和接收資料。TCP 還需要處理端到端之間的流量控制。

UDP 是一種無連線的,不可靠的傳輸協議。傳送方不需要與接收方建立連線,可以直接傳送資料。

TCP 通過序列號、確認應答、資料校驗等機制確保了傳輸的可靠性,適用於需要可靠資料傳輸的場景,應用層協議 HTTP,FTP 基於 TCP。UDP 沒有複雜的控制機制,不糾錯,不重發,不保證資料的準確性,不確保資料到達目的地;不過 UDP 傳送等量資料花費更小的流量,適用於對時延要求高但對準確性要求不高的場景,如視訊、音訊通訊。

Java 中有 3 種套接字類,java.net.Socket 和 java.net.ServerSocket 基於 TCP,java.net.DatagramSocket 基於 UDP。

TCP 示例

TCP 是面向連線的,所以在進行通訊之前傳送端(客戶端)需要先連線到接收端(服務端)。Java 中使用 java.net.Socket 來表示套接字,客戶端與服務端通過套接字來互發資料。使用 java.net.ServerSocket 來表示服務端套接字。

客戶端通過 1 個 java.net.Socket 來與服務端進行連線,服務端在接收到連線請求之後會建立 1 個 java.net.Socket 來與客戶端進行連線。之後,客戶端和服務端就可以通過兩個 Socket 來收發資料了。

下面分別建立一個服務端程式和客戶端程式,客戶端程式接收控制檯輸入的內容,客戶端控制檯每輸入一行,就往服務端傳送,服務端接收到訊息之後,將訊息列印到控制檯。當客戶端輸入 "Bye" 時,客戶端斷開與服務端的連線,客戶端程式退出,服務端程式繼續等待連線。

服務端程式程式碼

import java.net.*;
import java.io.*;

class Server {
    
    public static void main(String[] args) {
        // ServerSocket 實現了 AutoCloseable 介面,所以支援 try-with-resource 語句
        // 建立一個 ServerSocket,監聽 9090 埠
        try(ServerSocket serv = new ServerSocket(9090)){
            System.out.printf("Bind Port %d\n", serv.getLocalPort());
            Socket socket = null;
            while(true){
                // 接收連線,如果沒有連線,accept() 方法會阻塞
                socket = serv.accept();
                
                // 獲取輸入流,並使用 BufferedInputStream 和 InputStreamReader 裝飾,方便以字元流的形式處理,方便一行行讀取內容
                try(BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream()) )){
                    String msg = null;
                    char[] cbuf = new char[1024];
                    int len = 0;
                    while( (len = in.read(cbuf, 0, 1024)) != -1 ){ // 迴圈讀取輸入流中的內容
                        msg = new String(cbuf, 0, len);
                        if("Bye".equals(msg)) { // 如果檢測到 "Bye" ,則跳出迴圈,不再讀取輸入流中內容。
                            break;
                        }
                        System.out.printf("Received Message --> %s \n", msg);
                    }
                }catch (IOException e){
                    e.printStackTrace();
                }
                
            }
            
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客戶端程式程式碼

import java.net.*;
import java.io.*;
import java.util.*;

class Client{
    
    public static void main(String[] args){
        
        try(Socket socket = new Socket("localhost", 9090)){
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            Scanner scanner = new Scanner(System.in);
            scanner.useDelimiter("\r\n");
            String msg = null;
            while( !(msg = scanner.next()).equals("Bye") ){
                System.out.printf("Send Msg --> %s \n", msg);
                out.write(msg);
                out.flush(); // 立即傳送,否則需要積累到一定大小才一次性發送
            }
        }catch (IOException e){
            e.printStackTrace();
        }
        
    }
    
}

UDP 示例

UDP 不需要連線,客戶端與服務端通過傳送資料報來完成通訊。Java 中使用 java.net.DatagramSocket 來表示 UDP 客戶端和服務端的套接字,使用 java.net.DatagramPacket 來表示 UDP 的資料報。

下面基於 UDP 來實現與上面程式同樣的功能,不過訊息可能會出錯,某些訊息可能也不能到達服務端。

服務端程式程式碼

import java.net.*;
import java.io.*;

class Server {
    
    public static void main(String[] args){
        
        // 建立一個 DatagramPacket 例項,用來接收客戶端傳送過來的 UDP 資料報,這個例項可以重複利用。
        byte[] buf = new byte[8192]; // 快取區
        int len = buf.length;        // 要利用的快取區的大小
        DatagramPacket pac = new DatagramPacket(buf, len);
        
        // 建立服務端的套接字,需要指定繫結的埠號
        try(DatagramSocket serv = new DatagramSocket(9191)){
            
            while(true){
                serv.receive(pac); // 接收資料報。如果沒有資料報傳送過去,會阻塞
                System.out.println("Message --> " + new String(pac.getData(), 0, pac.getLength()));
            }
            
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    
}

客戶端程式程式碼

import java.io.*;
import java.net.*;
import java.util.*;

class Client {
    public static void main(String[] args){
        
        // 建立一個客戶端的 UDP 套接字,不需要指定任何資訊
        try(DatagramSocket client = new DatagramSocket()){
            
            // 建立一個數據報例項,資料和長度在傳送之前都會重新設定,所以這裡直接置為 0 即可。 
            // 由於是傳送端,所以需要設定服務端的地址和埠
            DatagramPacket pac = new DatagramPacket(new byte[0], 0, InetAddress.getByName("localhost"), 9191);
            
            // 掃描控制檯輸入
            Scanner scanner = new Scanner(System.in);
            scanner.useDelimiter("\r\n");
            String msg = null;
            while( !(msg = scanner.next()).equals("Bye") ){
                // 設定要傳送的資料
                pac.setData(msg.getBytes()); 
                // 傳送資料報
                client.send(pac);
                System.out.println("Sent Message --> " + msg);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
        
    }
}

需要注意的是,UDP 是面向無連線的,但 DatagramSocket 的 API 中提供了 connect 相關的方法,這裡的 connect 並非 TCP 中連線的意思。而是指定了當前的 UDP 套接字只能夠向指定的主機和埠傳送資料報。