首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Socket 阻塞式 TCP 通訊完整實作:從單執行緒到線程池優化

     編輯:Java 相關知識

阻塞式 Socket 通訊模型

Java java.net.Socket 與 ServerSocket 提供了最基礎的阻塞式 TCP 通訊能力。雖然 NIO (Non-blocking I/O) 更適合高併發,但阻塞模型因程式碼直觀、除錯容易,仍是中小型應用、內部工具、教學示範的首選。

一、伺服器端實作

1. 基礎單執行緒 Echo Server

public class EchoServer {
    private final int port = 8000;
    private ServerSocket serverSocket;

    public EchoServer() throws IOException {
        serverSocket = new ServerSocket(port);
        System.out.println("Server Started on port " + port);
    }

    public String echo(String msg) {
        return "echo: " + msg;
    }

    private PrintWriter getWriter(Socket socket) throws IOException {
        return new PrintWriter(socket.getOutputStream(), true); // auto-flush
    }

    private BufferedReader getReader(Socket socket) throws IOException {
        return new BufferedReader(new InputStreamReader(socket.getInputStream()));
    }

    public void service() {
        while (true) {
            Socket socket = null;
            try {
                socket = serverSocket.accept(); // 阻塞等待客戶端連線
                System.out.println("New connection: " + socket.getInetAddress() + ":" + socket.getPort());
                
                BufferedReader br = getReader(socket);
                PrintWriter pw = getWriter(socket);
                String msg;
                while ((msg = br.readLine()) != null) {
                    System.out.println("Received: " + msg);
                    pw.println(echo(msg));
                    if ("bye".equalsIgnoreCase(msg)) break; // 約定結束指令
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                closeQuietly(socket);
            }
        }
    }

    private void closeQuietly(Socket socket) {
        if (socket != null) try { socket.close(); } catch (IOException ignored) {}
    }

    public static void main(String[] args) throws IOException {
        new EchoServer().service();
    }
}

2. 關鍵點說明

  • serverSocket.accept() 阻塞直到有客戶端連入,回傳對應的 Socket。
  • 透過 getInputStream()/getOutputStream() 取得位元流,包裝為 BufferedReader/PrintWriter 處理文字行。
  • 單執行緒模型一次只能服務一個客戶端,後續連線會排隊等待。

二、客戶端實作

public class EchoClient {
    private final String host = "localhost";
    private final int port = 8000;
    private Socket socket;

    public EchoClient() throws IOException {
        socket = new Socket(host, port); // 連線至伺服器
    }

    private PrintWriter getWriter(Socket socket) throws IOException {
        return new PrintWriter(socket.getOutputStream(), true);
    }

    private BufferedReader getReader(Socket socket) throws IOException {
        return new BufferedReader(new InputStreamReader(socket.getInputStream()));
    }

    public void talk() throws IOException {
        try (BufferedReader br = getReader(socket);
             PrintWriter pw = getWriter(socket);
             BufferedReader console = new BufferedReader(new InputStreamReader(System.in))) {

            String msg;
            while ((msg = console.readLine()) != null) {
                pw.println(msg);           // 發送給伺服器
                System.out.println(br.readLine()); // 接收回應
                if ("bye".equalsIgnoreCase(msg)) break;
            }
        } finally {
            closeQuietly(socket);
        }
    }

    private void closeQuietly(Socket socket) {
        if (socket != null) try { socket.close(); } catch (IOException ignored) {}
    }

    public static void main(String[] args) throws IOException {
        new EchoClient().talk();
    }
}

三、優雅關閉與半關閉機制

1. 判斷連線狀態

boolean isConnected = socket.isConnected() && !socket.isClosed();

isConnected():曾經成功連線過即為 true(即使已斷開)。
isClosed():close() 被呼叫後為 true。

2. 訊息邊界處理三種常見策略

策略適用場景範例
特殊結束標記文字行協議雙方約定 "bye" 結束
長度前綴二進位資料流先發 4 bytes 長度,再發內容
流尾關閉單向傳輸發送端 shutdownOutput(),接收端讀到 -1

3. 半關閉

socket.shutdownOutput(); // 僅關閉輸出流,仍可讀取對方回應
socket.shutdownInput();  // 僅關閉輸入流
// 最後仍需呼叫 socket.close() 釋放底層資源

四、多執行緒優化:線程池模型

使用 ExecutorService 管理固定大小線程池,主執行緒只負責 accept(),實際通訊交給 Worker 執行緒:

public class MultiEchoServer {
    private final int port = 8000;
    private ServerSocket serverSocket;
    private final ExecutorService executorService;

    public MultiEchoServer() throws IOException {
        serverSocket = new ServerSocket(port);
        int poolSize = Runtime.getRuntime().availableProcessors() * 4;
        executorService = Executors.newFixedThreadPool(poolSize);
        System.out.println("Multi-thread Server Started, pool size: " + poolSize);
    }

    public void service() {
        while (true) {
            try {
                Socket socket = serverSocket.accept();
                executorService.execute(new Handler(socket)); // 分派任務
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class Handler implements Runnable {
        private final Socket socket;
        Handler(Socket socket) { this.socket = socket; }

        @Override public void run() {
            try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                 PrintWriter pw = new PrintWriter(socket.getOutputStream(), true)) {
                System.out.println("Handler: " + socket.getInetAddress());
                String msg;
                while ((msg = br.readLine()) != null) {
                    pw.println("echo: " + msg);
                    if ("bye".equalsIgnoreCase(msg)) break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                closeQuietly(socket);
            }
        }
        private void closeQuietly(Socket s) { if (s != null) try { s.close(); } catch (IOException ignored) {} }
    }

    public static void main(String[] args) throws IOException {
        new MultiEchoServer().service();
    }
}

五、效能與擴展建議

  • 緩衝區大小:Socket.setReceiveBufferSize()/setSendBufferSize() 根據網路 MTU 調整(預設 64KB)。
  • 心跳機制:長連線需定期發送 PING/PONG 偵測斷線。
  • 協議升級:考慮使用 Netty、Mina 等 NIO 框架支撐萬級併發。
  • 序列化:避免 Java 原生序列化,推薦 Protobuf、JSON、MessagePack。

結語

阻塞式 Socket 雖非高併發首選,但其「所見即所得」的程式模型極利於快速原型開發與教學。掌握 accept-read/write-close 生命週期、訊息邊界處理、線程池分派,即可構建穩健的 TCP 服務。若需支撐 C10K 以上併發,請轉向 NIO/Netty 架構。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved