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

Java TCP Socket 程式設計:客戶端/伺服端完整實作範例

     編輯:Java 相關知識

前言

Socket 是網路通訊的基礎,Java 提供 java.net.Socket (客戶端) 與 java.net.ServerSocket (伺服端) 實作 TCP 可靠串流傳輸。本文提供一個完整的回聲伺服器範例,展示阻塞式 I/O、多執行緒處理併發連線、優雅關閉與錯誤處理的最佳實務。

架構概覽

  • EchoServer:監聽埠口,每個連線啟動一執行緒處理。
  • ClientHandler:讀取客戶端訊息並原樣回傳。
  • EchoClient:連線伺服器、發送訊息、接收回應。

伺服端實作

package com.example.socket;

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

public class EchoServer {
    private static final int PORT = 8888;
    private static final int MAX_THREADS = 50;
    private final ExecutorService threadPool = Executors.newFixedThreadPool(MAX_THREADS);
    private volatile boolean running = true;

    public void start() {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            serverSocket.setSoTimeout(1000); // 定期檢查 running 旗標
            System.out.println("伺服器啟動,監聽埠口: " + PORT);

            while (running) {
                try {
                    Socket clientSocket = serverSocket.accept();
                    System.out.println("新連線: " + clientSocket.getInetAddress());
                    threadPool.execute(new ClientHandler(clientSocket));
                } catch (SocketTimeoutException e) {
                    // 超時繼續迴圈,檢查 running
                } catch (IOException e) {
                    if (running) System.err.println("接受連線失敗: " + e.getMessage());
                }
            }
        } catch (IOException e) {
            System.err.println("伺服器啟動失敗: " + e.getMessage());
        } finally {
            shutdown();
        }
    }

    public void stop() {
        running = false;
    }

    private void shutdown() {
        threadPool.shutdown();
        try {
            if (!threadPool.awaitTermination(10, TimeUnit.SECONDS)) {
                threadPool.shutdownNow();
            }
        } catch (InterruptedException e) {
            threadPool.shutdownNow();
            Thread.currentThread().interrupt();
        }
        System.out.println("伺服器已關閉");
    }

    private static class ClientHandler implements Runnable {
        private final Socket socket;

        ClientHandler(Socket socket) { this.socket = socket; }

        @Override
        public void run() {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
                 PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true)) {

                String line;
                while ((line = in.readLine()) != null) {
                    System.out.println("收到: " + line);
                    if ("bye".equalsIgnoreCase(line.trim())) {
                        out.println("Goodbye!");
                        break;
                    }
                    out.println("Echo: " + line); // 回傳
                }
            } catch (IOException e) {
                System.err.println("處理連線錯誤: " + e.getMessage());
            } finally {
                try { socket.close(); } catch (IOException ignored) {}
                System.out.println("連線關閉: " + socket.getInetAddress());
            }
        }
    }

    public static void main(String[] args) {
        EchoServer server = new EchoServer();
        // 註冊關閉鉤子,支援 Ctrl+C 優雅關閉
        Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
        server.start();
    }
}

客戶端實作

package com.example.socket;

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

public class EchoClient {
    private static final String HOST = "localhost";
    private static final int PORT = 8888;

    public void start() {
        try (Socket socket = new Socket(HOST, PORT);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
             PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
             Scanner scanner = new Scanner(System.in)) {

            System.out.println("已連線至伺服器。輸入訊息 (輸入 bye 結束):");

            String input;
            while (scanner.hasNextLine() && (input = scanner.nextLine()) != null) {
                out.println(input); // 發送
                String response = in.readLine(); // 接收回應
                if (response == null) {
                    System.out.println("伺服器已關閉連線");
                    break;
                }
                System.out.println("伺服器回應: " + response);
                if ("bye".equalsIgnoreCase(input.trim())) break;
            }
        } catch (UnknownHostException e) {
            System.err.println("找不到主機: " + HOST);
        } catch (IOException e) {
            System.err.println("通訊錯誤: " + e.getMessage());
        }
    }

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

執行與測試

  1. 先啟動伺服端:java com.example.socket.EchoServer
  2. 再啟動一或多個客戶端:java com.example.socket.EchoClient
  3. 在客戶端輸入文字,觀察伺服端回應 Echo: ...
  4. 輸入 bye 結束該客戶端連線。

關鍵技術點解析

技術點說明
執行緒池使用 Executors.newFixedThreadPool 限制最大併發執行緒,避免連線暴增耗盡資源。
字元編碼明確指定 UTF-8,避免跨平台亂碼。
緩衝讀寫BufferedReader/PrintWriter 提供行導向讀寫,簡化協定處理。
自動刷新PrintWriter(..., true) 啟用 autoFlush,println 後立即發送。
優雅關閉關閉鉤子 + ExecutorService.shutdown() 確保資源釋放。
Socket 選項setSoTimeout 讓 accept() 定期超時,能檢查關閉旗標。

進階擴充方向

  1. 非阻塞 I/O (NIO):使用 Selector、ServerSocketChannel、SocketChannel 處理上萬併發連線 (C10K 問題)。
  2. 協定設計:定義訊息長度前綴、JSON/Protobuf 序列化、心跳機制、斷線重連。
  3. SSL/TLS 加密:使用 SSLServerSocketFactory、SSLSocketFactory 建立安全通道。
  4. 框架整合:Netty、Mina、gRPC 等高階網路框架簡化開發。

常見問題

Address already in use (BindException)
埠口被佔用。檢查是否已有實例運行、或等待 TIME_WAIT 狀態釋放 (約 60 秒),或設定 serverSocket.setReuseAddress(true)。
中文亂碼
客戶端與伺服端編碼不一致。雙方統一使用 UTF-8,並在 InputStreamReader/OutputStreamWriter 明確指定。
大量連線導致 OOM
執行緒堆疊佔用記憶體。減小 -Xss (如 256k)、改用 NIO、或限制 maxTotal 連線數。

掌握阻塞式 Socket 程式設計是理解網路通訊基礎的關鍵,也是進階學習 NIO、Netty 等高效能框架的基石。

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