基礎阻塞 IO 模型(一請一應)
Server 端
public class BlockingServer {
private static final int PORT = 1111;
private static final Logger log = LoggerFactory.getLogger(BlockingServer.class);
public static void main(String[] args) {
try (ServerSocket server = new ServerSocket(PORT)) {
log.info("Server 啟動,監聽端口 {}", PORT);
while (true) {
Socket client = server.accept(); // 阻塞等待連接
// 簡單處理:每連接一線程(生產環境請用線程池)
new Thread(() -> handle(client)).start();
}
} catch (IOException e) {
log.error("Server 異常", e);
}
}
private static void handle(Socket socket) {
String clientInfo = socket.getRemoteSocketAddress().toString();
try (BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true)) {
log.info("客戶端連接: {}", clientInfo);
String line;
while ((line = in.readLine()) != null) { // 按行讀取
log.info("收到 [{}] : {}", clientInfo, line);
// 業務邏輯:回显 + 時間戳
out.println("Echo: " + line + " @ " + Instant.now());
}
} catch (IOException e) {
log.warn("客戶端斷開 [{}] : {}", clientInfo, e.getMessage());
} finally {
try { socket.close(); } catch (IOException ignored) {}
}
}
}
Client 端
public class BlockingClient {
private static final String HOST = "127.0.0.1";
private static final int PORT = 1111;
public static void main(String[] args) {
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))) {
System.out.println("已連接服務器,輸入內容回車發送,輸入 exit 退出");
String line;
while ((line = console.readLine()) != null) {
if ("exit".equalsIgnoreCase(line)) break;
out.println(line); // 發送
String resp = in.readLine(); // 同步等待響應
System.out.println("服務器回應: " + resp);
}
} catch (IOException e) {
System.err.println("客戶端異常: " + e.getMessage());
}
}
}
關鍵點深度解析
1. 字符編碼統一
- 雙端強制指定 StandardCharsets.UTF_8,避免平台默認編碼不一致導致亂碼。
- 使用
BufferedReader.readLine() / PrintWriter.println() 實現換行符分幀,簡單易用。
2. 線程模型演進
| 模型 | 代碼修改 | 適用場景 | 缺點 |
| 主循環 + 新線程 | 示例代碼 | 連接數 < 500、低併發 | 線程創建銷毀開銷大、OOM 風險 |
| 固定線程池 | ExecutorService pool = Executors.newFixedThreadPool(100); pool.execute(() -> handle(client)); | 中等併發、請求處理快 | 阻塞任務佔滿線程池導致拒絕服務 |
| NIO 選擇器 | Single Thread + Selector (Netty) | 高併發、長連接、海量連接 | 代碼複雜,建議直接用 Netty |
3. TCP 粘包/拆包與應用層協議
TCP 是字節流,無消息邊界。常見三種分幀策略:
- 定長:每條報文固定長度,不足補位。
- 分隔符:換行符
\n、\r\n 或特殊字符(示例採用)。
- 長度前綴:4 字節長度 + 體(推薦二進制協議)。
4. 心跳與空閒檢測
// Server 端:設置讀超時,超過 60s 無數據視為掉線
socket.setSoTimeout(60_000); // read() 超時拋 SocketTimeoutException
// 更優雅:Netty IdleStateHandler
// pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS));
// @Override userEventTriggered: IdleStateEvent.READER_IDLE -> close
5. 優雅關閉
// 雙向關閉協議
out.println("BYE"); // 發送結束標記
out.flush();
socket.shutdownOutput(); // 發送 FIN,對端 readLine() 返回 null
String last = in.readLine(); // 讀取對端最後響應
socket.close();
生產環境建議:直接使用 Netty
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
- 事件驅動、零拷貝、內存池、內建編解碼器、SSL、HTTP/2、WebSocket
- 避免重複造輪子,專注業務邏輯
總結
阻塞 Socket 適合學習網絡基礎、低併發內部工具。掌握「流讀寫、編碼、分幀、線程模型、超時、關閉」六大核心點,即可舉一反三。高併發、長連接、複雜協議場景,請毫不猶豫選擇 Netty。
- 上一篇文章: Java 字符串字符頻率統計:TreeMap 實現、Stream API 一行碼、性能對比與最佳實踐
- 下一篇文章: Java 單例模式四大寫法深度對比:餓漢、靜態內部類、雙重檢查鎖、枚舉——含線程安全、序列化、反射攻擊防禦
Copyright ©
程式碼之家 All Rights Reserved