什么是粘包、拆包

粘包、拆包是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:

 public class TimeClientHandler extends ChannelHandlerAdapter {

     private static final Logger LOGGER = LoggerFactory.getLogger(TimeClientHandler.class);

     private int counter;

     private byte[] req;

     public TimeClientHandler() {
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
} @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
for (int i = 0; i < 100; i++) {
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req); String body = new String(req, "UTF-8");
System.out.println("Now is:" + body + "; the counter is:" + ++counter);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
LOGGER.warn("Unexcepted exception from downstream:" + cause.getMessage());
ctx.close();
} }

TimeClientHandler的变化是,之前是发送一次"QUERY TIME ORDER"到服务端,现在变为发送100次"QUERY TIME ORDER"+标准换行符到服务端,并在客户端增加一个计数器,记录从服务端收到的响应次数。

服务单TimeServerHandler也简单改造一下,增加一个计数器记录一下从客户端收到的请求次数:

 public class TimeServerHandler extends ChannelHandlerAdapter {

     private int counter;

     @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req); String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
System.out.println("The time server receive order:" + body + "; the counter is:" + ++counter); String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
currentTime = currentTime + System.getProperty("line.separator"); ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
} }

按照设计,服务端应该会打印出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:

 public class TimeServer {

     public void bind(int port) throws Exception {
// NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup(); try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler()); // 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
} private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
arg0.pipeline().addLast(new LineBasedFrameDecoder(1024));
arg0.pipeline().addLast(new StringDecoder());
arg0.pipeline().addLast(new TimeServerHandler());
}
} }

改造点就在29行、30行两行,加入了LineBasedFrameDecoder与StringDecoder,同时TimeServerHandler也需要相应改造:

 public class TimeServerHandler extends ChannelHandlerAdapter {

     private int counter;

     @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String)msg;
System.out.println("The time server receive order:" + body + "; the counter is:" + ++counter); String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
currentTime = currentTime + System.getProperty("line.separator"); ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
} }

改造点在第7行,由于使用了StringDecoder,因此channelRead的第二个参数msg不再是ByteBuf类型而是String类型,因此这里只需要做一次String强转即可。

TimeClient改造类似:

 public class TimeClient {

     public void connect(int port, String host) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap(); b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeClientHandler());
};
}); // 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 等待客户端连接关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
} }

第13行、第14行这两行加入了LineBasedFrameDecoder与StringDecoder,TimeClientHandler相应改造:

 public class TimeClientHandler extends ChannelHandlerAdapter {

     private static final Logger LOGGER = LoggerFactory.getLogger(TimeClientHandler.class);

     private int counter;

     private byte[] req;

     public TimeClientHandler() {
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
} @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
for (int i = 0; i < 100; i++) {
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String)msg;
System.out.println("Now is:" + body + "; the counter is:" + ++counter);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
LOGGER.warn("Unexcepted exception from downstream:" + cause.getMessage());
ctx.close();
} }

第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的粘包和拆包

