Netty 中 TCP 粘包拆包问题

信息通过tcp传输过程中出现的状况 .

TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送

产生粘包和拆包问题的主要原因是,操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。

入图所示:

上图中演示了粘包和拆包的三种情况:

  • D1和D2两个包都刚好满足TCP缓冲区的大小,或者说其等待时间已经达到TCP等待时长,从而还是使用两个独立的包进行发送;
  • D1和D2两次请求间隔时间内较短,并且数据包较小,因而合并为同一个包发送给服务端;
  • 某一个包比较大,因而将其拆分为两个包D*_1和D*_2进行发送,而这里由于拆分后的某一个包比较小,其又与另一个包合并在一起发送。

发生这种情况的代码:

客户端发送数据 快速的发送 10条数据 :

public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private int count;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//使用客户端发送10条数据 hello,server 编号
for(int i= 0; i< 10; ++i) {
ByteBuf buffer = Unpooled.copiedBuffer("hello,server " +i, Charset.forName("utf-8"));
ctx.writeAndFlush(buffer);
}
} }

服务端接受打印:

服务器接收到数据 hello,server 0
服务器接收到数据 hello,server 1
服务器接收到数据 hello,server 2hello,server 3
服务器接收到数据 hello,server 4hello,server 5
服务器接收到数据 hello,server 6
服务器接收到数据 hello,server 7hello,server 8
服务器接收到数据 hello,server 9

很明显 其中有三条记录被粘在其他数据上,这就是TCP的粘包拆包现象

怎么解决:

  1. Netty自带的 解决方案:

    • 固定长度的拆包器 FixedLengthFrameDecoder,每个应用层数据包的都拆分成都是固定长度的大小

    • 行拆包器 LineBasedFrameDecoder,每个应用层数据包,都以换行符作为分隔符,进行分割拆分

    • 分隔符拆包器 DelimiterBasedFrameDecoder,每个应用层数据包,都通过自定义的分隔符,进行分割拆分

    • 基于数据包长度的拆包器 LengthFieldBasedFrameDecoder,将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度

FixedLengthFrameDecoder 解码器

服务端 添加 FixedLengthFrameDecoder 解码器 并指定长度

public class EchoServer {

  public static void main(String[] args) throws InterruptedException {

      EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//指定长度为9 则每次截取长度为9的字节
ch.pipeline().addLast(new FixedLengthFrameDecoder(9));
// 将 每次截取的字节编码为字符串
ch.pipeline().addLast(new StringDecoder());
//自定义处理类打印
ch.pipeline().addLast(new EchoServerHandler());
}
}); ChannelFuture future = bootstrap.bind(8000).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}

自定义服务端Handler 打印字符串:

public class EchoServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("message: " + msg.trim());
}
}

客户端发送信息 并添加字符串编码器 将信息已字符串的形式编码:

public class EchoClient {

  public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new EchoClientHandler());
}
}); ChannelFuture future = bootstrap.connect("127.0.0.1", 8000).sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}

客户端Handler 发送信息 刚好长度为9 :

public class EchoClientHandler extends SimpleChannelInboundHandler<String> {

  @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("123456789");
}
}

总结: FixedLengthFrameDecoder 解码器 将按照指定长度截取字节 并添加到List中向后传递 , 以本案例为例,如果字节数刚好为9,则全部打印,如果 字节数为18, 则拆分打印两次,如果为19 则最后一个字节不打印,如果不足9 则什么都不打印.

LineBasedFrameDecoder 行拆分器

通过行换行符 \n 或者 \r\n 进行分割,

将上面案例的FixedLengthFrameDecoder 解码器 换成 LineBasedFrameDecoder

并指定 截取每段的最大长度 (超过报错 不往后传递)

...

.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { // ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
ch.pipeline().addLast(new LineBasedFrameDecoder(5));
// 将前一步解码得到的数据转码为字符串
ch.pipeline().addLast(new StringDecoder());
// 最终的数据处理
ch.pipeline().addLast(new EchoServerHandler());
}
}); ...

客户端Handler 发送字符串, 最后的"1234" 不会打印,,

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("1\n123456\r\n1234");
}

服务端接收并打印结果 分别打印了 "1" 和 "1234" 而超过字节长度5 的 "123456"则报出TooLongFrameException错误

server receives message: 1

An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.netty.handler.codec.TooLongFrameException: frame length (6) exceeds the allowed maximum (5) server receives message: 1234

DelimiterBasedFrameDecoder 自定义分割符

和行分割符类似, 此解码器可以自定义分割符,常用构造方法:

 public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf... delimiters)

接收一个最大长度,和 任意个数的 分隔符(用ByteBuf的形式传入),解码器识别到任意一个 分割符 都会进行拆分

注册解码器:

传入 "$" 和 "*" 作为分割符,并指定最大长度为 5个字节

