释义
例子:zhuanlan.zhihu.com/p/538772022
- 半包:寄快递东西太大,需要拆成几个包裹邮寄,收件人收到包裹,东西不全,这不全的包就是半包,(半包问题是指接收端只收到了部分数据,而非
- 拆包:分成几份的过程就是拆包
- 粘包:要往家里每个人都送礼物,为了节省运费,放在一起发,节省运费(粘包问题是指数据在传输时,在一条消息中读取到了另一条消息的部分数据
UDP 没有此问题,因为 UDP 有边界,有报文长度,而 TCP 是以流的形式传输的,没有报文长度
解决方案
-
固定长度边界:每次发送消息时指定每个消息的固定长度,不满固定长度时,用固定字符串填充,比如空格
- 缺点:需要提前得知消息的范围,设置的小的话接收大消息会有问题,如果设置大的话,小消息又需要用大量的数据填充
-
分隔符:以固定符号表示结尾
- 优点:简单,不会浪费空间
- 缺点:
- 需要对内容本身做处理,防止内容出现分隔符,所以需扫描一遍传输的数据将其转义
- 需要每个字节比较,比较耗时
-
固定长度 + 内容:比如协议规定固定 4 位存放内容长度
- 优点:可以根据固定长度精准定位,也不用扫描转义字符
- 缺点:设计比较困难,大了浪费空间,毕竟每个报文都需要带长度,小了可能不够用
分隔符示例
public static void main(String[] args) {
// 分隔符解决方案,以 \n 为分隔符
ByteBuffer source = ByteBuffer.allocate(30);
source.put("hello,world\nI'm zhang san\nHo".getBytes());
split(source);
source.put("w are ".getBytes());
split(source);
source.put("you?\n".getBytes());
split(source);
}
private static void split(ByteBuffer source) {
// 读
source.flip();
// 循环到最后需要读取的一个位置
for (int i = 0; i < source.limit(); i++) {
// 找到分隔符,用get(i)是因为不会改变position,一次性初始化够ByteBuffer的空间
if (source.get(i) == '\n') {
// 完整的一条信息的长度
int length = i - source.position();
ByteBuffer target = ByteBuffer.allocate(length);
for (int j = 0; j < length; j++) {
target.put(source.get());
}
System.out.print(covertString(target));
}
}
// 从未读的地方开始重新写
source.compact();
}
public static String covertString(ByteBuffer byteBuffer) {
return new String(byteBuffer.array(), 0, byteBuffer.limit(), StandardCharsets.UTF_8);
}