【Netty】六、Netty粘包和拆包解决方案

一、TCP粘包和拆包

粘 / 黏
TCP是一个字节流协议,所谓流,就像流水一样,是连成一片的,没有分割线,你没法知道什么时候开始,什么时候结束,也就是我们通过TCP传输的数据是一连串没有界限的数据,TCP底层并不了解上层要传输的业务数据的具体含义,TCP只会根据缓冲区的大小及实际情况进行数据包的分割,那么我们一个完整的业务数据,可能会被TCP拆分成多个包进行发送,也有可能业务上的多条完整数据被合并成一个包发送,这就是TCP的粘包和拆包问题;

一个TCP协议传输的过程:

在这里插入图片描述
发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取;
在这里插入图片描述

  • 1、第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象;
  • 2、第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包,这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理;
  • 3、第三种情况,这种情况有两种表现形式,接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包,这两种情况如果不加特殊处理,对于接收端同样是不好处理的;

粘包和拆包发生原因

1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;
2、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;
3、待发送数据大于最大报文长度,TCP在传输前将进行拆包;
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;

粘包和拆包解决策略

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,该问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:

消息定长:

发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从缓冲区中读取固定长度的数据,这就自然而然的把每个数据包拆分开来;

设置消息边界:

服务端从网络流中按消息边界分离出消息内容,比如在数据包末尾增加回车换行符进行分割;

将消息分为消息头和消息体:

消息头中包含表示消息总长度(或者消息体长度)的字段,消息体是要读取的内容;

更复杂的应用层协议:

比如Netty中实现的一些协议对粘包、拆包进行处理;

二、 Netty粘包和拆包解决方案

Netty框架对于客户端和服务端之间的数据传输做了很好的处理,客户端在发送数据之前先对数据按一定的规则进行编码,服务端在接收到数据后按照相同的规则进行解码,这就是Netty解决粘包拆包问题的思路;
对于粘包拆包问题,Netty 已经为我们提供了很多不同的解码器,在无必要时不必重复发明轮子,我们可以直接使用Netty现成的解码器即可;
Netty中提供如下四种解码器用来解决粘包和拆包问题:

1、固定长度解码器FixedLengthFrameDecoder;

每个应用层数据包都拆分成都是固定长度的大小,比如1024字节;
对于使用固定长度的粘包和拆包场景,可以使用FixedLengthFrameDecoder,该解码器会每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足,其使用也比较简单,只需要在构造函数中指定每个消息的长度即可;
由于解码有可能需要等待下一个包进行补全,代码相对复杂,所以Netty框架帮我们提供了解码器,但是对于编码器,需要用户自行编写,因为编码时只需要将不足指定长度的部分进行补全即可,代码比较简单,所以Netty就没有帮我们实现编码器;
数据在编码发送的时候,以固定长度作为一条完整的消息,代码实现:

channelPipeline.addLast(new FixedLengthFrameDecoder(22));

这里面的22表示占用的字节个数,utf-8编码下,一个汉字占3个字节,一个英文字母占1个字节;

2、行解码器LineBasedFrameDecoder

每个应用层数据包,都以换行符作为分隔符(\r\n或者\n),进行分割拆分;
数据在编码发送的时候,会以换行符作为一条完整的消息;
也没有提供编码器;
代码实现:

channelPipeline.addLast(new LineBasedFrameDecoder(1024));

3、分隔符解码器DelimiterBasedFrameDecoder

每个应用层数据包,通过自定义的分隔符进行分割拆分,该解码器与LineBasedFrameDecoder本质上是一样的,都是使用分隔符对数据包进行拆分,只是可以指定自己的分割符;
数据在编码发送的时候,会以一个自定义的分隔符作为一条完整的消息;
也没有提供编码器;

  • 代码实现:
channelPipeline.addLast(
        new DelimiterBasedFrameDecoder(Integer.MAX_VALUE,
        Unpooled.copiedBuffer("$", CharsetUtil.UTF_8))
);

4、基于数据包长度的解码器 LengthFieldBasedFrameDecoder

将应用层数据包的长度,作为接收端应用层数据包的拆分依据,按照应用层数据包的大小解码,这个解码器要求应用层协议中包含数据包的长度,这里的长度是动态长度; abcd sduihfd32 09werferjgvlk
一般是LengthFieldBasedFrameDecoderLengthFieldPrepender配合起来使用,前者是解码,后者是编码,它们处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度,
LengthFieldBasedFrameDecoder会按照参数指定的包长度对接收到的数据进行解码,从而得到目标消息体数据,而LengthFieldPrepender则会在响应的数据前面添加指定的包长度,这个包长度保存了当前消息体的整体字节数据长度;
数据在编码发送的时候,会指定当前这条消息的长度;
在这里插入图片描述
使用前,对其构造函数参数进行说明:
maxFrameLength:指定了每个包所能传递的最大数据包大小;
lengthFieldOffset:指定了长度字段在字节码中的偏移量;
lengthFieldLength:指定了长度字段所占用的字节长度;
lengthAdjustment:对一些不仅包含有消息头和消息体的数据进行消息头的长度的调整,这样就可以只得到消息体的数据,这里的lengthAdjustment指定的就是消息头的长度;
initialBytesToStrip:对于长度字段在消息头中间的情况,可以通过initialBytesToStrip忽略掉消息头以及长度字段占用的字节,从而得到消息体的内容;

拆包/粘包 编码/解码

LengthFieldBasedFrameDecoder内部实现原理:

首先对长度字段进行设置,如果需要包含消息长度自身,则在原来长度的基础之上再加上lengthFieldLength的长度;

如果调整后的消息长度小于0,则抛出参数非法异常,对消息长度自身所占的字节数进行判断,以便采用正确的方法将长度字段写入到ByteBuf中,共有以下6种可能:

  1. 长度字段所占字节为1:如果使用1个Byte字节代表消息长度,则最大长度需要小于256个字节。对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeByte将长度值写入到ByteBuf中;
  2. 长度字段所占字节为2:如果使用2个Byte字节代表消息长度,则最大长度需要小于65536个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeShort将长度值写入到ByteBuf中;
  3. 长度字段所占字节为3:如果使用3个Byte字节代表消息长度,则最大长度需要小于16777216个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeMedium将长度值写入到ByteBuf中;
  4. 长度字段所占字节为4:创建新的ByteBuf,并通过writeInt将长度值写入到ByteBuf中;
  5. 长度字段所占字节为8:创建新的ByteBuf,并通过writeLong将长度值写入到ByteBuf中;
  6. 其它长度值:直接抛出Error;

ChannelPipeline的顺序性

ChannelPipeline channelPipeline = socketChannel.pipeline();
//channelPipeline.addLast(new FixedLengthFrameDecoder(22));
//channelPipeline.addLast(new LineBasedFrameDecoder(1024));
/*channelPipeline.addLast(
        new DelimiterBasedFrameDecoder(Integer.MAX_VALUE,
        Unpooled.copiedBuffer("$", CharsetUtil.UTF_8))
);*/
channelPipeline.addLast(new LengthFieldBasedFrameDecoder(
        1024, 0, 4, 0, 4));
channelPipeline.addLast(new LengthFieldPrepender(4));
// 国内的术语:粘包、拆包 ,其实就是 编码 和 解码
channelPipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
channelPipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));

channelPipeline.addLast(DecoderClientHandler.INSTANCE);

向ChannelPipeline 里面添加的处理器,是有顺序的,先添加的先执行;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值