一、什么场景会粘包和拆包
1.1 数据传输时粘包和拆包传输
导致一次发送的数据被分成多个数据包进行传输,或者多次发送的数据被粘成一个数据包进行传输
- 使用TCP进行数据传输时,TCP是一种有序的字节流,其中是一个一个的数据报文发送到系统的缓冲区中。因此在发送端和接收端之间无法保证数据的分割和边界。这就可能导致数据粘连在一起,或者被拆分成多个部分进行传输。所以基于TCP协议的应用层协议,要从TCP数据流中取得符合自己协议格式的Message 都需要面临粘包和拆包的问题。
- TCP是流协议,其中是一个个数据报文,所以他不会按照应用层的消息规范来对消息进行分隔。那么一个应用层消息,有可能一个应用层消息是占用了1.5个TCP报文,也有可能是2个应用层消息才占用1个TCP报文(TCP的Nagle算法就是这么工作的)。这个时候就需要针对这种拆包或者粘包情况来进行消息处理才能还原成应用层协议数据。
1.2 数据接收方读取数据拆包和粘包
接收方原因导致的粘包拆包是指在接收数据包时,由于接收端的应用程序读取速度过慢或Socket Buffer设置不当等原因,导致数据在Socket Buffer中积压造成一次读取多个数据包,或者一个数据包分成多次读取
- 应用程序读取速度过慢或缓冲区过大导致粘包:如果发送方发送3个每个长度为50的数据包,接收方每次从缓冲区读取100字节,第一次读取到50个字节后,过了很久再读取第二次。这时后两个数据包都到达了缓冲区被一次性读取了出去。这就发生了粘包!
- 应用程序读取过快或一次读取过小导致拆包:如果发送方发送3个长度100的数据包,接收方每次读取50个字节,那么数据包就不完整了。这就是发生了拆包!
1.3 理解总结
对于TCP传输过程中的粘包拆包:使用TCP来传输数据的话,TCP传输过程中发生粘包和拆包其实对我们应用层的协议关系不大。因为我们不直接接收和处理TCP报文,TCP报文由TCP协议自己处理。最后组装成发送时的字节流缓存到缓冲区。
对于读取数据发生的粘包拆包: 对我们有影响的粘包和拆包其实说的是我们应用层的协议的数据包 在读取中发生了粘包或者拆包,从而没法直接判断报文的边界,需要对这种拆包和粘包情况进行处理。来确定协议报文的边界来解析报文。
二、粘包拆包举例
情况1 : 应用层两个数据包,正好分隔成两个TCP数据包传输,并且分成了两个数据包读取。
此时不需要粘包或者拆包处理
情况2: 应用层两个数据包都很小,传输后被封装在了1个TCP数据包中,或者读取缓冲区时直接一次性都读取了。
这种情况,就出现了粘包 ,需要处理
情况3: 应用层传输两个数据包,其中一个较大被拆成了两个TCP数据包,读取时总共需要读取三个数据包。这种情况就是出现了拆包, 需要处理。
三、Netty拆包粘包现象案例
对粘包和拆包的理解
每次客户端向服务端发送数据,就相当于是通过TCP传输一段自定义格式的消息。那这个消息也可以看成我们自己的一个自定义应用层协议的一个数据报文。所以如果被服务端分多次解析或者多次发送被一次解析对我们应用层的协议来说就是发生了沾包和拆包了。
3.1 Netty 测试粘包现象
客户端代码:连接建立时发送10句打招呼的语句
/**
* 通道激活时发送10句打招呼的内容
* @param ctx ctx
* @throws Exception 异常
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//连发10遍消息,判断服务端读取是否会粘包
for (int i = 1; i <= 10; i++) {
ChannelFuture channelFuture = ctx.writeAndFlush("Hello! 我是Netty客户端!");
}
}
服务端代码:解析收到的消息内容,并且统计读取缓冲区的次数
int count=0;
/**
* 通道读取事件
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("客户端发送过来的消息:" + msg);
System.out.println("读取次数:"+(++count));
}
运行结果:十次发送的内容,发生了粘包被一次性读取出来了。
服务器启动成功。。。
解码器开始解码
客户端发送过来的消息:Hello! 我是Netty客户端!Hello! 我是Netty客户端!Hello! 我是Netty客户端!Hello! 我是Netty客户端!Hello! 我是Netty客户端!Hello! 我是Netty客户端!Hello! 我是Netty客户端!Hello! 我是Netty客户端!Hello! 我是Netty客户端!Hello! 我是Netty客户端!
读取次数:1
3.2 Netty拆包测试
客户端代码:连接建立时发送足够大的数据,发送10次。如果服务端读取大于10次,说明每次发送的数据被拆包了。
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//一次发送102400字节数据
byte[] bytes = new byte[102400];
Arrays.fill(bytes, (byte) 10);
for (int i = 0; i < 10; i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer(bytes));
}
}
服务端代码:读取完什么都不回应,看看读取这些数据每次读取的大小和需要读取的次数。
int count = 0;
/**
* 通道读取事件
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("长度是:" + byteBuf.readableBytes());
System.out.println("读取次数 = " + (++count));
}
运行结果:读取超过10次,说明发送的数据包被分成多次读取了 发生了拆包!
服务器启动成功。。。
长度是:2048
读取次数 = 1
长度是:32768
.......省略.......
读取次数 = 16
长度是:65536
读取次数 = 17
长度是:6144
读取次数 = 18
四、处理粘包和拆包
TCP协议是一个字节流协议,只负责将应用层协议发送的消息数据数据(例如Http请求报文)按照顺序传输并组装给接收端。在传输过程中,和收过程中都会发生粘包和拆包现象。所以对于上层应用层来说,不可能每次读取流就正好是一个应用层数据报文。所以就需要从一个数据流进行切割,切割出应用层报文的大小进行解析。
4.1 业内沾包和拆包问题的解决方案
- 固定长度消息体:累计读取固定长度数据就认为读取完成了一个数据报文。
- 换行符作为消息结束符:读取到换行符,就认为一个报文结束
- 使用特殊的符号作为结束符:方法2可以看成方法3的特例,使用这种方法需要保证消息体不包含这个特殊字符。
- 通过消息头中定义长度字段来标识消息的总长度。
4.2 Netty对解决方案的实现
Netty提供了四种解码器来解决粘包和拆包的问题,分别对应上面四种方案
- 固定长度的拆包器 FixedLengthFrameDecoder,每个应用层数据包的都拆分成都是固定长度的大小。解析时直接按照大小解析。
- 行拆包器 LineBasedFrameDecoder,每个应用层数据包,都以换行符作为分隔符,进行分割拆分。
- 分隔符拆包器 DelimiterBasedFrameDecoder,每个应用层数据包,都通过自定义的分隔符,进行分割拆分。
- 基于数据包长度的拆包器 LengthFieldBasedFrameDecoder,将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度
4.3 对 3.1、3.2 中的案例拆包改造
使用行拆包器 LinebasedFrameDecoder 改造3.1
- pipeline中添加LinebasedFrameDecoder
ch.pipeline().addLast(new LineBasedFrameDecoder(2048));
这里2048是拆包器最大接受的数据大小,一次接收数据大于这个值超过会报错。 - 客户端发送消息增加换行
ctx.writeAndFlush(Unpooled.copiedBuffer("你好呀,我是Netty客户端"+i+"\n",CharsetUtil.UTF_8));
使用 DelimiterBasedFrameDecoder解码器 改造3.1
pipeline增加拆包解码器文章来源:https://www.toymoban.com/news/detail-421997.html
//要用字节流中取得分隔符的字节段
ByteBuf byteBuf =Unpooled.copiedBuffer("$".getBytes(StandardCharsets.UTF_8));
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(2048, byteBuf));
客户端发送数据用特殊字符结尾文章来源地址https://www.toymoban.com/news/detail-421997.html
ctx.writeAndFlush(Unpooled.copiedBuffer("你好呀,我是Netty客户端"+i+"$",
CharsetUtil.UTF_8));
到了这里,关于Netty自定义应用层协议逃不开的粘包和拆包处理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!