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

相关推荐
JustHappy4 小时前
古法编程秘籍(二):什么是代码模块化?别背概念,把房间收拾明白就够了
前端·后端
小江的记录本4 小时前
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
java·前端·jvm·后端·python·spring·面试
IT_陈寒8 小时前
Python闭包里藏的这个坑,差点让我加班到凌晨
前端·人工智能·后端
IT_陈寒8 小时前
Java注解空指针?这个坑我踩得莫名其妙
前端·人工智能·后端
土狗TuGou8 小时前
SQL内功笔记 · 第8篇:事务的四大特性与隔离级别
数据库·笔记·后端·sql·mysql·oracle
ZengLiangYi8 小时前
React Query + REST API 最佳实践
javascript·后端·react.js
星浩AI9 小时前
项目实战:合同智能审批 · LangGraph + HITL 人机协同方案 [有源码]
后端·langchain·agent
JavaGuide9 小时前
Codex 接入第三方模型 DeepSeek、GLM、Kimi 教程:CC-Switch 和 Codex++ 两种方案对比
后端·ai编程
ZengLiangYi9 小时前
Fastify 加 Electron:把 Web 服务嵌进桌面应用
前端·javascript·后端
李白你好9 小时前
页面资产梳理 · 技术指纹识别 · Spring 端点探测
java·后端·spring