【netty系列-09】深入理解和解决tcp的粘包拆包

Netty系列整体栏目


内容链接地址
【一】深入理解网络通信基本原理和tcp/ip协议https://zhenghuisheng.blog.csdn.net/article/details/136359640
【二】深入理解Socket本质和BIOhttps://zhenghuisheng.blog.csdn.net/article/details/136549478
【三】深入理解NIO的基本原理和底层实现https://zhenghuisheng.blog.csdn.net/article/details/138451491
【四】深入理解反应堆模式的种类和具体实现https://zhenghuisheng.blog.csdn.net/article/details/140113199
【五】深入理解直接内存与零拷贝https://zhenghuisheng.blog.csdn.net/article/details/140721001
【六】select、poll和epoll多路复用的区别https://zhenghuisheng.blog.csdn.net/article/details/140795733
【七】深入理解和使用Netty中组件https://zhenghuisheng.blog.csdn.net/article/details/141166098
【八】深入Netty组件底层原理和基本实现https://zhenghuisheng.blog.csdn.net/article/details/141685088
【九】深入理解和解决tcp的粘包拆包https://zhenghuisheng.blog.csdn.net/article/details/141860959

深入理解tcp的粘包拆包原理

  • 一,tcp层的粘包分包问题
    • 1,通过代码直观的表现出粘包的问题
      • 1.1,服务端代码实现
      • 1.2,客户端代码实现
    • 2,导致粘包拆包因素以及解决方案
      • 2.1. 各个包之间没有设置边界感导致
      • 2.2. 服务端度缓冲区数据处理导致
      • 2.3,粘包拆包解决方案
        • 2.3.1,分隔符设置边界
        • 2.3.2,固定长度解码器
        • 2.3.3,使用长度字段解码器

一,tcp层的粘包分包问题

在前面了解完整个netty的基本组件和使用之后,本篇文章讲解一个关于网络编程的重点,就是在netty中是如何处理这种tcp层面的粘包和半包问题。

依旧得回归下图,在网络通信编程中,数据要从客户端发送到另一个对端,都需要从客户端的应用层,将数据封装成报文,往下层层封装,然后通过以太网等将数据发送给对端,对端接收到数据之后,将数据从物理层往上层层解析,最终数据解析到应用层,解析后获取到客户端发送的数据。由于在操作系统层面呢,操作系统内部将tcp层以下的协议全部封装好,将内部所有的细节以及实现封装成一个个socket,让开发者只需要更加的关注与应用层的开发,通过操作socket实现与对端的通信。

请添加图片描述

在前面的nio中讲到,reactor反应堆模式的三大特性分别是:Selector、SocketChannel和Buffer ,并且netty是基于nio实现的,所以不管是在原生的nio中,还是在基于nio实现的netty中,都离不开这个 Buffer ,而在本篇文章中要讲解的这个粘包和半包问题,就是由于这个Buffer缓冲区导致的。如下图,Buffer又有读buffer和写buffer,由于tcp的全双工的特性,因此底层是实现了同时读写的功能

请添加图片描述

1,通过代码直观的表现出粘包的问题

1.1,服务端代码实现

何为粘包,顾名思义,就是多个包黏贴在一起了。接下来通过一段代码来表现出粘包的问题,还是那套配方,编写服务端主启动类,客户端主启动类,然后就是一个个由用户自定义实现的一些 handler 。服务端主启动类的配置如下

/*** @author zhenghuisheng* @date 2024/9/1 21:35* 粘包服务端主启动类*/
public class StickPackageServer {private static Integer port = 8888;public static void main(String[] args) {// 创建自定义事件组,一个线程循环的处理事件,类似与nio的selectorEventLoopGroup loopGroup = new NioEventLoopGroup();try{//创建服务端主启动类ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(loopGroup)    //绑定组.channel(NioServerSocketChannel.class).localAddress(port)         //绑定端口.childHandler(new ChannelInitializer<SocketChannel>() { //初始化channel,将事件加入@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline().addLast(new StickPackageServerHandler());     //将事件加入到管道中}});//完成绑定,内部如果异步实现bind,因此需要阻塞拿到返回结果ChannelFuture future = bootstrap.bind().sync();//关闭future时也需要阻塞,内部也采用的是异步操作future.channel().closeFuture().sync();}catch (Exception e){e.printStackTrace();}finally {try {//处理中断异常loopGroup.shutdownGracefully().sync();} catch (InterruptedException e) {e.printStackTrace();}}}
}

服务端中处理事件的Handler如下,这边主要统计客户端发送了多少个报文过来

@Slf4j
public class StickPackageServerHandler extends ChannelInboundHandlerAdapter {private AtomicInteger counter = new AtomicInteger(0);@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf byteBuf = (ByteBuf)msg;String request = byteBuf.toString(CharsetUtil.UTF_8);log.info("服务端接收到请求数量为" + counter.incrementAndGet());String resp = request + "成功请求";ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));}
}

