實現「伺服器發送訊息 → 客戶端回應 → 伺服器根據內容判斷再回覆」的多輪互動,核心在於:**伺服器為每個客戶端連線建立專屬執行緒,在執行緒內循環處理輸入/輸出流**;客戶端同樣需啟動接收執行緒異步讀取伺服器推送。
[Server Main Thread] ──accept()──▶ [ClientHandler Thread-1] ◀──Socket──▶ [Client Main Thread]
│ 循環 read/write │
▼ ▼
[ClientHandler Thread-2] ◀──Socket──▶ [Client Receiver Thread]
│ │
▼ ▼
... (多客戶端並發)
public class ChatServer {
private static final int PORT = 8189;
private static final Set clients = Collections.synchronizedSet(new HashSet<>());
public static void main(String[] args) throws IOException {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("伺服器啟動,監聽埠 " + PORT);
while (true) {
Socket client = serverSocket.accept();
clients.add(client);
new Thread(new ClientHandler(client)).start();
}
}
}
static class ClientHandler implements Runnable {
private final Socket socket;
private BufferedReader in;
private PrintWriter out;
ClientHandler(Socket socket) { this.socket = socket; }
@Override
public void run() {
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
String clientAddr = socket.getInetAddress().getHostAddress();
System.out.println(clientAddr + " 已連線");
broadcast("【系統】" + clientAddr + " 加入聊天室", socket);
String msg;
while ((msg = in.readLine()) != null) { // 循環讀取客戶端訊息
System.out.println("收自 " + clientAddr + ": " + msg);
// 業務邏輯:根據訊息內容決定回覆
String reply = processMessage(msg);
out.println(reply); // 回覆該客戶端
broadcast("[" + clientAddr + "] " + msg, socket); // 廣播給其他人
}
} catch (IOException e) {
System.err.println("客戶端異常斷開: " + e.getMessage());
} finally {
clients.remove(socket);
closeQuietly(socket);
broadcast("【系統】" + socket.getInetAddress().getHostAddress() + " 離開", null);
}
}
private String processMessage(String msg) {
// 範例:簡單指令處理
if ("/time".equals(msg)) return "伺服器時間: " + Instant.now();
if ("/quit".equals(msg)) throw new RuntimeException("客戶端請求斷開");
return "收到: " + msg; // 預設回覆
}
private void broadcast(String msg, Socket exclude) {
for (Socket c : clients) {
if (c != exclude && !c.isClosed()) {
try {
PrintWriter w = new PrintWriter(new OutputStreamWriter(c.getOutputStream(), StandardCharsets.UTF_8), true);
w.println(msg);
} catch (IOException ignored) {}
}
}
}
private void closeQuietly(Socket s) {
try { if (s != null) s.close(); } catch (IOException ignored) {}
}
}
}
public class ChatClient {
private static final String HOST = "localhost";
private static final int PORT = 8189;
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket(HOST, PORT)) {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
BufferedReader console = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
// 接收執行緒:持續讀取伺服器推送
Thread receiver = new Thread(() -> {
try {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line); // 顯示伺服器廣播/回覆
}
} catch (IOException e) {
System.err.println("連線中斷: " + e.getMessage());
}
});
receiver.setDaemon(true); // 主執行緒結束時自動退出
receiver.start();
// 主執行緒:讀取鍵盤輸入發送
String input;
System.out.print("> ");
while ((input = console.readLine()) != null) {
out.println(input);
if ("/quit".equals(input)) break;
System.out.print("> ");
}
}
}
}
| 要點 | 實現方式 | 原因 |
|---|---|---|
| 每客戶端一執行緒 | new Thread(new ClientHandler(socket)).start() | 阻塞式 readLine() 不會阻塞主監聽迴圈,支援高併發 |
| 雙向異步通訊 | 客戶端啟動獨立 receiver 執行緒 | 主執行緒可持續發送,同時即時接收伺服器推送 |
| 訊息邊界處理 | 使用 readLine() (換行符分隔) | 簡單可靠;生產環境建議自定協議 (長度前綴/JSON/Protobuf) |
| 資源釋放 | try-with-resources + finally 關閉 Socket | 防止檔案描述符洩漏,確保伺服器穩定運行 |
| 編碼統一 | 明確指定 StandardCharsets.UTF_8 | 避免中文亂碼,跨平台相容 |
ExecutorService 替代手動 new Thread,限制最大併發數。Selector 或 Netty 事件驅動模型。PING 檢測死連線,清理殭屍 Socket。SSLServerSocket)、認證授權、速率限制。Java Socket 多輪雙向通訊的核心模式是:**伺服器 accept 循環 + 客戶端專屬執行緒循環 read/write + 客戶端雙執行緒 (發送/接收)**。掌握此模式並注意資源管理、編碼統一、訊息邊界,即可構建穩健的網路互動應用。