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层是怎么被“揉捏”的。
这张图展示的是粘包场景。拆包就是反过来——应用层1条大消息,TCP层拆成多个段发送,应用层分多次收到。
2.6 我在项目中踩过的坑
我曾经在一个即时通讯项目里,客户端上报位置信息,每秒发一次。服务端那边收到的数据经常乱掉——有时候两条位置粘在一起,解析出来经纬度完全不对。后来排查才发现,就是典型的粘包问题。
还有一次做文件传输,一个10MB的文件,服务端收了一部分就卡住了。我一开始以为是网络断了,调试了半天才发现是拆包导致接收不完整,程序没处理好边界。
我的建议:写TCP程序时,永远不要假设“一次read就能拿到完整的一条消息”。你必须在应用层自己定义消息边界。怎么定义?下一章我会详细讲。
2.7 小结一下
这一章我们通过实际代码,亲眼看到了粘包和拆包的现象。说白了,TCP只保证字节流的顺序和可靠性,但它不保证消息的边界。你发10次write,它可能1次read就收完;你发1次write,它也可能分10次read才收完。
记住这个核心结论:TCP是流,不是消息。所有基于TCP的应用层协议,都必须自己处理消息边界问题。
可可精品资料库