Socket 是網絡編程的基石。本文通過一個「雙向聊天室」完整 Demo,演示 TCP 長連接下的並發收發架構:主線程監聽鍵盤輸入發送,子線程循環接收對端消息。代碼可直接運行,適合初學者理解 Socket 全雙工通信模型。
┌─────────────────────────────────────────────────────────────┐ │ TalkMe (主類) │ ├─────────────────────────────────────────────────────────────┤ │ main() │ │ ├─ listener() → Thread A: ServerSocket.accept() 循環接收 │ │ └─ talker(ip) → Thread B: Socket 連接遠端 + 發送循環 │ └─────────────────────────────────────────────────────────────┘
package demo.socket;
import java.io.*;
import java.net.*;
public class TalkMe {
private static final int PORT = 9666;
public static void main(String[] args) throws IOException {
System.out.print("請輸入對方 IP: ");
String peerIp = new BufferedReader(new InputStreamReader(System.in)).readLine();
TalkMe app = new TalkMe();
app.listener().start(); // 線程 A: 作為服務端監聽
app.talker(peerIp).start(); // 線程 B: 作為客戶端連接
}
// 線程 A: 監聽對方連接並接收消息
private Thread listener() {
return new Thread(() -> {
try (ServerSocket server = new ServerSocket(PORT)) {
System.out.println("監聽中... 端口: " + PORT);
try (Socket socket = server.accept();
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
System.out.println("連接建立: " + socket.getInetAddress());
String msg;
while ((msg = in.readLine()) != null) {
System.out.println("\n[對方] " + msg);
System.out.print("我: "); // 提示符
}
}
} catch (IOException e) {
System.err.println("監聽異常: " + e.getMessage());
}
}, "Listener-Thread");
}
// 線程 B: 連接對方並發送鍵盤輸入
private Thread talker(String peerIp) {
return new Thread(() -> {
try (Socket socket = new Socket(peerIp, PORT);
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
BufferedReader keyboard = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
System.out.println("連接成功: " + peerIp + ":" + PORT);
System.out.print("我: ");
String line;
while ((line = keyboard.readLine()) != null) {
out.println(line); // 自動 flush
System.out.print("我: ");
}
} catch (UnknownHostException e) {
System.err.println("未知主機: " + peerIp);
} catch (IOException e) {
System.err.println("通信異常: " + e.getMessage());
}
}, "Talker-Thread");
}
}
java demo.socket.TalkMeServerSocket.accept() 阻塞等待連接,建立後循環 readLine() 接收new Socket(ip, port) 連接遠端,循環讀取鍵盤 readLine() 並 println() 發送System.out.print("我: ") 區分輸入/輸出// 接收端
new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))
// 發送端
new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true) // true = autoFlush
關鍵:StandardCharsets.UTF_8 顯式指定編碼,避免跨平台亂碼;PrintWriter 第二參數 true 開啟自動刷新,println() 即發送。
所有 Socket、流對象均在 try(...) 中聲明,異常/正常退出自動 close(),無洩漏。
| 問題 | 原因 | 改進 |
|---|---|---|
| 單機雙開端口衝突 | 雙方用同一 PORT 監聽 | 雙方協商不同端口,或改為一端監聽一端連接 |
| NAT/防火牆不通 | 公網 IP 不可直連 | 端口映射、STUN/TURN、中繼服務器、VPN |
| 消息粘包/半包 | TCP 字節流無邊界 | 應用層協議:長度前綴 / 分隔符 / 序列化框架 |
| 異常時線程無提示 | catch 僅打印錯誤 | 加入重連邏輯、心跳檢測、斷線重連 |
// 1. 協議層:定義消息格式
public record ChatMessage(String from, String content, long timestamp) {}
// 2. 編解碼器:JSON 序列化 + 長度前綴
public class MessageCodec {
public byte[] encode(ChatMessage msg) { ... }
public ChatMessage decode(byte[] data) { ... }
}
// 3. Netty 重構:事件驅動、非阻塞、管道處理
@ChannelHandler.Sharable
public class ChatClientHandler extends SimpleChannelInboundHandler {
@Override protected void channelRead0(ChannelHandlerContext ctx, ChatMessage msg) {
System.out.println("[" + msg.timestamp() + "] " + msg.from() + ": " + msg.content());
}
}
這個 Demo 展示了 Socket 通信的核心要素:ServerSocket 監聽、Socket 連接、流包裝、雙線程全雙工、編碼顯式化、資源自動管理。理解這些基礎,再學習 NIO、Netty、gRPC 等高級框架才能知其然更知其所以然。