知识点编号:012
难度等级:⭐⭐⭐(必掌握)
面试频率:🔥🔥🔥🔥🔥
🎯 一句话总结
TCP把数据当成字节流,不保留消息边界,导致粘包拆包!📦
🤔 什么是粘包和拆包?
TCP的特性
TCP = 字节流协议
把数据看作连续的字节流,没有消息边界!
发送方:
send("Hello")
send("World")
接收方可能收到:
1️⃣ "HelloWorld" (粘包)
2️⃣ "Hel" + "loWorld" (拆包)
3️⃣ "Hello" + "World" (正常)
🎭 粘包示例
发送方:
第1次:send("消息1")
第2次:send("消息2")
第3次:send("消息3")
接收方recv(100)可能收到:
"消息1消息2消息3" ← 三个消息粘在一起了!
无法区分消息边界!
不知道哪里是消息1的结尾,哪里是消息2的开始!
🎭 拆包示例
发送方:
send("这是一条很长很长很长的消息...")(1000字节)
接收方:
第1次recv(100):"这是一条很长很长"
第2次recv(100):"很长的消息..."
一条消息被拆成多次接收!
💡 为什么会粘包拆包?
原因1:Nagle算法(粘包)
Nagle算法:
- 优化网络效率
- 将小数据包合并后发送
- 减少网络传输次数
示例:
send("A") |
send("B") |→ 合并成 "ABC" 一起发
send("C") |
结果:接收方收到 "ABC"(粘包)
原因2:TCP缓冲区(粘包)
发送缓冲区:
send("A") → [A]
send("B") → [AB]
send("C") → [ABC]
TCP层一次性发送 "ABC"
接收方一次性收到 "ABC"(粘包)
原因3:接收缓冲区大小(拆包)
发送:1000字节数据
接收缓冲区:只有500字节
第1次读:500字节
第2次读:500字节
一条消息被拆成两次接收(拆包)
原因4:MSS限制(拆包)
MSS(Maximum Segment Size):
通常1460字节
发送5000字节:
TCP自动拆分:
包1:1460字节
包2:1460字节
包3:1460字节
包4:620字节
接收方分多次收到(拆包)
🔧 解决方案
方案1:固定长度
// 每条消息固定100字节
byte[] buffer = new byte[100];
while (true) {
int len = inputStream.read(buffer);
if (len == 100) {
String message = new String(buffer);
System.out.println("收到消息:" + message);
}
}
优点:简单
缺点:浪费空间(短消息要填充)
方案2:特殊分隔符
// 使用\n作为分隔符
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream)
);
while (true) {
String message = reader.readLine(); // 读到\n为止
System.out.println("收到消息:" + message);
}
// 发送时
PrintWriter writer = new PrintWriter(outputStream, true);
writer.println("消息1"); // 自动加\n
writer.println("消息2");
优点:简单,节省空间
缺点:消息内容不能包含分隔符
方案3:消息头+长度(推荐)
// 协议格式:[长度(4字节)][消息内容]
// 发送消息
public void sendMessage(OutputStream out, String msg) throws IOException {
byte[] data = msg.getBytes("UTF-8");
// 写入长度(4字节)
DataOutputStream dos = new DataOutputStream(out);
dos.writeInt(data.length);
// 写入内容
dos.write(data);
dos.flush();
}
// 接收消息
public String receiveMessage(InputStream in) throws IOException {
DataInputStream dis = new DataInputStream(in);
// 读取长度
int length = dis.readInt();
// 读取内容
byte[] data = new byte[length];
dis.readFully(data); // 确保读满length字节
return new String(data, "UTF-8");
}
// 使用示例
sendMessage(out, "Hello");
sendMessage(out, "World");
String msg1 = receiveMessage(in); // "Hello"
String msg2 = receiveMessage(in); // "World"
优点:
- 可以处理任意内容
- 长度明确
- 不浪费空间
缺点:
- 需要约定协议格式
方案4:使用Netty(工业级)
// Netty自带粘包拆包解决方案
// 1. 固定长度
pipeline.addLast(new FixedLengthFrameDecoder(100));
// 2. 分隔符
pipeline.addLast(new DelimiterBasedFrameDecoder(1024,
Delimiters.lineDelimiter()));
// 3. 长度字段
pipeline.addLast(new LengthFieldBasedFrameDecoder(
1024, // 最大帧长度
0, // 长度字段偏移量
4, // 长度字段长度
0, // 长度调整
4 // 跳过的字节数
));
// 业务Handler
pipeline.addLast(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
// msg已经是完整的一条消息了!
String message = msg.toString(CharsetUtil.UTF_8);
System.out.println("收到:" + message);
}
});
🐛 常见面试题
Q1:什么是TCP粘包和拆包?怎么解决?
答案:
粘包:多条消息合并成一条
拆包:一条消息拆分成多条
原因:
1. TCP是字节流协议,无消息边界
2. Nagle算法合并小包
3. TCP缓冲区
4. MSS限制
解决方案:
1. 固定长度
- 每条消息固定字节数
- 简单但浪费空间
2. 特殊分隔符
- 如\n、\r\n
- 简单但内容不能包含分隔符
3. 消息头+长度(推荐)
- 消息头包含长度信息
- [长度][内容]
- 灵活,工业标准
4. 使用框架
- Netty提供现成解决方案
- LengthFieldBasedFrameDecoder
项目经验:
我们项目使用长度+内容的方案
协议格式:[4字节长度][消息内容]
完美解决了粘包拆包问题
Q2:UDP有粘包拆包问题吗?
答案:
UDP没有粘包问题!
原因:
1. UDP是数据报协议
- 有明确的消息边界
- 每个send()对应一个独立的数据报
2. UDP保证边界
- send("A") → 一个数据报
- send("B") → 另一个数据报
- 接收方一定是分开收到
3. UDP可能丢包、乱序,但不会粘包
但是:
UDP有大小限制(通常64KB)
超过限制会被拆分(IP层分片)
但这是IP层的行为,不是UDP的粘包
结论:
TCP有粘包拆包 → 需要自己处理边界
UDP无粘包 → 天然有边界,但可能丢包
🎓 总结
粘包拆包的关键点:
- 原因:TCP是字节流,无消息边界
- 粘包:多条消息合并
- 拆包:一条消息拆分
- 解决:固定长度、分隔符、长度字段
记忆口诀:
TCP字节流无边界 📦
粘包拆包常出现 ⚠️
固定长度或分隔 ✂️
长度字段最推荐 ✅
文档创建时间:2025-10-31