基於 TCP Socket 的多執行緒聊天室是網路程式設計經典練習。本文提供完整 Server/Client 端原始碼,深度解析 ServerSocket、Socket、ExecutorService、I/O 流處理、廣播機制與優雅關閉等核心技術點。
ServerSocket 監聽端口,主執行緒 accept() 阻塞等待連線,每新連線派發一個 ThreadServer 任務到執行緒池處理。readLine()),子執行緒 ThreadClient 讀取控制台輸入並發送。exit 指令觸發斷開。編碼統一使用 GB2312 (建議現代專案改用 UTF-8)。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();
}
}
}
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();
}
}
}
Executors.newCachedThreadPool() 為每個連線建立獨立執行緒,避免單執行緒阻塞導致整個伺服器無響應。高併發場景建議改用 ThreadPoolExecutor 固定大小池 + 有界隊列,防止 OOM。
Collections.synchronizedList 或 CopyOnWriteArrayList 確保多執行緒下 clients 列表操作安全。廣播時需同步迭代或快照複製。
使用 BufferedReader.readLine() 配合 PrintWriter.println(),以換行符 \n 作為訊息分隔符。注意:若訊息本身包含換行,需轉義或改用長度前綴/特定分隔符協議。
Server/Client 雙端必須使用相同編碼 (GB2312 / UTF-8) 建構 InputStreamReader/OutputStreamWriter,否則中文亂碼。
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 的最佳基石。