1.2,客户端代码实现

首先是客户端主启动类的代码实现,将要处理事件的 StickPackageClientHandler 加入

/*** @author zhenghuisheng* @date 2024/9/1 21:38*/
public class StickPackageClient {private static Integer port = 8888;private static String host = "127.0.0.1";public static void main(String[] args) {// 创建自定义事件组,一个线程循环的处理事件,类似与nio的selectorEventLoopGroup loopGroup = new NioEventLoopGroup();try{//客户端只需要用bootStrapBootstrap bootstrap = new Bootstrap();bootstrap.group(loopGroup).channel(NioSocketChannel.class).remoteAddress(new InetSocketAddress(host,port))    //和服务器不一样,这里只需要连接服务器地址即可.handler(new ChannelInitializer<SocketChannel>() {  //和服务端不同,服务端使用的childHandler客户端只需要具体的handler即可@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline().addLast(new StickPackageClientHandler());}});//完成绑定,内部如果异步实现bind,因此需要阻塞拿到返回结果ChannelFuture future = bootstrap.connect().sync();//关闭future时也需要阻塞,内部也采用的是异步操作future.channel().closeFuture().sync();}catch (Exception e){e.printStackTrace();}}
}

随后定义这个 StickPackageClientHandler 事件的具体实现,通过实现这个 channelActive 方法来触发所需要执行的动作。这里主要是定义好对应的数据,内部将对应的数据封装成报文发送给对端,然后循环10次往服务端发送数据。

/*** @author zhenghuisheng* @date 2024/9/1 21:38*/
@Slf4j
public class StickPackageClientHandler extends SimpleChannelInboundHandler<ByteBuf> {private AtomicInteger counter = new AtomicInteger(0);@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {log.info("接收到的请求数量为:" + counter.incrementAndGet());}//事件被触发后所执行的动作@Overridepublic void channelActive(ChannelHandlerContext ctx) {//定义请求内容String request = "abcdefghijklmnopqrstuvwxyz" + System.getProperty("line.separator");final ByteBufAllocator byteBufAllocator = ctx.alloc();ByteBuf msg = null;//给服务器发送10个报文for(int i=0;i<10;i++){msg = byteBufAllocator.buffer(request.length());msg.writeBytes(request.getBytes());ctx.writeAndFlush(msg);}}
}

随后先启动服务端,然后再启动客户端,打印的结果如下。

INFO com.zhs.netty.netty.stickypackage.StickPackageServerHandler - 服务端请求数量为1

按理来说服务端也应该接收到10个报文,但是打印日志显示服务端只接收到一个报文,因此猜想而知就是数据在传输过程中,为了提升整个系统的吞吐量,某个流程将这10个报文封装成了一个包发送给了服务端,导致服务端只接收到了1个数据包

2,导致粘包拆包因素以及解决方案

根据上面的例子,在客户端中,发送了10个包,按理来说就是发送的一个报文对应一个封包,应该会有10个包,而在实际的打印日志中只有一个包,说明就产生了粘包的情况,就是将多个报文包粘在一起了。

在这里插入图片描述

拆包因素就是和粘包的相反,粘包是因为每个数据报文太小,而将多个包合成一个数据包。拆包就是因为单个包的报文太大了,如单个包的大小为2000字节,超过了tcp最大1460字节包大小,游戏需要分成两段报文包发送给对端,即一个数据包多次发送。这样就需要合并两个报文包下面的同一段报文。

在这里插入图片描述

这就是经典的生产者消费者问题了,客户端的写buffer对应的就是生产者,服务端的读buffer对应的就是消费者

在这里插入图片描述

生产者为了提高整个系统的效率,以IPv4为例,tcp每个报文最大的长度是1460个字节,假设客户端这边连续上传10个这种100字节的数据报文,总大小也在1000字节,那么客户端就会认为这10个报文我一次就可以发送给对端,那么这10个包就被黏在一起了

当然在tcp层中,tcp的粘包拆包问题并不是其本身的缺陷,而是内部的一种机制,就是说tcp内部是不知道应用层每个包的大小,多少个包之类的,也不会对包与包之间的边界处理,因此只能在应用层或者通过相关协议去做一些限制。

2.1. 各个包之间没有设置边界感导致

举个例子,就像我们平常时开发的批量删除中,如果要删除多个id,一个是直接用数组将id从前端传给后端,但是现在不考虑这种方式;另一种是通过逗号或者其他的分隔符拼接成字符串将数据传给后端,如下面这段这种格式,这样后台先去解析这段字符串,然后再将对应的id进行删除。对于后台来讲,整个数据的边界感就是逗号 ,

"1,2,3,4,5,6"

但是在netty中,buffer缓冲区在合并包时是没有边界感的,就是不能像我们认为一样手动的去添加这种分隔符等,因此这就可能出现多个包粘成一个包的情况,最后不能对这些请求做出正确的响应。就像上面的这段代码,按理来说会有10个报文,并且在客户端这边接收到10个响应,但是最终在客户端这边只接收到了一个响应数据。由于客户端这边没有给实际的边界感,当服务端接收到数据时,也不能根据对应的边界做处理,只能将整包一起处理并响应。

2.2. 服务端度缓冲区数据处理导致

在服务段的readBuf中读取到数据时,如果在应用层没有设置边界,那么服务端也可能会根据滑动串口读取固定的数据,那么也可能会使得数据出现粘包情况。并且如果服务出现阻塞情况,所有数据都挤压在一起,那么也会导致出现粘包的情况

即使说通过设置 TCP_NODELAY 这个在客户端那边无延迟的情况,就是来一个报文立马发送给对端,也可能因为收到服务端这边阻塞或者没设这边界的情况,出现粘包。即禁用 Nagle 算法

.option(ChannelOption.TCP_NODELAY, true)  // 禁用 Nagle 算法

2.3,粘包拆包解决方案

既然知道粘包的主要原因是tcp对包与包之间的边界无感知,那么解决方案就呼之欲出了,那就是加边界呗。

2.3.1,分隔符设置边界

可以直接加换行符,也可以自定义边界。如以换行符为边界的代码如下,只需要在加pipeline之前,加一个 LineBasedFrameDecoder 的对象即可,服务端和客户端都需要加上这句 **.addLast(new LineBasedFrameDecoder(26)) **。服务端和客户端两边都要加以下这段代码

socketChannel.pipeline().addLast(new LineBasedFrameDecoder(26)) 		//设置边界.addLast(new StickPackageServerHandler());     //将事件加入到管道中

也可以自定义边界分隔符,每个报文之间通过这个 @_ 设置边界符

ByteBuf delimiter = Unpooled.copiedBuffer("@_".getBytes());
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(26,delimiter)).addLast(new StickPackageClientHandler());

