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 的方式。

相关推荐
Marktowin3 小时前
Mybatis-Plus更新操作时的一个坑
java·后端
赵文宇3 小时前
CNCF Dragonfly 毕业啦!基于P2P的镜像和文件分发系统快速入门,在线体验
后端
程序员爱钓鱼4 小时前
Node.js 编程实战:即时聊天应用 —— WebSocket 实现实时通信
前端·后端·node.js
Libby博仙4 小时前
Spring Boot 条件化注解深度解析
java·spring boot·后端
源代码•宸5 小时前
Golang原理剖析(Map 源码梳理)
经验分享·后端·算法·leetcode·golang·map
小周在成长5 小时前
动态SQL与MyBatis动态SQL最佳实践
后端
瓦尔登湖懒羊羊5 小时前
TCP的自我介绍
后端
小周在成长5 小时前
MyBatis 动态SQL学习
后端
子非鱼9215 小时前
SpringBoot快速上手
java·spring boot·后端