TCP 是字節流協議,不保證應用層消息邊界。InputStream.read() 在以下情況阻塞:
必須在應用層定義消息邊界,或採用超時/關閉機制打破阻塞。
原理:發送端先發送固定長度頭部(如 4 字節 int),聲明消息體長度。接收端先讀頭部,再精確讀取指定字節數。
// 發送端
byte[] body = "Hello".getBytes(StandardCharsets.UTF_8);
ByteBuffer header = ByteBuffer.allocate(4).putInt(body.length);
out.write(header.array());
out.write(body);
out.flush();
// 接收端
byte[] lenBytes = new byte[4];
in.readFully(lenBytes); // 精確讀 4 字節
int length = ByteBuffer.wrap(lenBytes).getInt();
byte[] body = new byte[length];
in.readFully(body); // 精確讀消息體
優點:不依賴超時、支持長連接、解決粘包/拆包、高效可靠。HTTP Keep-Alive 通過 Content-Length 或 Transfer-Encoding: chunked 實現同一邏輯。
Socket socket = new Socket(host, port);
socket.setSoTimeout(5000); // 5 秒無數據拋出 SocketTimeoutException
try {
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) {
sb.append(new String(buf, 0, len, StandardCharsets.UTF_8));
if (len < buf.length) break; // 讀取完畢(對端關閉或單次讀取未滿緩衝區)
}
} catch (SocketTimeoutException e) {
// 處理超時:重試、心跳檢測、關閉連接
} catch (IOException e) {
// 處理連接斷開
}
關鍵點:
setSoTimeout 必須在 Socket 級別設置,InputStream 級別無效SocketTimeoutException 區分超時與對端關閉(read() == -1)// 發送端
out.write(data);
out.flush();
socket.shutdownOutput(); // 發送 FIN,標記數據發送完畢
// 接收端
while ((len = in.read(buf)) != -1) { // read 返回 -1 表示對端關閉輸出流
// 處理數據
}
// 此時可繼續發送響應(半關閉),或 socket.close() 全關閉
適用場景:一次性文件傳輸、短連接 HTTP 1.0、無需雙向持續通信的場景。
不適用:長連接、雙工通信、RPC 調用鏈(關閉連接成本高、無法復用)。
// 發送端
out.write("command|param1|param2\n"); // 換行符作為定界符
out.flush();
// 接收端
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) { // 遇到 \n 返回
parseCommand(line);
}
缺點:數據中不能包含定界符,需轉義;二進制數據不適用。
| 方案 | 適用場景 | 複雜度 | 長連接支持 | 性能 |
|---|---|---|---|---|
| 長度前綴 | 高性能 RPC、IM、遊戲、物聯網 | 中 | ★★★★★ | 最高 |
| 超時機制 | 請求響應、簡單客戶端、兼容舊協議 | 低 | ★★★☆☆ | 中 |
| 關閉連接 | 文件下載、一次性任務、HTTP 1.0 | 低 | ☆☆☆☆☆ | 高(但連接開銷大) |
| 定界符 | 文本協議、調試接口、簡單設備通信 | 低 | ★★★☆☆ | 中 |
ByteBuf 避免 byte[] 拷貝,支持零拷貝shutdownOutput → 讀取 -1 → close理解阻塞本質,選擇合適的邊界劃分策略,是構建高可靠網絡應用的核心功力。