在这里插入图片描述

如果设置了自定义边界,在客户端对应的handler中,也需要将发送的内容后面拼接一个 @_ ,这样服务端在接收到数据之后可以直接根据这个自定义的边界获取以及处理相关数据了

String request = "abcdefghijklmnopqrstuvwxyz" + "@_";

在这里插入图片描述

服务端这边打印的日志详情如下,通过这种边界设置对应文本的方式确实解决了这种粘包的问题

[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为1
[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为2
[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为3
[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为4
[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为5
[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为6
[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为7
[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为8
[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为9
[nioEventLoopGroup-2-2] INFO c.r.w.c.n.s.StickPackageServerHandler - [channelRead,20] - 服务端接收到请求数量为10

2.3.2,固定长度解码器

使用分隔符设置边界确实可以解决粘包问题,但是只适用于一些文本类型的消息,如果是使用这种二进制流数据,那么上面的加字符分割的方式就不好使了,那么就可以服务端和客户端两边约定好定长的数据格式进行分界了。

在服务端的pipeline中加上一个 FixedLengthFrameDecoder 类,并将长度设置为客户端报文请求长度

new FixedLengthFrameDecoder(FixedLengthEchoClient.REQUEST.length())

服务端中通过下面这段代码将响应数的长度放回给客户端

ctx.writeAndFlush(Unpooled.copiedBuffer(FixedLengthEchoServer.RESPONSE.getBytes()))

客户端中也加上这个 FixedLengthFrameDecoder 定长实现类,设置响应段的长度

new FixedLengthFrameDecoder(FixedLengthEchoServer.RESPONSE.length())

客户端的handler中在发送数据时,通过一下两行代码发送数据

msg = Unpooled.buffer(FixedLengthEchoClient.REQUEST.length());
msg.writeBytes(FixedLengthEchoClient.REQUEST.getBytes());
2.3.3,使用长度字段解码器

这个看起来和上面那个好像,上面那个是固定长度的解码器,但是这个使用的是动态的长度字段解码器,就是每个包的大小都告诉服务端,服务端根据一些内部的偏移量等去解析数据

pipeline.addLast(new LengthFieldBasedFrameDecoder(1024,   // 最大帧长度0,      // 长度字段的偏移量4,      // 长度字段的字节数0,      // 长度字段的调整值4       // 跳过长度字段的字节数
));

如使下面这段示例,基于长度字段进行拆分帧,并且在发送消息时,在消息头自动加上4个字节长度

// 解码器: 基于长度字段拆分帧
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
// 编码器: 在发送消息时,自动在消息前加上长度字段
ch.pipeline().addLast(new LengthFieldPrepender(4));

在客户端的handler中,通过 Unpooled.copiedBuffer 将数据封装成 ByteBuf 传递

String message = "Hello from client";
ByteBuf buf = Unpooled.copiedBuffer(message.getBytes());

服务端在响应的时候也需要加上这段,将数据响应给客户端

ByteBuf response = Unpooled.copiedBuffer("Message received".getBytes());
ctx.writeAndFlush(response);

http://www.ppmy.cn/news/1521625.html

相关文章

【网络安全】漏洞挖掘

漏洞描述 Spring框架为现代基于java的企业应用程序(在任何类型的部署平台上)提供了一个全面的编程和配置模型。 Spring Cloud 中的 serveless框架 Spring Cloud Function 中的 RoutingFunction 类的 apply 方法将请求头中的“spring.cloud.function.routing-expression”参数…

8、Django Admin后台中添加Logo

在项目settings.py文件 # 导入os&#xff0c;并且修改DIRS内容如下所示 import os TEMPLATES [{BACKEND: django.template.backends.django.DjangoTemplates,DIRS: [os.path.join(BASE_DIR, templates/)],APP_DIRS: True,OPTIONS: {context_processors: [django.template.con…

Nginx运维规范及安全配置

1.禁止在location字段对所有请求进行转发 location / {root html;index index.html idindex.htm;proxy_pass http://100.x.x.x:xxx/; }没有对url请求进行过滤&#xff0c;将所有请求转发到后台服务&#xff0c;会导致攻击类的URL被转发到后台&#xff0c;存在安全隐患 禁止使用…

glsl着色器学习(四)

前面讲到已经创建了程序对象&#xff0c;链接到顶点着色器和片段着色器&#xff0c;接着继续。 const positionLoc gl.getAttribLocation(prg, position); const normalLoc gl.getAttribLocation(prg, normal); const texcoordLoc gl.getAttribLocation(prg, texcoord);cons…

数据结构:(LeetCode101)对称二叉树

给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&#xff1a; 树中节点数目在范围…

无需更换摄像头,无需施工改造,降低智能化升级成本的智慧工业开源了。

智慧工业视觉监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;从而大大减少企业级应用约95%的开发成本。用户只需在界面上…

数理金融工程毕业之后求职应用方向,量化交易方面如何

炒股自动化&#xff1a;申请官方API接口&#xff0c;散户也可以 python炒股自动化&#xff08;0&#xff09;&#xff0c;申请券商API接口 python炒股自动化&#xff08;1&#xff09;&#xff0c;量化交易接口区别 Python炒股自动化&#xff08;2&#xff09;&#xff1a;获取…

国产隔离放大器:增强信号完整性和系统安全性的指南

隔离放大器是电子领域的关键组件&#xff0c;特别是在信号完整性和电气隔离至关重要的应用中。这些放大器隔离输入和输出信号&#xff0c;使它们能够在没有直接电气连接的情况下跨不同系统传输数据。这确保了电路一部分的高压尖峰或噪声不会影响另一部分&#xff0c;从而保护了…

随机森林的知识博客:原理与应用

随机森林&#xff08;Random Forest&#xff09;是一种基于决策树的集成学习算法&#xff0c;它通过组合多棵决策树的预测结果来提升模型的准确性和稳健性。随机森林具有强大的分类和回归能力&#xff0c;广泛应用于各种机器学习任务。本文将详细介绍随机森林的原理、构建方法及…

(C++ STL)容器适配器stack、queue、priority_queue的简单实现与源码

容器适配器stack、queue、priority_queue 一、容器适配器二、deque容器1.deque的原理介绍2.deque的特点3.选择deque作为stack和queue的底层默认容器 三、stack简单实现与源码四、queue简单实现与源码五、priority_queue简单实现与源码 以下代码环境为 VS2022 C。 一、容器适配…

sqli-labs靶场(56-60)

56关 ?id-1)union select 1,2,database()-- 看数据库 ?id-1) union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadatabase()-- 看表 ?id-1) union select 1,group_concat(column_name),3 from information_schema.columns wh…

Mysql8 主从复制主从切换(超详细)

文章目录 1 主从复制1.1 实施前提1.2 主节点配置(在192.168.25.91操作)1.3 从节点配置(在192.168.25.92操作)1.4 创建用于主从同步的用户1.5 开启主从同步1.5 主从同步验证 2 主从切换2.1 实施前提2.2 主节点设置只读(在192.168.25.91操作)2.3 检查主从数据是否同步完毕(在192.…

IC 设计前端到后端的流程和 EDA 工具?

IC设计前端也称逻辑设计&#xff0c;后端设计也称物理设计&#xff0c;两者并没有严格的界限&#xff0c;一般涉及到 与工艺有关的设计就是后端设计。 1&#xff1a;规格制定&#xff1a;客户向芯片设计公司提出设计要求。 2&#xff1a;详细设计&#xff1a;芯片设计公司&am…

2024年上海松江启动建筑绿色低碳发展专项检查,共绘城市节能新篇章

2024年9月4日&#xff0c;2024年度松江区建筑工程绿色低碳发展工作专项检查会议正式开展&#xff0c;会议内容主要围绕以下三点&#xff0c; 1、《关于开展 2024年度本市建筑领域绿色低碳发展工作监督检查的通知》宣贯。 2、分项计量、能效测评工作验收要求介绍。 3、专项检…

【初出江湖】分布式之什么是分布式存储?

目录标题 分布式存储分布式存储系统特点分布式存储原理分布式存储的应用场景分布式存储和集中式存储的区别 分布式存储 分布式存储是一种将数据分散存储在多个节点上的存储方式。与传统的集中式存储相比&#xff0c;分布式存储将数据分布在多个节点上&#xff0c;每个节点都可…

2024 年高教社杯全国大学生数学建模竞赛题目-A 题 “板凳龙” 闹元宵

“板凳龙”&#xff0c;又称“盘龙”&#xff0c;是浙闽地区的传统地方民俗文化活动。人们将少则几十条&#xff0c; 多则上百条的板凳首尾相连&#xff0c;形成蜿蜒曲折的板凳龙。盘龙时&#xff0c;龙头在前领头&#xff0c;龙身和龙尾 相随盘旋&#xff0c;整体呈圆盘状。一…

【论文阅读】Single-Stage Visual Query Localization in Egocentric Videos

paper&#xff1a;[2306.09324] Single-Stage Visual Query Localization in Egocentric Videos (arxiv.org) code&#xff1a;hwjiang1510/VQLoC: (NeurIPS 2023) Open-set visual object query search & localization in long-form videos (github.com) 简介 动机&…

Flask中多app应用怎么完成

在Flask中实现多app应用&#xff0c;通常指的是在同一个Flask项目中运行多个相对独立的应用&#xff08;或称为模块、组件&#xff09;。这种需求在大型项目中尤为常见&#xff0c;因为它可以帮助开发者更好地组织代码&#xff0c;提高项目的可维护性和可扩展性。以下将详细阐述…

11.2.软件系统分析与设计-数据库分析与设计

数据库分析与设计 数据库分析与设计的步骤

深入了解CSS滤镜属性filter

CSS的filter属性是一个非常强大的工具&#xff0c;它允许你向元素应用各种图形效果&#xff0c;如模糊、亮度、对比度、色彩调整等&#xff0c;而无需使用图像编辑软件或额外的HTML元素。filter属性可以包含多个函数&#xff0c;这些函数通过空格分隔&#xff0c;并可以叠加使用…