Skip to content

在网络编程的世界里,很多初学者都会掉进一个“直觉陷阱”:认为我发了一个 hello,对方就理应完整地收到一个 hello。

TCP 并不理解业务上的“话语”,它只是一条永不停歇的传送带,机械地搬运着每一个字节。这种“无边界”的特性,直接导致了我们在开发高性能通信框架(如 Netty)时必须面对的头号难题:粘包与半包。

有一个服务端和客户端的代码如下

java
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket ss=new ServerSocket(9000);
        while (true) {
            Socket s = ss.accept();
            InputStream in = s.getInputStream();
            // 这里这么写,有没有问题
            byte[] arr = new byte[4];
            while(true) {
                int read = in.read(arr);
                // 这里这么写,有没有问题
                if(read == -1) {
                    break;
                }
                System.out.println(new String(arr, 0, read));
            }
        }
    }
}
java
public class Client {
    public static void main(String[] args) throws IOException {
        Socket max = new Socket("localhost", 9000);
        OutputStream out = max.getOutputStream();
        out.write("hello".getBytes());
        out.write("world".getBytes());
        out.write("你好".getBytes());
        max.close();
    }
}

输出的内容是:

hell
owor
ld�
�好

缓冲区大小与中文字节:缓冲区 byte[] arr = new byte[4] 只有 4 个字节,而我们的数据流是连续的。

为什么会出现这种输出?

我们逐行分析服务端的读取过程:

  1. 第一轮读取:从 Socket 缓冲区读取了 4 个字节,内容是 hell。此时 hello 还没读完。
  2. 第二轮读取:读取接下来的 4 个字节。这包括了 hello 剩下的 o 以及 world 的前三个字母 wor。这就是所谓的粘包(Sticky Package)——两条逻辑上独立的消息在物理读取时被“粘”在了一个缓冲区里。
  3. 第三轮读取:读取 world 剩下的 ld,以及“你”字的前两个字节(UTF-8 中一个汉字占 3 字节)。此时,缓冲区内容为 [l, d, 你1, 你2]。由于“你”的字节被截断了,new String 无法正确解码,于是出现了乱码 ``。这就是半包(Half Package)——一条完整的逻辑消息被拆分成了多次读取。
  4. 第四轮读取:读取“你”字的最后一个字节和“好”字的三个字节。

核心结论:TCP 是“流”

这种现象的根本原因在于:TCP 是一个面向字节流的协议,它没有消息边界

  • 半包(拆包):是因为接收方的缓冲区大小限制、或者 MTU 限制,导致一条完整的消息被拆分。
  • 粘包:是因为发送方为了提高效率(Nagle 算法),或者接收方处理速度跟不上,导致多条消息在缓冲区中堆积,被一次性读取。

对于 TCP 来说,它只负责把 A 发送的字节序列原封不动地传给 B,它并不关心这些字节哪些属于“消息 1”,哪些属于“消息 2”。

解决消息边界的三种主流方案

在实际开发中(如使用 Netty),我们通常通过以下方式来“识破”消息边界:

1. 固定长度(Fixed Length)

发送方约定每条消息必须是固定的 10 个字节,不够的用空格补齐。

  • 优点:解析简单。
  • 缺点:浪费带宽。

2. 分隔符(Delimiter Based)

每条有效消息的结尾加一个特定字符(如 \n)。

  • 优点:灵活。
  • 缺点:消息内容中不能包含分隔符,否则需要转义,比较麻烦。

3. 长度域(Length Field Based)—— 最推荐

在消息头部用 4 个字节存储这段消息的总长度,后面紧跟消息正文。

  • 工作原理
    1. 先读 4 个字节,算出长度 (N)。
    2. 只有当缓冲区积累够 (N) 个字节时,才认为是一条完整的消息。
  • 优点:精准、高效,Netty 中的 LengthFieldBasedFrameDecoder 就是基于此原理实现的。