导致一次发送的数据被分成多个数据包进行传输,或者多次发送的数据被粘成一个数据包进行传输
接收方原因导致的粘包拆包是指在接收数据包时,由于接收端的应用程序读取速度过慢或Socket Buffer设置不当等原因,导致数据在Socket Buffer中积压造成一次读取多个数据包,或者一个数据包分成多次读取
对于TCP传输过程中的粘包拆包:使用TCP来传输数据的话,TCP传输过程中发生粘包和拆包其实对我们应用层的协议关系不大。因为我们不直接接收和处理TCP报文,TCP报文由TCP协议自己处理。最后组装成发送时的字节流缓存到缓冲区。
对于读取数据发生的粘包拆包: 对我们有影响的粘包和拆包其实说的是我们应用层的协议的数据包 在读取中发生了粘包或者拆包,从而没法直接判断报文的边界,需要对这种拆包和粘包情况进行处理。来确定协议报文的边界来解析报文。
情况1 : 应用层两个数据包,正好分隔成两个TCP数据包传输,并且分成了两个数据包读取。
此时不需要粘包或者拆包处理
情况2: 应用层两个数据包都很小,传输后被封装在了1个TCP数据包中,或者读取缓冲区时直接一次性都读取了。
这种情况,就出现了粘包 ,需要处理
情况3: 应用层传输两个数据包,其中一个较大被拆成了两个TCP数据包,读取时总共需要读取三个数据包。这种情况就是出现了拆包, 需要处理。
对粘包和拆包的理解
每次客户端向服务端发送数据,就相当于是通过TCP传输一段自定义格式的消息。那这个消息也可以看成我们自己的一个自定义应用层协议的一个数据报文。所以如果被服务端分多次解析或者多次发送被一次解析对我们应用层的协议来说就是发生了沾包和拆包了。
客户端代码:连接建立时发送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
客户端代码:连接建立时发送足够大的数据,发送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请求报文)按照顺序传输并组装给接收端。在传输过程中,和收过程中都会发生粘包和拆包现象。所以对于上层应用层来说,不可能每次读取流就正好是一个应用层数据报文。所以就需要从一个数据流进行切割,切割出应用层报文的大小进行解析。
Netty提供了四种解码器来解决粘包和拆包的问题,分别对应上面四种方案
使用行拆包器 LinebasedFrameDecoder 改造3.1
ch.pipeline().addLast(new LineBasedFrameDecoder(2048));
这里2048是拆包器最大接受的数据大小,一次接收数据大于这个值超过会报错。ctx.writeAndFlush(Unpooled.copiedBuffer("你好呀,我是Netty客户端"+i+"\n",CharsetUtil.UTF_8));
使用 DelimiterBasedFrameDecoder解码器 改造3.1
pipeline增加拆包解码器
//要用字节流中取得分隔符的字节段
ByteBuf byteBuf =Unpooled.copiedBuffer("$".getBytes(StandardCharsets.UTF_8));
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(2048, byteBuf));
客户端发送数据用特殊字符结尾
ctx.writeAndFlush(Unpooled.copiedBuffer("你好呀,我是Netty客户端"+i+"$",
CharsetUtil.UTF_8));