Socket 通信的本質是「雙向字節流」。本文通過三個版本演進,展示如何從「單線程順序處理」→「多線程並發處理」構建健壯 TCP 服務端,並對比兩種流包裝方式 (BufferedReader/PrintWriter vs DataInputStream/DataOutputStream) 的適用場景。
public class MyServerV1 {
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(5678)) {
while (true) {
try (Socket client = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream())) {
String line;
while ((line = in.readLine()) != null) {
System.out.println("收到: " + line);
out.println("has receive....");
out.flush(); // 關鍵:手動 flush
if ("end".equals(line)) break;
}
}
}
}
}
}
問題:處理完一個客戶端才能接受下一個,併發能力為零。
public class MultiUserServer extends Thread {
private final Socket client;
public MultiUserServer(Socket client) { this.client = client; }
@Override public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream())) {
String line;
while ((line = in.readLine()) != null) {
System.out.println("[" + Thread.currentThread().getName() + "] " + line);
out.println("has receive....");
out.flush();
if ("end".equals(line)) break;
}
} catch (IOException e) {
// 記錄日誌
} finally {
try { client.close(); } catch (IOException ignored) {}
}
}
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(5678)) {
System.out.println("多線程服務端啟動,端口 5678");
while (true) {
new MultiUserServer(server.accept()).start();
}
}
}
}
優點:每個客戶端獨立線程,真正並發。缺點:無限創建線程,高併發下 OOM。
public class ThreadPoolServer {
private static final ExecutorService POOL = new ThreadPoolExecutor(
50, 200, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("Worker-%d").build(),
new ThreadPoolExecutor.AbortPolicy() // 隊列滿拒絕新連接
);
public static void main(String[] args) throws IOException {
Runtime.getRuntime().addShutdownHook(new Thread(() -> POOL.shutdown()));
try (ServerSocket server = new ServerSocket(5678)) {
server.setSoTimeout(1000);
System.out.println("線程池服務端啟動,端口 5678");
while (true) {
try {
Socket client = server.accept();
POOL.submit(() -> handleClient(client));
} catch (SocketTimeoutException ignored) {}
}
}
}
private static void handleClient(Socket client) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8), true)) {
String line;
while ((line = in.readLine()) != null) {
System.out.println("[" + Thread.currentThread().getName() + "] " + line);
out.println("Server echo: " + line);
if ("end".equalsIgnoreCase(line)) break;
}
} catch (IOException e) {
// log
} finally {
try { client.close(); } catch (IOException ignored) {}
}
}
}
| 維度 | BufferedReader + PrintWriter | DataInputStream + DataOutputStream |
|---|---|---|
| 數據類型 | 文本/字符串 (行制) | 基本類型 + 字節/字符串 (二進制) |
| 編碼處理 | InputStreamReader/OutputStreamWriter 指定編碼 | UTF-8 修飾字符串 (readUTF/writeUTF),其餘為二進制 |
| 適用協議 | 文本協議 (HTTP, SMTP, Redis RESP, 自定義行制) | 二進制協議、混合類型、需精確控制字節序 |
| 字符串讀取 | readLine() (換行符分隔) | readUTF() (長度前綴 + UTF-8 字節) |
| 性能 | 字符轉換開銷稍大 | 二進制直接讀寫,效能略優 |
| 推薦場景 | 文本協議首選 | 自定義二進制協議、與 C/C++ 交互、高性能內部 RPC |
// 發送端
try (DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
out.writeInt(12345); // 4字節 int
out.writeLong(9876543210L); // 8字節 long
out.writeUTF("Hello 中文"); // 2字節長度 + UTF-8字節
out.writeBoolean(true); // 1字節
out.flush();
}
// 接收端
try (DataInputStream in = new DataInputStream(socket.getInputStream())) {
int i = in.readInt();
long l = in.readLong();
String s = in.readUTF(); // 必須雙端協議一致
boolean b = in.readBoolean();
}
StandardCharsets.UTF_8,杜絕亂碼Socket 編程核心不在 API,而在 併發模型、協議設計、資源管理、異常隔離。從單線程 → 多線程 → 線程池的演進,體現了「先正確、再健壯、後高性能」的工程哲學。文本協議用 BufferedReader/PrintWriter,二進制協議用 DataInputStream/DataOutputStream 或專業序列化框架 (Protobuf, MessagePack)。