.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new DelimiterBasedFrameDecoder(5,
Unpooled.wrappedBuffer("$".getBytes()),Unpooled.wrappedBuffer("*".getBytes())));
// 将前一步解码得到的数据转码为字符串
ch.pipeline().addLast(new StringDecoder()); // 最终的数据处理
ch.pipeline().addLast(new EchoServerHandler());
}
});

客户端 发送数据:

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("1$123456*1234$789$");
}

服务端只打印了 "1" 当解析到 "123456" 时 就报错了 后面就没有再解析了,会缓存着 等到该通道关闭 或者有后续数据发送过来时 才继续解析

LengthFieldBasedFrameDecoder

自定义数据长度,发送的 字节数组中 包含 描述 数据长度的字段 和 数据本身,

解码过程

常用字段:

  • maxFrameLength:指定了每个包所能传递的最大数据包大小,(上图中的最大长度为11)
  • lengthFieldOffset:指定了长度字段在字节码中的偏移量;(11这个描述长度的数据是在数组的第几位开始)
  • lengthFieldLength:指定了长度字段所占用的字节长度;(11 占 1个字节)
  • lengthAdjustment: 长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。 ( 11 这个域 不光光描述 Hello,world, 一般设置为0,)
  • initialBytesToStrip : 丢弃的起始字节数。丢弃处于有效数据前面的字节数量。比如前面有1个节点的长度域,则它的值为1. ( 如果为0代表不丢弃,则将长度域也向后传递)

服务端添加 解码器:

  • 最大长度 为 长度描述域 的值11 + 长度描述域本身占用的长度 1 = 12
  • 长度描述域放在数据包的第一位, 没有偏移 为0
  • 长度描述域 长度为1
  • 无需矫正
  • 一个字节也不丢弃
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 这里将FixedLengthFrameDecoder添加到pipeline中,指定长度为20
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(12,0,1,0,0));
// 将前一步解码得到的数据转码为字符串 ch.pipeline().addLast(new StringDecoder());
// 最终的数据处理
ch.pipeline().addLast(new EchoServerHandler());
}
});

客户端发送数据 发送最Netty 底层操作 的ByteBuf对象 发送时 无需任何编码:

 @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buffer = Unpooled.buffer();
buffer.writeByte(11);
buffer.writeBytes("Hello,World".getBytes());
ctx.writeAndFlush(buffer);
}

服务端接收数据为 (11代表的制表符)Hello,World

这样发送 每次都要计算 数据长度,并手动添加到 数据的前面,很不方便 配合LengthFieldPrepender 使用,这个编码码器可以计算 长度,并自动添加到 数据的前面

改造客户端 先拦截数据按字符串编码,再计算字节长度 添加 长度描述字段 并占用一个字节 (这个长度要与客户端的解码器 lengthFieldLength参数 值保持一致) :

 .handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LengthFieldPrepender(1));
ch.pipeline().addLast(new StringEncoder());
// 客户端发送消息给服务端,并且处理服务端响应的消息
ch.pipeline().addLast(new EchoClientHandler());
}
});

客户端发送 有字符串编码器 可以直接发送字符串:

  @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("Hello,World");
}

自定义协议

上面介绍的 各种解码器 已经可以应付绝大多数场景, 如果遇到 特殊的状况 我们也可以自定义协议

定义 协议对象:

//协议包
public class MessageProtocol {
private int len; //关键
private byte[] content; public int getLen() {
return len;
} public void setLen(int len) {
this.len = len;
} public byte[] getContent() {
return content;
} public void setContent(byte[] content) {
this.content = content;
}
}

客户端发送:

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { for(int i = 0; i< 5; i++) {
String mes = "Hello,World";
byte[] content = mes.getBytes(Charset.forName("utf-8"));
int length = mes.getBytes(Charset.forName("utf-8")).length; //创建协议包对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
ctx.writeAndFlush(messageProtocol); }
}

该协议的 自定义 编码器 将协议包发送出去:

public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
System.out.println("MyMessageEncoder encode 方法被调用");
out.writeInt(msg.getLen());
out.writeBytes(msg.getContent());
}
}

将客户端 发送数据的Handler 和 编码器 注册 这里就不写了

服务端解码器 读取长度 并 判断可读数据的长度是否足够 :

public class MyMessageDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { in.markReaderIndex(); //读取长度
int length = in.readInt();
//如果可读长度大于 数据长度 说明数据完整
if (in.readableBytes()>length){
byte[] content = new byte[length];
in.readBytes(content);
//封装成 MessageProtocol 对象,放入 out, 传递下一个handler业务处理
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
out.add(messageProtocol);
}else{
//如果数据不够长 将已经读过的的int 数据还原回去 留下次读取
in.resetReaderIndex();
}
}
}

服务端成功读取:

本例中存在很多问题, 明白这个意思就行, 感兴趣的话 可以 自己动手优化

