粘包、半包和拆包

265 阅读2分钟

释义

例子:zhuanlan.zhihu.com/p/538772022

  • 半包:寄快递东西太大,需要拆成几个包裹邮寄,收件人收到包裹,东西不全,这不全的包就是半包,(半包问题是指接收端只收到了部分数据,而非
  • 拆包:分成几份的过程就是拆包
  • 粘包:要往家里每个人都送礼物,为了节省运费,放在一起发,节省运费(粘包问题是指数据在传输时,在一条消息中读取到了另一条消息的部分数据

UDP 没有此问题,因为 UDP 有边界,有报文长度,而 TCP 是以流的形式传输的,没有报文长度

解决方案

  1. 固定长度边界:每次发送消息时指定每个消息的固定长度,不满固定长度时,用固定字符串填充,比如空格

    • 缺点:需要提前得知消息的范围,设置的小的话接收大消息会有问题,如果设置大的话,小消息又需要用大量的数据填充
  2. 分隔符:以固定符号表示结尾

    • 优点:简单,不会浪费空间
    • 缺点:
      • 需要对内容本身做处理,防止内容出现分隔符,所以需扫描一遍传输的数据将其转义
      • 需要每个字节比较,比较耗时
  3. 固定长度 + 内容:比如协议规定固定 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);
}