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

Java TCP Socket 多執行緒聊天室完整實作:Server/Client 雙端原始碼解析

     編輯:Java 相關知識

基於 TCP Socket 的多執行緒聊天室是網路程式設計經典練習。本文提供完整 Server/Client 端原始碼,深度解析 ServerSocket、Socket、ExecutorService、I/O 流處理、廣播機制與優雅關閉等核心技術點。

架構設計

  • Server:單一 ServerSocket 監聽端口,主執行緒 accept() 阻塞等待連線,每新連線派發一個 ThreadServer 任務到執行緒池處理。
  • Client:主執行緒讀取伺服器推送訊息 (阻塞 readLine()),子執行緒 ThreadClient 讀取控制台輸入並發送。
  • 協議:文本行協議,exit 指令觸發斷開。編碼統一使用 GB2312 (建議現代專案改用 UTF-8)。

Server 端完整實作

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

public class TCPServer {
    private static final int PORT = 8888;
    private final ServerSocket serverSocket;
    private final List<Socket> clients = Collections.synchronizedList(new ArrayList<>());
    private final ExecutorService executor = Executors.newCachedThreadPool();

    public TCPServer() throws IOException {
        serverSocket = new ServerSocket(PORT);
        System.out.println("聊天室伺服器啟動,監聽端口: " + PORT);
    }

    public void start() {
        try {
            while (!serverSocket.isClosed()) {
                Socket clientSocket = serverSocket.accept();
                clients.add(clientSocket);
                executor.execute(new ClientHandler(clientSocket));
                broadcast("【系統】用戶 " + clientSocket.getInetAddress() + " 加入,當前在線: " + clients.size());
            }
        } catch (IOException e) {
            if (!serverSocket.isClosed()) e.printStackTrace();
        } finally {
            shutdown();
        }
    }

    private void broadcast(String msg) {
        String payload = msg + "\n"; // 換行符作為訊息邊界
        synchronized (clients) {
            for (Socket client : clients) {
                try (PrintWriter out = new PrintWriter(
                        new OutputStreamWriter(client.getOutputStream(), "GB2312"), true)) {
                    out.println(payload);
                } catch (IOException e) {
                    System.err.println("發送給 " + client.getInetAddress() + " 失敗: " + e.getMessage());
                }
            }
        }
    }

    private void removeClient(Socket socket) {
        clients.remove(socket);
        try { socket.close(); } catch (IOException ignored) {}
        broadcast("【系統】用戶 " + socket.getInetAddress() + " 離開,當前在線: " + clients.size());
    }

    private void shutdown() {
        executor.shutdownNow();
        synchronized (clients) {
            for (Socket s : clients) {
                try { s.close(); } catch (IOException ignored) {}
            }
            clients.clear();
        }
        try { serverSocket.close(); } catch (IOException ignored) {}
    }

    /** 處理單一客戶端連線的任務 */
    private class ClientHandler implements Runnable {
        private final Socket socket;
        private final BufferedReader in;

        ClientHandler(Socket socket) throws IOException {
            this.socket = socket;
            this.in = new BufferedReader(
                new InputStreamReader(socket.getInputStream(), "GB2312"));
        }

        @Override
        public void run() {
            String msg;
            try {
                while ((msg = in.readLine()) != null) {
                    if ("exit".equalsIgnoreCase(msg.trim())) {
                        break; // 觸發移除邏輯
                    }
                    String broadcastMsg = socket.getInetAddress() + ": " + msg;
                    broadcast(broadcastMsg);
                }
            } catch (IOException e) {
                System.err.println("讀取 " + socket.getInetAddress() + " 訊息異常: " + e.getMessage());
            } finally {
                removeClient(socket);
            }
        }
    }

    public static void main(String[] args) {
        try {
            new TCPServer().start();
        } catch (IOException e) {
            System.err.println("伺服器啟動失敗: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Client 端完整實作

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

public class TCPClient {
    private static final String SERVER_HOST = "127.0.0.1"; // 本機測試
    private static final int SERVER_PORT = 8888;

    public static void main(String[] args) {
        try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
             BufferedReader in = new BufferedReader(
                 new InputStreamReader(socket.getInputStream(), "GB2312"));
             PrintWriter out = new PrintWriter(
                 new OutputStreamWriter(socket.getOutputStream(), "GB2312"), true);
             BufferedReader console = new BufferedReader(new InputStreamReader(System.in))) {

            System.out.println("已連線到聊天室,輸入 'exit' 退出。");

            // 執行緒:持續讀取伺服器推送訊息
            Thread readerThread = new Thread(() -> {
                try {
                    String serverMsg;
                    while ((serverMsg = in.readLine()) != null) {
                        System.out.println(serverMsg);
                    }
                } catch (IOException e) {
                    System.err.println("連線已斷開: " + e.getMessage());
                }
            }, "Server-Reader");
            readerThread.setDaemon(true); // 主執行緒結束時自動退出
            readerThread.start();

            // 主執行緒:讀取控制台輸入並發送
            String input;
            while ((input = console.readLine()) != null) {
                out.println(input);
                if ("exit".equalsIgnoreCase(input.trim())) {
                    break;
                }
            }
        } catch (IOException e) {
            System.err.println("客戶端異常: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

關鍵技術點深度解析

1. 執行緒池隔離阻塞 I/O

Executors.newCachedThreadPool() 為每個連線建立獨立執行緒,避免單執行緒阻塞導致整個伺服器無響應。高併發場景建議改用 ThreadPoolExecutor 固定大小池 + 有界隊列,防止 OOM。

2. 線程安全的客戶端集合

Collections.synchronizedList 或 CopyOnWriteArrayList 確保多執行緒下 clients 列表操作安全。廣播時需同步迭代或快照複製。

3. 訊息邊界處理

使用 BufferedReader.readLine() 配合 PrintWriter.println(),以換行符 \n 作為訊息分隔符。注意:若訊息本身包含換行,需轉義或改用長度前綴/特定分隔符協議。

4. 編碼一致性

Server/Client 雙端必須使用相同編碼 (GB2312 / UTF-8) 建構 InputStreamReader/OutputStreamWriter,否則中文亂碼。

5. 優雅關閉與資源釋放

  • Client 發送 exit → Server 偵測到 → 從列表移除 → 關閉 Socket → 廣播離線通知。
  • 使用 try-with-resources (Client 端) 或 finally 區塊 (Server 端) 確保流與 Socket 關閉。
  • ExecutorService.shutdownNow() 停止接受新任務並嘗試中斷執行中任務。

生產環境擴展建議

維度當前實作生產級改進
I/O 模型阻塞 I/O (BIO) + 執行緒池NIO (Netty) 或虛擬執行緒 (Java 21+)
協議文本行協議Protobuf / JSON over WebSocket / gRPC
用戶身份IP 地址登入認證 (JWT/Session) + 用戶 ID
訊息類型純文本私聊/群聊/系統通知/檔案/表情 等結構化類型
持久化無資料庫儲存歷史訊息、離線推送
高可用單點叢集 + Redis Pub/Sub 廣播 + 負載均衡

編譯執行驗證

# 終端 1: 啟動 Server
javac TCPServer.java && java TCPServer

# 終端 2: 啟動 Client A
javac TCPClient.java && java TCPClient

# 終端 3: 啟動 Client B
java TCPClient

在任一 Client 輸入文字按 Enter,另一 Client 即可收到廣播訊息。輸入 exit 退出。

這個經典範例雖簡單,卻涵蓋了網路程式設計的核心骨架:監聽、接受、多工處理、協議設計、資源管理,是進階學習 Netty、WebSocket、gRPC 的最佳基石。

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