2. 粘包拆包问题演示

好,咱们别光说不练。这一章我带你亲手搭一个TCP服务端和客户端,发几条消息看看效果。你很快就会发现——嗯,TCP这玩意儿,确实会“自作主张”。

2.1 先搭个最简单的TCP服务端

我习惯用Java写网络程序,因为NIO和BIO的API都比较直观。下面这个服务端,说白了就是监听一个端口,收到啥就打印啥。

import java.io.*;
import java.net.*;

public class TcpServer {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("服务端已启动,端口:8888");

        Socket socket = serverSocket.accept();
        InputStream in = socket.getInputStream();
        byte[] buffer = new byte[1024];
        int len;

        while ((len = in.read(buffer)) != -1) {
            String msg = new String(buffer, 0, len);
            System.out.println("收到消息:" + msg);
        }

        socket.close();
        serverSocket.close();
    }
}

这段代码很简单。服务端一次最多读1024字节,然后直接打印。你想想看,如果客户端连续发多条消息,服务端会怎么处理?

2.2 再写个客户端,连续发消息

客户端这边,我让它连续发5条消息。每条消息之间不加任何延迟,模拟高并发场景。

import java.io.*;
import java.net.*;

public class TcpClient {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("127.0.0.1", 8888);
        OutputStream out = socket.getOutputStream();

        String[] messages = {"Hello", "World", "TCP", "Sticky", "Packet"};

        for (String msg : messages) {
            out.write(msg.getBytes());
            System.out.println("发送消息:" + msg);
        }

        out.close();
        socket.close();
    }
}

运行一下,先启动服务端,再启动客户端。你猜服务端会打印出什么?

2.3 观察现象:粘包来了

我实际跑了一次,服务端输出是这样的:

收到消息:HelloWorldTCPStickyPacket

看到了吗?5条消息粘在一起了!这就是典型的粘包现象。客户端明明发了5次write,服务端一次read就把所有内容全收走了。

为什么会这样?

TCP是流式协议,它不关心你分了几次write。它只负责把字节流从A端送到B端。如果发送频率高、数据量小,TCP会把多个小包合并成一个大的TCP段发送,这就是粘包。

2.4 换个场景:拆包也出现了

那拆包呢?我换个写法。让客户端一次发一个很大的消息,比如超过1024字节。

// 客户端发送一个2000字节的消息
String bigMsg = "A".repeat(2000);
out.write(bigMsg.getBytes());

服务端输出:

收到消息:AAAA...(1024个A)
收到消息:AAAA...(976个A)

一条消息被拆成了两段接收。这就是拆包。因为服务端的缓冲区只有1024字节,一次读不完,剩下的下次再读。

注意:粘包和拆包不是互斥的。实际项目中,它们经常同时出现。比如你发了10条消息,可能前3条粘在一起,第4条被拆成两半,第5、6条又粘在一起……乱得很。

2.5 用一张图看清本质

下面这张SVG图,我画了TCP传输的完整流程。你可以看到应用层的数据包,到了TCP层是怎么被“揉捏”的。

TCP粘包拆包示意图 客户端应用层 发送5条小消息 客户端TCP层 合并成1个TCP段 网络传输 服务端TCP层 收到1个TCP段 服务端应用层 读到1条大消息(粘包) 应用层5条消息 → TCP层合并为1个段 → 应用层收到1条

这张图展示的是粘包场景。拆包就是反过来——应用层1条大消息,TCP层拆成多个段发送,应用层分多次收到。

2.6 我在项目中踩过的坑

我曾经在一个即时通讯项目里,客户端上报位置信息,每秒发一次。服务端那边收到的数据经常乱掉——有时候两条位置粘在一起,解析出来经纬度完全不对。后来排查才发现,就是典型的粘包问题。

还有一次做文件传输,一个10MB的文件,服务端收了一部分就卡住了。我一开始以为是网络断了,调试了半天才发现是拆包导致接收不完整,程序没处理好边界。

我的建议:写TCP程序时,永远不要假设“一次read就能拿到完整的一条消息”。你必须在应用层自己定义消息边界。怎么定义?下一章我会详细讲。

2.7 小结一下

这一章我们通过实际代码,亲眼看到了粘包和拆包的现象。说白了,TCP只保证字节流的顺序和可靠性,但它不保证消息的边界。你发10次write,它可能1次read就收完;你发1次write,它也可能分10次read才收完。

记住这个核心结论:TCP是流,不是消息。所有基于TCP的应用层协议,都必须自己处理消息边界问题。

可可精品资料库