TCP粘包和拆包

背景

可以通过图解对TCP粘包和拆包进行说明,具体内容如下图

设想一下,客户端向服务端分别发送了两个数据包,名为 D1 和 D2。鉴于服务端每次读取的字节数量并不确定,于是就可能出现以下五种情形:

  1. 服务端通过两次读取操作,获取到了两个相互独立的数据包,即 D1 和 D2,在此过程中不存在粘包或者拆包现象。
  2. 服务端一次性接收了两个数据包,此时 D1 和 D2 粘合在了一起,这种情况被称作 TCP 粘包
  3. 服务端分两次进行读取,第一次读取到了完整的 D1 包以及 D2 包的部分内容,第二次则读取到了 D2 包的剩余内容,这便被称为 TCP 拆包
  4. 服务端同样分两次读取,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1 包的剩余内容 D1_2 以及 D2 包的完整内容。
  5. 如果滑动窗口特别小,那么可能 D1 D2 包就会变成多个拆分的包

总之,除了第一种情况是正常的,其余都是不正常的。

解决策略

鉴于底层的 TCP 无法理解上层的业务数据,因而在底层层面无法确保数据包不会被拆分以及重组。这个问题唯有通过上层的应用协议栈设计来加以解决。

依据业界主流协议的解决方案,可以总结如下:

  1. 设定消息为定长,例如规定每个报文的大小固定为 200 字节,若长度不足,则用空位填补空格。
  2. 在包尾添加回车换行符进行分割,就像 FTP 协议那样。
  3. 把消息划分为消息头和消息体两部分,在消息头中包含能够表示消息总长度(或者消息体长度)的字段,通常的设计思路是在消息头的第一个字段使用 int32 来表示消息的总长度。
  4. 更为复杂的应用层协议。

评估如下:

  1. 第一种方案,在具体业务中,日志的长度不是固定的,要是设置的小了,可能会拆包,要是设置的大可以填补,但是具体我要设置多大呢?1K,1M 么?
  2. 第二种方案,要是根据特殊符号进行分割,那日志中刚好有对应的特殊符号,要怎么处理呢?
  3. 第三种方案,可以封装成一个合适的消息对象,然后再进行交互,这个方式还是可以的。
  4. 第四种方案,直接使用更上层的应用协议,其实就是不用自己再去实现第三种方案,上层协议已经帮你实现了,在这里就不讨论了。

那么我们就看下只用 TCP 协议的话,如何解决粘包和拆包的问题吧

方案

我选择添加一个特殊的头部信息进行处理,如果不是对应的头部信息,将不进行处理,其中消息内容如下:

描述 长度(位) 描述
标识符 2 确认该信息是否是自定义的信息
长度 4 表示这个消息的长度是多少
数据 n 数据
发送端添加头部信息(Client)

在调用 writeAndFlush时,对数据 body 添加头部信息代码如下所示:

scss 复制代码
private static byte[] appendCustomPrefix(byte[] body) {
    byte[] bodyLength = intToByteArray(body.length);
    byte[] result = new byte[TCP_PREFIX.length + bodyLength.length + body.length];
    //添加自定义标识前缀
    System.arraycopy(TCP_PREFIX, 0, result, 0, TCP_PREFIX.length);
    //添加长度前缀
    System.arraycopy(bodyLength, 0, result, TCP_PREFIX.length, bodyLength.length);
    //添加数据
    System.arraycopy(body, 0, result, TCP_PREFIX.length + bodyLength.length, body.length);
    return result;
}

public static final byte[] TCP_PREFIX = new byte[]{(byte) '0', (byte) '1'};

private static byte[] intToByteArray(int value) {
    return new byte[]{(byte) ((value >> 24) & 0xFF), (byte) ((value >> 16) & 0xFF), (byte) ((value >> 8) & 0xFF), (byte) (value & 0xFF)};
}
接收端去掉头部信息(Server)

在 TcpServer 端的 ChannelHandler 中decode 数据时,添加如下的逻辑,去掉头部信息,代码如下:

ini 复制代码
public class TioDelimiterBasedFrameDecoder {

    public List<byte[]> unpack(byte[] message) {
        List<byte[]> result = new ArrayList<>();
        if (message == null || message.length < 6) {
            return addMessage(message, 0, 0, result);
        }
        return unpack(message, 0, result);
    }

    private List<byte[]> unpack(byte[] message, int start, List<byte[]> result) {
        if (start >= message.length) {
            return result;
        }
        if (!isCustomMessage(message, start)) {
            return addMessage(message, start, message.length, result);
        }
        start += TCP_PREFIX.length;
        int length = byteArrayToInt(message, start);
        start += 4;
        int end = start + length;
        if (end > message.length) {
            return addMessage(message, start, message.length, result);
        }
        addMessage(message, start, end, result);
        unpack(message, end, result);
        return result;
    }

    private boolean isCustomMessage(byte[] message, int start) {
        for (int i = 0; i < TCP_PREFIX.length; i++) {
            if (message[start + i] != TCP_PREFIX[i]) {
                return false;
            }
        }
        return true;
    }

    private List<byte[]> addMessage(byte[] message, int start, int end, List<byte[]> result) {
        if (start == 0 || end == 0) {
            result.add(message);
        } else {
            byte[] data = new byte[end - start];
            System.arraycopy(message, start, data, 0, end - start);
            result.add(data);
        }
        return result;
    }
    
   
    private int byteArrayToInt(byte[] bytes, int start) {
        int value = 0;
        for (int i = start; i < start + 4; i++) {
            value = (value << 8) | (bytes[i] & 0xFF);
        }
        return value;
    }

}

其他方案

如果是使用的 netty 可以添加 DelimiterBasedFrameDecoderStringDecoder 的ChannelHandler 的方式。

相关推荐
zzb15804 小时前
RAG from Scratch-优化-query
java·数据库·人工智能·后端·spring·mybatis
必胜刻5 小时前
RESTful 基础:资源、路径与方法对应关系详解
后端·restful
XPoet5 小时前
AI 编程工程化:Hook——AI 每次操作前后的自动检查站
前端·后端·ai编程
J2虾虾5 小时前
在SpringBoot中使用Druid
java·spring boot·后端·druid
程序员小假6 小时前
为什么要有 time _wait 状态,服务端这个状态过多是什么原因?
java·后端
qwert10377 小时前
跨域问题解释及前后端解决方案(SpringBoot)
spring boot·后端·okhttp
90后的晨仔7 小时前
OpenClaw Windows 完整安装指南
后端
IT_陈寒8 小时前
Vue组件复用率提升300%?这5个高阶技巧让你的代码焕然一新!
前端·人工智能·后端
beata9 小时前
Spring Boot基础-2:Spring Boot 3.x 起步依赖(Starter)深度拆解:为什么引入一个依赖就够了?
spring boot·后端
享棣9 小时前
Win11 安装 Nacos 2.0.4 完整版文档 文档说明
后端