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 可以添加 DelimiterBasedFrameDecoder 和 StringDecoder 的ChannelHandler 的方式。

相关推荐
金銀銅鐵3 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路4 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x4 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn6 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈6 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780517 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123457 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒7 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记7 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598787 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端