Netty笔记(6) - 粘包拆包问题及解决方案的更多相关文章

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

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

  2. netty之==TCP粘包/拆包问题解决之道(一)

    一.TCP粘包/拆包是什么 TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在 ...

  3. TCP 粘包 - 拆包问题及解决方案

    目录 TCP粘包拆包问题 什么是粘包 - 拆包问题 为什么存在粘包 - 拆包问题 粘包 - 拆包 演示 粘包 - 拆包 解决方案 方式一: 固定缓冲区大小 方式二: 封装请求协议 方式三: 特殊字符结 ...

  4. Netty的TCP粘包/拆包(源码二)

    假设客户端分别发送了两个数据包D1和D2给服务器,由于服务器端一次读取到的字节数是不确定的,所以可能发生四种情况: 1.服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包. 2.服 ...

  5. Netty解决TCP粘包/拆包问题 - 按行分隔字符串解码器

    服务端 package org.zln.netty.five.timer; import io.netty.bootstrap.ServerBootstrap; import io.netty.cha ...

  6. 深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?

    前言 学习Netty避免不了要去了解TCP粘包/拆包问题,熟悉各个编解码器是如何解决TCP粘包/拆包问题的,同时需要知道TCP粘包/拆包问题是怎么产生的. 在此博文前,可以先学习了解前几篇博文: 深入 ...

  7. 《精通并发与Netty》学习笔记(14 - 解决TCP粘包拆包(二)Netty自定义协议解决粘包拆包)

    一.Netty粘包和拆包解决方案 Netty提供了多个解码器,可以进行分包的操作,分别是: * LineBasedFrameDecoder (换行)   LineBasedFrameDecoder是回 ...

  8. 《精通并发与Netty》学习笔记(13 - 解决TCP粘包拆包(一)概念及实例演示)

    一.粘包/拆包概念 TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认 ...

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

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

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

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

随机推荐

  1. webservice--WSDL文件生成本地的代理类

    我们在对应第三方接口时常用:项目上右键---->服务引用---->WCF Web Service,如下图的页面----->填好url后---->转到,就可以发现服务,生成代理类 ...

  2. 深度学习应用篇-计算机视觉-视频分类[8]:时间偏移模块(TSM)、TimeSformer无卷积视频分类方法、注意力机制

    深度学习应用篇-计算机视觉-视频分类[8]:时间偏移模块(TSM).TimeSformer无卷积视频分类方法.注意力机制 1.时间偏移模块(TSM) 视频流的爆炸性增长为以高精度和低成本执行视频理解任 ...

  3. MySQL常用操作指令大全

    前言: 一.基础概念 二.子句顺序 三.使用MySQL(USE.SHOW) 四.检索数据(SELECT) 五.排序检索数据(ORDER BY) 六.过滤数据(WHERE) 七.数据过滤(AND.OR. ...

  4. Linux 多种方式实现文件共享

    文件共享服务在Linux系统上有多种方式,最常用的有Samba,vsftp,iSCSI,NFS这四种方式,如下将分别配置四种不同的文件共享服务. VSFTP 文件传输 FTP是文件传输协议.用于Int ...

  5. 遥感图像处理笔记之【FastAI Multi-label image classification】

    遥感图像处理学习(4) 前言 遥感系列第4篇.遥感图像处理方向的学习者可以参考或者复刻 本文初编辑于2023年12月15日 2024年1月24日搬运至本人博客园平台 文章标题:FastAI Multi ...

  6. 轻薄本杀入3000元档!真我笔记本Air评测:轻松实现12小时超长续航

    一.前言:真我笔记本Air带轻薄本杀入3K档位 拓宽产品线是如今各大数码品牌的核心策略之一,尤其是跨界,比如手机厂商,就不断在平板.智能穿戴.PC笔记本等领域延伸,甚至是新能源汽车. 现在,realm ...

  7. JS Leetcode 503. 下一个更大元素 II 题解分析,依旧单调栈做法解决此题

    壹 ❀ 引 我在JS Leetcode 496. 下一个更大元素 I 更清晰的图解单调栈做法一文中,介绍了单调栈做法解决下一个更大元素的问题,比较巧的是这道题还有升级版,题目来自Leetcode503 ...

  8. zabbix-server.service failed解决方法

    1.问题描述 centos7中安装的zabbix server在重启系统后无法启动了,查看状态报错如下: 2.问题原因 selinux没有关闭! 3.解决 永久关闭selinux, 将SELINUX值 ...

  9. win32-使用EnumWindows比较两个窗口的Z轴

    通过使用EnumWindows()和枚举窗口来手动确定EnumChildWindows()来直接确定哪个窗口在z轴上比另一个窗口高. struct myEnumInfo { HWND hwnd1; H ...

  10. (微服务)服务治理:熔断器介绍以及hystrix-go的使用

    一.什么是熔断器 要理解熔断器,可以先看看电路中使用的保险丝. 保险丝(fuse)也被称为电流保险丝,IEC127 标准将它定义为"熔断体(fuse-link)".保险丝是一种保证 ...