Netty2:粘包/拆包问题与使用LineBasedFrameDecoder的解决方案的更多相关文章

  1. Netty使用LineBasedFrameDecoder解决TCP粘包/拆包

    TCP粘包/拆包 TCP是个”流”协议,所谓流,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TC ...

  2. Netty(三)TCP粘包拆包处理

    tcp是一个“流”的协议,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题. 粘包.拆包问题说明 假设客户端分别发送数据包D1和D ...

  3. TCP粘包/拆包问题

    无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河 ...

  4. 1. Netty解决Tcp粘包拆包

    一. TCP粘包问题 实际发送的消息, 可能会被TCP拆分成很多数据包发送, 也可能把很多消息组合成一个数据包发送 粘包拆包发生的原因 (1) 应用程序一次写的字节大小超过socket发送缓冲区大小 ...

  5. Netty(二)——TCP粘包/拆包

    转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7814644.html 前面讲到:Netty(一)--Netty入门程序 主要内容: TCP粘包/拆包的基础知 ...

  6. Netty 粘包 & 拆包 & 编码 & 解码 & 序列化 介绍

    目录: 粘包 & 拆包及解决方案 ByteToMessageDecoder 基于长度编解码器 基于分割符的编解码器 google 的 Protobuf 序列化介绍 其他的 前言 Netty 作 ...

  7. 第四章 TCP粘包/拆包问题的解决之道---4.2--- 未考虑TCP粘包导致功能异常案例

    4.2 未考虑TCP粘包导致功能异常案例 如果代码没有考虑粘包/拆包问题,往往会出现解码错位或者错误,导致程序不能正常工作. 4.2.1 TimeServer 的改造 Class : TimeServ ...

  8. Netty 粘包/拆包应用案例及解决方案分析

    熟悉TCP变成的可以知道,无论是客户端还是服务端,但我们读取或者发送消息的时候,都需要考虑TCP底层粘包/拆包机制,下面我们先看一下TCP 粘包/拆包和基础知识,然后模拟一个没有考虑TCP粘包/拆包导 ...

  9. Netty4实战 - TCP粘包&拆包解决方案

    Netty是目前业界最流行的NIO框架之一,它的健壮性.高性能.可定制和可扩展性在同类框架中都是首屈一指.它已经得到了成百上千的商业项目的验证,例如Hadoop的RPC框架Avro就使用了Netty作 ...

随机推荐

  1. windows10系统终极净化方法

    去年购入一台华硕FL8000U,性能很是不错,但是硬件只能兼容win10,不支持win7(linux倒是可以,但是始终用不顺手),win10里面杂七杂八的确实很多,本人重度强迫症+洁癖+极简主义,所以 ...

  2. SAP系统三层架构

    SAP系统三层架构:表示层,应用层,数据库层   表示层:也可以说个人用户client.由表示层提出数据请求,然后应用层对请求进行处理,再通过数据库层DBMS系统对数据进行处理.   从应用服务器的缓 ...

  3. Android/Linux Thermal Governor之IPA分析与使用

    IPA(Intelligent Power Allocator)模型的核心是利用PID控制器,Thermal Zone的温度作为输入,可分配功耗值作为输出,调节Allocator的频率和电压值. 由P ...

  4. .deb软件包的安装和软件的卸载

    前言: .deb格式的软件包是Debian和Ubuntu等Linux发行版软件安装包的文件扩展名. 使用.deb格式软件安装包安装软件 命令如下: sudo dpkg -i package_file. ...

  5. Circular view path [home]: would dispatch back to the current handler URL [/home] again. Check your ViewResolver setup!

    Circular view path [home]: would dispatch back to the current handler URL [/home] again. Check your ...

  6. GraphicsMagick+im4java实现高质量大图的处理

    http://www.open-open.com/lib/view/open1355754261963.html 做网站往往需要处理各种各样的图片,对于jdk自带的一套图片处理库,他的特点是稳定简单, ...

  7. 在Mybatis-spring上基于注解的数据源实现方案

    一.遇到的痛点 最近在学习Spring-boot过程中,涉及到操作数据库.按照DOC引入mybatis-spring-boot-starter,然后按照套路配置application.properti ...

  8. SpringBoot(二)_项目属性配置

    修改端口 在main/resources/application.properties修改端口 server.port=8088 此时启动访问localhost:8088/hello 就会看到 Hel ...

  9. 车牌定位与畸变校正(python3.7,opencv4.0)

    一.前言及思路简析 目前车牌识别系统在各小区门口随处可见,识别效果貌似都还可以.查阅资料后,发现整个过程又可以细化为车牌定位.畸变校正.车牌分割和内容识别四部分.本篇随笔主要介绍车牌定位及畸变校正两部 ...

  10. Vue.js 牛刀小试(持续更新~~~)

    一.前言 这个系列的文章开始于今年9月从上一家公司辞职后,在找工作的过程中,觉得自己应该学习一些新的东西,从前几章的更新日期也可以看出,中间隔了很长的时间,自己也经历了一些事情,既然现在已经稳定了,就 ...