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

相关推荐
百成Java12 分钟前
基于springboot的旅游网站
java·spring boot·后端·mysql·spring·智能家居·旅游
Pandaconda33 分钟前
【计算机网络 - 基础问题】每日 3 题(二十三)
开发语言·网络·笔记·后端·计算机网络·面试·职场和发展
IT杨秀才35 分钟前
Go语言的Context妙用
后端·go
野生派蒙36 分钟前
IDEA 关闭自动补全功能(最新版本)
java·开发语言·ide·后端·学习·intellij-idea
为java添砖加瓦1 小时前
【读写分离?聊聊Mysql多数据源实现读写分离的几种方案】
java·数据库·spring boot·后端·mysql·spring·mybatis
2401_857617621 小时前
Spring Boot技术:构建高效网上购物平台
java·spring boot·后端
Pandaconda2 小时前
【计算机网络 - 基础问题】每日 3 题(二十五)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展
河北小田2 小时前
局部变量成员变量、引用类型、this、static
java·后端·程序员
Tony Bai2 小时前
“类型名称”在Go语言规范中的演变
java·开发语言·后端·golang
bug菌¹2 小时前
滚雪球学SpringCloud[1.3]:SpringCloud环境搭建
后端·spring·spring cloud