Netty自定义协议的粘包和拆包处理

TCP粘包和拆包

熟悉TCP编程的可能都知道,无论是服务端还是客户端,当我们读取或者是发送消息的时候,都需要考虑TCP底层的粘包和拆包机制。
TCP是个“流”协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,它们是连成一片的,其间并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实现情况进行包的拆分,所以在业务上认为,一个完整的包可能会被TCP拆成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。
(1)消息定长,例如每个报文的大小为固定长度,如果不够,空格补齐;
(2)在包尾增加回车换行符进行分割,例如HTTP、FTP协议;
(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int来表示消息的总长度;
(4)更复杂的应用层协议

Netty ByteBuf

当我们进行数据传输的时候,往往需要使用到缓冲区,常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer
从功能角度而言,ByteBuffer完全可以满足NIO编程的需要,但是NIO编程过于复杂,也存在局限性。ByteBuffer长度固定,一旦分配完成,不可动态修改。

JDK ByteBuffer由于只有一个位置指针用于处理读写操作,因此每次读写的时候都需要额外调用flip(),否则功能将出错。

Netty ByteBuf提供了两个指针用于支持顺序读取和写入操作:readerIndex用于标识读取索引,writerIndex用于标识写入索引。两个位置指针将ByteBuf缓冲区分割成三个区域。
(1) readerIndex到writerIndex之间的空间为可读的字节缓冲区
(2) writerIndex到capacity之间为可写的字节缓冲区
(3) 0到readerIndex之间是已经读取过的缓冲区
这里写图片描述


最近在做一个项目,遇到了自定义协议的粘包和拆包的问题。服务端使用Netty与客户端进行交互,协议为客户端自定义的协议,协议大致如下。

整个数据包的结构

2*byte的消息头+数据长度+数据体

magic   |   magic   |   length   |   [data]

magic(byte): 0x46
length(int): data的长度
data(byte[]): data数组

data数组的结构

 key | length | data | key | length | data | key | length | data | key | length | data   

key: 键名
length: data的长度
data: 值


下面为自定义协议粘包和拆包处理的大致实现,用到了Netty ByteBuf的几个方法。
isReadable: 缓冲区是否可读
markReaderIndex: 记录当前缓冲区读指针的位置
resetReaderIndex: 重置缓冲区至markReaderIndex的位置处(markReaderIndex的初始位置为0)
readerIndex: 当前缓冲区读指针的位置
readableBytes: 当前缓冲区可读的字节数
read*: 读取数据

备注: 当一开始使用resetReaderIndex重置缓冲区读指针的位置时候,读指针会被重置0;
当使用markReaderIndex记录过当前缓冲区读指针的位置时,再使用resetReaderIndex重置缓冲区读指针的位置,读指针会被重置到markReaderIndex记录的位置处。

public class TvlDecoder extends ByteToMessageDecoder {

    private static Logger logger = Logger.getLogger(getClass());
    private static byte[] MAGIC = new byte[]{0x46, 0x46};

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        //记录读取的位置
        in.markReaderIndex();

        //判断缓冲区是否可读
        if (!in.isReadable()) {
            in.resetReaderIndex();
            return;
        }

        //读取消息头,如果可读字节数不够,重置读指针的位置
        int headerLength = MAGIC.length + Integer.BYTES;
        if (in.readableBytes() < headerLength) {
            in.resetReaderIndex();
            return;
        }

        //检查协议头
        byte[] magic = new byte[MAGIC.length];
        in.readBytes(magic);
        if (!Arrays.equals(magic, MAGIC)) {
            throw new DecoderException("errorMagic: " + Arrays.toString(magic));
        }

        //数据拼包
        int length = in.readInt();
        int readableBytes = in.readableBytes();
        //可读字节数不够,重置读指针的位置
        if (readableBytes < length) {
            in.resetReaderIndex();
            return;
        }

        //读取data数据
        ByteBuf data = in.readBytes(length);

        //封装java类
        Object obj = ......
        out.add(obj);
    }
}
  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值