这里写目录标题
一、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
一般是LengthFieldBasedFrameDecoder
与LengthFieldPrepender
配合起来使用,前者是解码,后者是编码,它们处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度,
LengthFieldBasedFrameDecoder
会按照参数指定的包长度对接收到的数据进行解码,从而得到目标消息体数据,而LengthFieldPrepender则会在响应的数据前面添加指定的包长度,这个包长度保存了当前消息体的整体字节数据长度;
数据在编码发送的时候,会指定当前这条消息的长度;
使用前,对其构造函数参数进行说明:
maxFrameLength:指定了每个包所能传递的最大数据包大小;
lengthFieldOffset:指定了长度字段在字节码中的偏移量;
lengthFieldLength:指定了长度字段所占用的字节长度;
lengthAdjustment:对一些不仅包含有消息头和消息体的数据进行消息头的长度的调整,这样就可以只得到消息体的数据,这里的lengthAdjustment指定的就是消息头的长度;
initialBytesToStrip:对于长度字段在消息头中间的情况,可以通过initialBytesToStrip忽略掉消息头以及长度字段占用的字节,从而得到消息体的内容;
拆包/粘包 编码/解码
LengthFieldBasedFrameDecoder内部实现原理:
首先对长度字段进行设置,如果需要包含消息长度自身,则在原来长度的基础之上再加上lengthFieldLength
的长度;
如果调整后的消息长度小于0,则抛出参数非法异常,对消息长度自身所占的字节数进行判断,以便采用正确的方法将长度字段写入到ByteBuf中,共有以下6种可能:
- 长度字段所占字节为1:如果使用1个Byte字节代表消息长度,则最大长度需要小于256个字节。对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeByte将长度值写入到ByteBuf中;
- 长度字段所占字节为2:如果使用2个Byte字节代表消息长度,则最大长度需要小于65536个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeShort将长度值写入到ByteBuf中;
- 长度字段所占字节为3:如果使用3个Byte字节代表消息长度,则最大长度需要小于16777216个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeMedium将长度值写入到ByteBuf中;
- 长度字段所占字节为4:创建新的ByteBuf,并通过writeInt将长度值写入到ByteBuf中;
- 长度字段所占字节为8:创建新的ByteBuf,并通过writeLong将长度值写入到ByteBuf中;
- 其它长度值:直接抛出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 里面添加的处理器,是有顺序的,先添加的先执行;