Netty2:粘包/拆包问题与使用LineBasedFrameDecoder的解决方案
什么是粘包、拆包
粘包、拆包是Socket编程中最常遇见的一个问题,本文来研究一下Netty是如何解决粘包、拆包的,首先我们从什么是粘包、拆包开始说起:
TCP是个"流"协议,所谓流,就是没有界限的一串数据,TCP底层并不了解上层业务的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上:
- 一个完整的包可能会被TCP拆分为多个包进行发送(拆包)
- 多个小的包也有可能被封装成一个大的包进行发送(粘包)
这就是所谓的TCP粘包与拆包
下图演示了粘包、拆包的场景:
基本上有四种情况:
- Data1、Data2都分开发送到了Server端,没有产生粘包与拆包的情况
- Data1、Data2数据粘在了一起,打成了一个大的包发送到了Server端,这种情况就是粘包
- Data1被分成Data1_1与Data1_2,Data1_1先到服务端,Data1_2与Data2再到服务端,这种情况就是拆包
- Data2被分成Data2_1与Data2_2,Data1与Data2_1先到服务端,Data2_2再到服务端,同上,这也是一种拆包的场景
粘包、拆包产生的原因
上面我们详细了解了TCP粘包与拆包,那么粘包与拆包为什么会发生呢,大致上有三种原因:
- 应用程序写入的字节大小大于Socket发送缓冲区大小
- 进行MSS大小的TCP,MSS是最大报文段长度的缩写,是TCP报文段中的数据字段最大长度,MSS=TCP报文段长度-TCP首部长度
- 以太网的Payload大于MTU,进行IP分片,MTU是最大传输单元的缩写,以太网的MTU为1500字节
粘包、拆包解决策略
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下:
- 消息定长,例如每个报文的大小固定为200字节,如果不够空位补空格
- 包尾增加回车换行符进行分割,例如FTP协议
- 将消息分为消息头和消息体,消息头中包含表示长度的字段,通常涉及思路为消息头的第一个字段使用int32来表示消息的总长度
- 更复杂的应用层协议
未考虑TCP粘包导致功能异常演示
基于Netty的第一篇文章《Netty1:初识Netty》,TimeServer与TimeClient不变,简单修改一下TimeServerHandler与TimeClientHandler即可以模拟出TCP粘包的情况,首先修改TimeClientHandler:
1 public class TimeClientHandler extends ChannelHandlerAdapter { 2 3 private static final Logger LOGGER = LoggerFactory.getLogger(TimeClientHandler.class); 4 5 private int counter; 6 7 private byte[] req; 8 9 public TimeClientHandler() { 10 req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes(); 11 } 12 13 @Override 14 public void channelActive(ChannelHandlerContext ctx) throws Exception { 15 ByteBuf message = null; 16 for (int i = 0; i < 100; i++) { 17 message = Unpooled.buffer(req.length); 18 message.writeBytes(req); 19 ctx.writeAndFlush(message); 20 } 21 } 22 23 @Override 24 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 25 ByteBuf buf = (ByteBuf)msg; 26 byte[] req = new byte[buf.readableBytes()]; 27 buf.readBytes(req); 28 29 String body = new String(req, "UTF-8"); 30 System.out.println("Now is:" + body + "; the counter is:" + ++counter); 31 } 32 33 @Override 34 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 35 LOGGER.warn("Unexcepted exception from downstream:" + cause.getMessage()); 36 ctx.close(); 37 } 38 39 }
TimeClientHandler的变化是,之前是发送一次"QUERY TIME ORDER"到服务端,现在变为发送100次"QUERY TIME ORDER"+标准换行符到服务端,并在客户端增加一个计数器,记录从服务端收到的响应次数。
服务单TimeServerHandler也简单改造一下,增加一个计数器记录一下从客户端收到的请求次数:
1 public class TimeServerHandler extends ChannelHandlerAdapter { 2 3 private int counter; 4 5 @Override 6 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 7 ByteBuf buf = (ByteBuf)msg; 8 byte[] req = new byte[buf.readableBytes()]; 9 buf.readBytes(req); 10 11 String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length()); 12 System.out.println("The time server receive order:" + body + "; the counter is:" + ++counter); 13 14 String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER"; 15 currentTime = currentTime + System.getProperty("line.separator"); 16 17 ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); 18 ctx.writeAndFlush(resp); 19 } 20 21 @Override 22 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 23 ctx.close(); 24 } 25 26 }
按照设计,服务端应该会打印出100次"Time time server...",客户端应当会打印出100次"Now is ...",因为客户端向服务端发送了100次"QUERY TIME ORDER"的请求,实际运行起来呢?先看一下服务端的打印:
The time server receive order:QUERY TIME ORDER QUERY TIME ORDER ...省略,这里有55个 QUERY TIME ORD; the counter is:1 The time server receive order: ...省略,这里有42个 QUERY TIME ORDER; the counter is:2
counter最终等于2,表明服务端实际上只收到了2条请求,很显然这里发生了粘包,即多个客户端的包合成了一个发送到了服务端,服务端每收到一个包的大小为1024字节。
接着看一下客户端的打印:
Now is:BAD ORDER BAD ORDER ; the counter is:1
因为服务端只收到了2条消息,因此客户端也只会收到2条消息,因为服务端两次收到的内容都不满足"QUERY TIME ORDER",因此返回"BAD ORDER"到客户端,但是为什么客户端的counter=1呢?回过头来仔细想想,因此服务端发送给客户端的消息也发生了粘包。因此这里简单得出一个结论:粘包/拆包不仅仅发生在客户端给服务端发送数据,服务端回数据给客户端同样有可能发生粘包/拆包。
上面的例子演示了粘包,拆包其实一样的,既然可以知道服务端每收到一个包的大小为1024字节,那客户端每次发送一个大于1024字节的数据给服务端就可以了,有兴趣的朋友可以自己尝试一下。
利用LineBasedFrameDecoder解决粘包问题
为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包,针对上面发送"QUERY TIME ORDER"+标准换行符的这种场景,简单使用LineBasedFrameDecoder就可以解决上面发生的粘包问题。
首先对TimeServer进行改造,加入LineBasedFrameDecoder与StringDecoder:
1 public class TimeServer { 2 3 public void bind(int port) throws Exception { 4 // NIO线程组 5 EventLoopGroup bossGroup = new NioEventLoopGroup(); 6 EventLoopGroup workerGroup = new NioEventLoopGroup(); 7 8 try { 9 ServerBootstrap b = new ServerBootstrap(); 10 b.group(bossGroup, workerGroup) 11 .channel(NioServerSocketChannel.class) 12 .option(ChannelOption.SO_BACKLOG, 1024) 13 .childHandler(new ChildChannelHandler()); 14 15 // 绑定端口,同步等待成功 16 ChannelFuture f = b.bind(port).sync(); 17 // 等待服务端监听端口关闭 18 f.channel().closeFuture().sync(); 19 } finally { 20 // 优雅退出,释放线程池资源 21 bossGroup.shutdownGracefully(); 22 workerGroup.shutdownGracefully(); 23 } 24 } 25 26 private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { 27 @Override 28 protected void initChannel(SocketChannel arg0) throws Exception { 29 arg0.pipeline().addLast(new LineBasedFrameDecoder(1024)); 30 arg0.pipeline().addLast(new StringDecoder()); 31 arg0.pipeline().addLast(new TimeServerHandler()); 32 } 33 } 34 35 }
改造点就在29行、30行两行,加入了LineBasedFrameDecoder与StringDecoder,同时TimeServerHandler也需要相应改造:
1 public class TimeServerHandler extends ChannelHandlerAdapter { 2 3 private int counter; 4 5 @Override 6 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 7 String body = (String)msg; 8 System.out.println("The time server receive order:" + body + "; the counter is:" + ++counter); 9 10 String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER"; 11 currentTime = currentTime + System.getProperty("line.separator"); 12 13 ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); 14 ctx.writeAndFlush(resp); 15 } 16 17 @Override 18 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 19 ctx.close(); 20 } 21 22 }
改造点在第7行,由于使用了StringDecoder,因此channelRead的第二个参数msg不再是ByteBuf类型而是String类型,因此这里只需要做一次String强转即可。
TimeClient改造类似:
1 public class TimeClient { 2 3 public void connect(int port, String host) throws Exception { 4 EventLoopGroup group = new NioEventLoopGroup(); 5 try { 6 Bootstrap b = new Bootstrap(); 7 8 b.group(group) 9 .channel(NioSocketChannel.class) 10 .option(ChannelOption.TCP_NODELAY, true) 11 .handler(new ChannelInitializer<SocketChannel>() { 12 protected void initChannel(SocketChannel ch) throws Exception { 13 ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); 14 ch.pipeline().addLast(new StringDecoder()); 15 ch.pipeline().addLast(new TimeClientHandler()); 16 }; 17 }); 18 19 // 发起异步连接操作 20 ChannelFuture f = b.connect(host, port).sync(); 21 // 等待客户端连接关闭 22 f.channel().closeFuture().sync(); 23 } finally { 24 // 优雅退出,释放NIO线程组 25 group.shutdownGracefully(); 26 } 27 } 28 29 }
第13行、第14行这两行加入了LineBasedFrameDecoder与StringDecoder,TimeClientHandler相应改造:
1 public class TimeClientHandler extends ChannelHandlerAdapter { 2 3 private static final Logger LOGGER = LoggerFactory.getLogger(TimeClientHandler.class); 4 5 private int counter; 6 7 private byte[] req; 8 9 public TimeClientHandler() { 10 req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes(); 11 } 12 13 @Override 14 public void channelActive(ChannelHandlerContext ctx) throws Exception { 15 ByteBuf message = null; 16 for (int i = 0; i < 100; i++) { 17 message = Unpooled.buffer(req.length); 18 message.writeBytes(req); 19 ctx.writeAndFlush(message); 20 } 21 } 22 23 @Override 24 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 25 String body = (String)msg; 26 System.out.println("Now is:" + body + "; the counter is:" + ++counter); 27 } 28 29 @Override 30 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 31 LOGGER.warn("Unexcepted exception from downstream:" + cause.getMessage()); 32 ctx.close(); 33 } 34 35 }
第25行这里使用String进行强转即可。接下来看一下服务端的打印:
The time server receive order:QUERY TIME ORDER; the counter is:1 The time server receive order:QUERY TIME ORDER; the counter is:2 The time server receive order:QUERY TIME ORDER; the counter is:3 The time server receive order:QUERY TIME ORDER; the counter is:4 The time server receive order:QUERY TIME ORDER; the counter is:5 ... The time server receive order:QUERY TIME ORDER; the counter is:98 The time server receive order:QUERY TIME ORDER; the counter is:99 The time server receive order:QUERY TIME ORDER; the counter is:100
看到服务端正常counter从1打印到了100,即收到了100个完整的客户端请求,客户端的打印如下:
Now is:Sat Apr 07 16:00:51 CST 2018; the counter is:1 Now is:Sat Apr 07 16:00:51 CST 2018; the counter is:2 Now is:Sat Apr 07 16:00:51 CST 2018; the counter is:3 Now is:Sat Apr 07 16:00:51 CST 2018; the counter is:4 Now is:Sat Apr 07 16:00:51 CST 2018; the counter is:5 ... Now is:Sat Apr 07 16:00:51 CST 2018; the counter is:98 Now is:Sat Apr 07 16:00:51 CST 2018; the counter is:99 Now is:Sat Apr 07 16:00:51 CST 2018; the counter is:100
看到同样的客户端也正常counter从1打印到了100,即收到了100个完整的服务端响应,至此,使用LineBasedFrameDecoder与StringDecoder解决了上述粘包问题。
整个LineBasedFrameDecoder的原理也比较简单:
LineBasedFrameDecoder依次遍历ByteBuf中的可读字节,判断是否有"\n"或者"\r\n",如果有就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行,它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度,如果连续读到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。 StringDecoder的功能非常简单,就是将接收到的对象转换为字符串,然后继续调用后面的Handler LineBasedFrameDecoder+StringDecoder就是按行切换的文本解码器,被设计用于支持TCP的粘包和拆包
转载于:https://www.cnblogs.com/xrq730/p/8724391.html
Netty2:粘包/拆包问题与使用LineBasedFrameDecoder的解决方案相关推荐
- Netty详解(五):Netty TCP粘包 拆包
1. 概述 无论是服务端还是客户端,我们读取或者发送消息的时候,都需要考虑TCP底层的粘包和拆包机制.下面我们来通过Netty来详解TCP底层的粘包和拆包机制. 2. TCP底层的粘包和拆包机制 TC ...
- Netty如何解决粘包拆包?(二)
前言 TCP是个流协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间并没有分界线.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所 ...
- Netty 粘包 拆包 编码 解码 序列化 介绍
目录: 粘包 & 拆包及解决方案 ByteToMessageDecoder 基于长度编解码器 基于分割符的编解码器 google 的 Protobuf 序列化介绍 其他的 前言 Netty 作 ...
- 《精通并发与Netty》学习笔记(13 - 解决TCP粘包拆包(一)概念及实例演示)
一.粘包/拆包概念 TCP是一个"流"协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的 ...
- Netty(二)——TCP粘包/拆包
转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7814644.html 前面讲到:Netty(一)--Netty入门程序 主要内容: TCP粘包/拆包的基础知 ...
- 粘包拆包,Netty及远洋通信中的解决方案!超实用
在进行Java NIO学习时,发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题. 一.什么粘包和拆包? ...
- 网络(15)-粘包拆包,Netty解决方案及远洋通信中的解决方案!超实用
在进行Java NIO学习时,发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题. 一.什么粘包和拆包? ...
- Java包数据消息头消息尾_读Socket流时产生阻塞的解决方案(粘包拆包问题)
转自:https://www.cnblogs.com/qhyuan1992/p/5385289.html 其实最终讨论的是TCP通信过程中的粘包拆包(半包)问题. 在用socket写一个服务器时遇到了 ...
- Netty学习总结(5)——Netty之TCP粘包/拆包问题的解决之道
无论是服务端还是客户端,读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个"流"协议. 流:没有界限的一串数据.如同河里的流水,它们是连成 ...
最新文章
- boost::spirit模块实现使用迭代器位置注释 AST的测试程序
- busybox的编译和使用
- 双系统安装:Deepin 尝鲜
- tensorflow安装中踩到的坑protobuf、h5py、tensorboard、werkzeug
- bigint hive java类型_【干货】Hive常用函数大全
- winsock类型病毒后遗症处理
- PROTEL技术大全
- 联想员工亲历联想大裁员:公司不是我的家
- html embed自动播放,html embed标签怎么用
- python中heapq的库是什么_Python中heapq模块的用法
- Device Sheets
- Spring Boot的shiro整合(下)
- java堆和栈分别存什么类型_栈、堆、方法区分别存储什么内容
- 服务器中了勒索病毒怎么办,服务器中了勒索病毒怎么解决,服务器中了勒索病毒怎么处理
- html中锚记标记的隐藏,在 Dreamweaver 中选择、查看和设置不可见元素 - Dreamweaver 用户指南...
- 【上传文件】基于阿里云的视频点播VOD、对象存储OSS实现音视频图片等文件上传
- 电商中订单的状态有哪几种,请依次说明各个状态的生命周期
- java计算机毕业设计BS景区票务管理系统设计与实现(附源码、数据库)
- 国内10大物联网公司排行榜,求职必备‼️
- RSA加密过程详解 | 公钥加密| 密码学| 信息安全
热门文章
- php func_get_args(),PHP中func_get_args(),func_get_arg(),func_num_args()有什么不同
- 文献阅读-Pan-Cancer Analysis of lncRNA Regulation Supports Their Targeting of Cancer Genes in Each Tumor
- php红盟,php教程_CI框架源码完全分析之核心文件URI.php
- python3可视化窗口操作_Python3.x+PyQtChart实现数据可视化界面(PyQtChart绘图;还有保存图片)和业务逻辑分离案例01_自己写的,有UI界面源代码...
- mysql 5.7.26安装步骤_centOS7.4 安装 mysql 5.7.26的教程详解
- android中的横幅通知
- 时分多路复用(Time Division Multiplexing,TDM)
- 计算机采用流水线目的是什么?
- php公众获取用户信息,PHP--通过公众号获取用户微信信息
- java 垃圾回收入门