`InputStream.read()` 在流未關閉且無資料時永久阻塞。長連接雙方需協商「訊息邊界」。
HTTP `Content-Length`、自定義協議 `Header(4 bytes len) + Body`。讀取端先讀長度,再精確讀取 `len` 位元組,`read` 不會阻塞在訊息中間。
// 發送端
ByteBuffer buf = ByteBuffer.allocate(4 + body.length);
buf.putInt(body.length).put(body);
socket.getOutputStream().write(buf.array());
// 接收端
DataInputStream in = new DataInputStream(socket.getInputStream());
int len = in.readInt(); // 阻塞直到讀滿 4 字節
byte[] body = new byte[len];
in.readFully(body); // 阻塞直到讀滿 len 字節
socket.setSoTimeout(5000); // 5 秒無資料拋 SocketTimeoutException
try {
while (true) {
int n = in.read(buf);
if (n == -1) break; // 流關閉
process(buf, n);
}
} catch (SocketTimeoutException e) {
// 心跳檢查、重發、或判定連線異常
}
注意:超時僅適用於「預期持續有資料」場景;若業務長時間無資料 (如推送),需應用層心跳。
發送端寫完資料呼叫 `socket.shutdownOutput()` (發送 FIN),接收端 `read()` 返回 `-1` 正常結束。適用於「請求-回應」單向傳輸,**不適用於雙向長連接**。
// 以 \n 或 \r\n 結束
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = br.readLine()) != null) { // 內部處理緩衝與分隔符
process(line);
}
缺點:資料內含分隔符需轉義;不適用二進制資料。
| 場景 | 推薦方案 |
|---|---|
| 高效能二進制長連接 (遊戲、IM) | 長度前綴 + 心跳 + setSoTimeout(心跳間隔*2) |
| HTTP/REST 短連接 | Content-Length / Chunked + setSoTimeout |
| 簡單文本協議 (Telnet-like) | 行分隔符 + BufferedReader |
| 單次請求回應 | shutdownOutput + read 至 -1 |
public class LengthPrefixedClient {
private final Socket socket;
private final DataInputStream in;
private final DataOutputStream out;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
public LengthPrefixedClient(String host, int port) throws IOException {
socket = new Socket(host, port);
socket.setSoTimeout(10000); // 10 秒超時 (心跳間隔 5 秒)
in = new DataInputStream(socket.getInputStream());
out = new DataOutputStream(socket.getOutputStream());
// 發送心跳
scheduler.scheduleAtFixedRate(() -> {
try { send(HEARTBEAT); } catch (IOException e) { shutdown(); }
}, 5, 5, TimeUnit.SECONDS);
}
public void send(byte[] payload) throws IOException {
synchronized (out) {
out.writeInt(payload.length);
out.write(payload);
out.flush();
}
}
public byte[] receive() throws IOException {
int len = in.readInt();
byte[] data = new byte[len];
in.readFully(data);
return data;
}
public void shutdown() {
scheduler.shutdownNow();
closeQuietly(in); closeQuietly(out); closeQuietly(socket);
}
}