TCP 是位元組流協議,無訊息邊界。「資料不完整」是常態,而非異常。發送端一次 write 的資料,接收端可能需多次 read 才能讀完;或多次 write 合併成一次 read(黏包)。
// 發送端
byte[] payload = "Hello".getBytes(StandardCharsets.UTF_8);
ByteBuffer header = ByteBuffer.allocate(4).putInt(payload.length);
out.write(header.array());
out.write(payload);
out.flush();
// 接收端
byte[] lenBytes = new byte[4];
readFully(in, lenBytes, 4);
int len = ByteBuffer.wrap(lenBytes).getInt();
byte[] body = new byte[len];
readFully(in, body, len);
// 發送端
out.write(("Hello\n").getBytes(StandardCharsets.UTF_8));
out.flush();
// 接收端 - 使用 BufferedReader
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String line = reader.readLine(); // 讀到 \n 為止
// 通用讀取工具方法
public static void readFully(InputStream in, byte[] buf, int offset, int len)
throws IOException {
while (len > 0) {
int r = in.read(buf, offset, len);
if (r == -1) throw new EOFException();
offset += r;
len -= r;
}
}
// 訊息協定類
public class TcpMessage {
public static final int HEADER_SIZE = 4;
public static void send(OutputStream out, byte[] body) throws IOException {
ByteBuffer bb = ByteBuffer.allocate(HEADER_SIZE + body.length);
bb.putInt(body.length);
bb.put(body);
out.write(bb.array());
out.flush();
}
public static byte[] receive(InputStream in) throws IOException {
byte[] header = new byte[HEADER_SIZE];
readFully(in, header, 0, HEADER_SIZE);
int bodyLen = ByteBuffer.wrap(header).getInt();
if (bodyLen < 0 || bodyLen > 10_000_000)
throw new IOException("Invalid length: " + bodyLen);
byte[] body = new byte[bodyLen];
readFully(in, body, 0, bodyLen);
return body;
}
}
| 誤區 | 事實 |
|---|---|
| 「TCP 會亂序」 | TCP 保證有序交付,「不完整」是流切分問題 |
| 「睡眠幾毫秒再讀」 | 不可靠,網路抖動會導致失敗 |
| 「available() > 0 才讀」 | available() 不可靠,阻塞 read 才是正解 |
| 「開大緩衝區一次讀完」 | 無法解決黏包/拆包,且大訊息會 OOM |
// Netty Pipeline 配置
pipeline.addLast(
new LengthFieldBasedFrameDecoder(1024*1024, 0, 4, 0, 4),
new LengthFieldPrepender(4),
new StringDecoder(StandardCharsets.UTF_8),
new MyBusinessHandler()
);
readFully 循環讀取直到滿足預期長度socket.setSoTimeout(ms)