Netty系列(二):Netty拆包/沾包问题的解决方案

news2024/12/23 9:37:04

上一篇说到Netty系列(一):Springboot整合Netty,自定义协议实现,本文聊一些拆包/沾包问题。

拆包/沾包问题

TCP是面向字节流的协议,在发送方发送的若干包数据到接收方接收时,这些数据包可能会被粘成一个数据包,而从接收缓冲区看,后一包数据的头紧接着前一包数据的尾,这就形成沾包问题

但如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP 就会将其拆分为多次发送,这就是拆包问题,也就是将一个大的包拆分为多个小包进行发送,接收端接收到多个包才能组成一个完整数据。

为什么UDP没有粘包?

粘包/拆包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包/拆包问题。

而TCP是面向字节流,没有边界,操作系统在发送 TCP 数据的时候,底层会有一个缓冲区,通过这个缓冲区来进行优化,例如缓冲区为1024个字节大小,如果一次发送数据量小于1024,则会合并多个数据作为一个数据包发送;如果一次发送数据量大于1024,则会将这个包拆分成多个数据包进行发送。上述两种情况也是沾包和拆包问题。

img
上图出现的四种情况包括:

  1. 正常发送,两个包恰好满足TCP缓冲区的大小或达到TCP等待时长,分别发送两个包。
  2. 沾包:D1、D2都过小,两者进行了沾包处理。
  3. 拆包沾包:D2过大,进行了拆包处理,而拆出去的一部分D2_1又与D1进行粘包处理。
  4. 沾包拆包:D1过大,进行了拆包处理,而拆出去的一部分D1_2又与D2进行粘包处理。

解决方案

对于粘包和拆包问题,通常可以使用这四种解决方案:

  1. 使用固定数据长度进行发送,发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0等填充到指定长度再发送。
  2. 发送端在每个包的末尾使用固定的分隔符,例如##@##。如果发生拆包需等待多个包发送过来之后再找到其中的##@##进行合并。如果发送沾包则找到其中的##@##进行拆分。
  3. 将消息分为头部和消息体,头部中保存整个消息的长度,这种情况下接收端只有在读取到足够长度的消息之后,才算是接收到一个完整的消息。
  4. 通过自定义协议进行粘包和拆包的处理。

Netty拆包沾包处理

Netty对解决粘包和拆包的方案做了抽象,提供了一些解码器(Decoder)来解决粘包和拆包的问题。如:

LineBasedFrameDecoder:以行为单位进行数据包的解码,使用换行符\n或者\r\n作为依据,遇到\n或者\r\n都认为是一条完整的消息。
DelimiterBasedFrameDecoder:以特殊的符号作为分隔来进行数据包的解码。
FixedLengthFrameDecoder:以固定长度进行数据包的解码。
LenghtFieldBasedFrameDecode:适用于消息头包含消息长度的协议(最常用)。
基于Netty进行网络读写的程序,可以直接使用这些Decoder来完成数据包的解码。对于高并发、大流量的系统来说,每个数据包都不应该传输多余的数据(所以补齐的方式不可取),LenghtFieldBasedFrameDecode更适合这样的场景。

LineBasedFrameDecoder

使用LineBasedFrameDecoder解决粘包问题,其会根据"\n"或"\r\n"对二进制数据进行拆分,封装到不同的ByteBuf实例中

    /**
     * 服务启动器
     *
     * @return
     */
    @Bean
    public ServerBootstrap serverBootstrap() {
        ServerBootstrap serverBootstrap = new ServerBootstrap()
                // 指定使用的线程组
                .group(boosGroup(), workerGroup())
                // 指定使用的通道
                .channel(NioServerSocketChannel.class)
                // 指定连接超时时间
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyProperties.getTimeout())          
                // 通过换行符处理沾包/拆包
                .childHandler(new NettyServerLineBasedHandler());
        return serverBootstrap;
    }
public class NettyServerLineBasedHandler extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 使用LineBasedFrameDecoder解决粘包问题,其会根据"\n"或"\r\n"对二进制数据进行拆分,封装到不同的ByteBuf实例中,并且每次查找的最大长度为1024字节
        pipeline.addLast(new LineBasedFrameDecoder(1024, true, true));
        // 将上一步解码后的数据转码为Message实例
        pipeline.addLast(new MessageDecodeHandler());
        // 对发送客户端的数据进行编码
        pipeline.addLast(new MessageEncodeHandler());
        // 对数据进行最终处理
        pipeline.addLast(new ServerListenerHandler());
    }
}

DelimiterBasedFrameDecoder

以特殊的符号作为分隔来进行数据包的解码,上文中就是以##@##作为分割符作为示例展开讲解的。这里再粘贴一下关键代码:
使用DelimiterBasedFrameDecoder处理拆包/沾包,并且每次查找的最大长度为1024字节。

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // 数据分割符
        String delimiterStr = "##@##";
        ByteBuf delimiter = Unpooled.copiedBuffer(delimiterStr.getBytes());
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 使用自定义分隔符处理拆包/沾包,并且每次查找的最大长度为1024字节
        pipeline.addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
        // 将上一步解码后的数据转码为Message实例
        pipeline.addLast(new MessageDecodeHandler());
        // 对发送客户端的数据进行编码,并添加数据分隔符
        pipeline.addLast(new MessageEncodeHandler(delimiterStr));
        // 对数据进行最终处理
        pipeline.addLast(new ServerListenerHandler());
    }

MessageEncodeHandler对发送数据进行添加分割符并编码操作

public class MessageEncodeHandler extends MessageToByteEncoder<Message> {
    // 数据分割符
    String delimiter;

    public MessageEncodeHandler(String delimiter) {
        this.delimiter = delimiter;
    }

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf out) throws Exception {
        out.writeBytes((message.toJsonString() + delimiter).getBytes(CharsetUtil.UTF_8));
    }
}

FixedLengthFrameDecoder

服务端代码设置,在NettyConfig配置中将worker处理器改为NettyServerFixedLengthHandler,使用固定100字节长度处理消息。

    /**
     * 服务启动器
     *
     * @return
     */
    @Bean
    public ServerBootstrap serverBootstrap() {
        ServerBootstrap serverBootstrap = new ServerBootstrap()
                // 指定使用的线程组
                .group(boosGroup(), workerGroup())
                // 指定使用的通道
                .channel(NioServerSocketChannel.class)
                // 指定连接超时时间
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyProperties.getTimeout())
                // 指定为固定长度字节的处理器
                .childHandler(new NettyServerFixedLengthHandler());
        return serverBootstrap;
    }

NettyServerFixedLengthHandler类代码,使用FixedLengthFrameDecoder设置按固定100字节数去拆分接收到的ByteBuf。并自定义一个消息编码器,对字节长度不足100字节的消息进行补0操作。

public class NettyServerFixedLengthHandler extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // 固定字节长度
        Integer length = 100;
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 按固定100字节数拆分接收到的ByteBuf的解码器
        pipeline.addLast(new FixedLengthFrameDecoder(length));
        // 将上一步解码后的数据转码为Message实例
        pipeline.addLast(new MessageDecodeHandler());
        // 对发送客户端的数据进行自定义编码,并设置字节长度不足补0
        pipeline.addLast(new MessageEncodeFixedLengthHandler(length));
        // 对数据进行最终处理
        pipeline.addLast(new ServerListenerHandler());
    }
}

自定义MessageEncodeFixedLengthHandler编码类,使用固定字节长度编码消息,字节长度不足时补0。


public class MessageEncodeFixedLengthHandler extends MessageToByteEncoder<Message> {
    private int length;

    public MessageEncodeFixedLengthHandler(int length) {
        this.length = length;
    }


    /**
     * 使用固定字节长度编码消息,字节长度不足时补0
     *
     * @param ctx the {@link ChannelHandlerContext} which this {@link MessageToByteEncoder} belongs to
     * @param msg the message to encode
     * @param out the {@link ByteBuf} into which the encoded message will be written
     * @throws Exception
     */
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        String jsonStr = msg.toJsonString();
        // 如果长度不足,则进行补0
        if (jsonStr.length() < length) {
            jsonStr = addSpace(jsonStr);
        }
        // 使用Unpooled.wrappedBuffer实现零拷贝,将字符串转为ByteBuf
        ctx.writeAndFlush(Unpooled.wrappedBuffer(jsonStr.getBytes()));
    }

    /**
     * 如果没有达到指定长度进行补0
     *
     * @param msg
     * @return
     */
    private String addSpace(String msg) {
        StringBuilder builder = new StringBuilder(msg);
        for (int i = 0; i < length - msg.length(); i++) {
            builder.append(0);
        }
        return builder.toString();
    }
}

LenghtFieldBasedFrameDecode

LenghtFieldBasedFrameDecode适用于消息头包含消息长度的协议,根据消息长度判断是否读取完一个数据包。

    /**
     * 服务启动器
     *
     * @return
     */
    @Bean
    public ServerBootstrap serverBootstrap() {
        ServerBootstrap serverBootstrap = new ServerBootstrap()
                // 指定使用的线程组
                .group(boosGroup(), workerGroup())
                // 指定使用的通道
                .channel(NioServerSocketChannel.class)
                // 指定连接超时时间
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyProperties.getTimeout())
                // 请求头包含数据长度
                .childHandler(new NettyServerLenghtFieldBasedHandler());
        return serverBootstrap;
    }
public class NettyServerLenghtFieldBasedHandler extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 请求头包含数据长度,根据长度进行沾包拆包处理
        /**
         * maxFrameLength:指定了每个包所能传递的最大数据包大小;
         * lengthFieldOffset:指定了长度字段在字节码中的偏移量;
         * lengthFieldLength:指定了长度字段所占用的字节长度;
         * lengthAdjustment:对一些不仅包含有消息头和消息体的数据进行消息头的长度的调整,这样就可以只得到消息体的数据,这里的lengthAdjustment指定的就是消息头的长度;
         * initialBytesToStrip:对于长度字段在消息头中间的情况,可以通过initialBytesToStrip忽略掉消息头以及长度字段占用的字节。
         */
        pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));
        // 在请求头添加字节长度字段
        pipeline.addLast(new LengthFieldPrepender(2));
        // 将上一步解码后的数据转码为Message实例
        pipeline.addLast(new MessageDecodeHandler());
        // 对发送客户端的数据进行编码,字节长度不足补0
        pipeline.addLast(new MessageEncodeHandler());
        // 对数据进行最终处理
        pipeline.addLast(new ServerListenerHandler());
    }
}

总结

造成TCP协议粘包/拆包问题的原因是TCP协议数据传输是基于字节流的,它不包含消息、数据包等概念,是无界的,需要应用层协议自己设计消息的边界,即消息帧(Message Framing)。如果应用层协议没有使用基于长度或者基于分隔符(终结符)划分边界等方式进行处理,则会导致多个消息的粘包和拆包。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/30803.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux笔记

一。基础思想 一切皆文件。 两条权限原则&#xff1a; 权限分组原则权限最小原则 su是切换用户&#xff0c;而sudo则是用root权限执行某操作&#xff08; 普通用户sudo安全&#xff09; Linux目录 系统只存在一颗文件树、从/开始&#xff0c;所有的文件都挂载在这个节点上。…

JaCoCo增量覆盖率的基本实现原理

什么是增量覆盖率 如图所示&#xff0c;在master分支提交了HelloController&#xff0c;然后从master拉了个新分支test&#xff1b;提交了第1次代码&#xff0c;增加了WorldController&#xff1b;提交了第2次代码&#xff0c;增加了DonController。增量的获取方式有两种&#…

报表工具使用教程-FineReport决策报表导出Plus

前言 通过决策报表导出插件&#xff0c;用户可以将单张决策报表导出为 Excel &#xff0c;PDF&#xff0c;Word 格式文件。 那么用户如何将决策报表导出为 PPT 或 Image 格式文件呢&#xff1f;如何将多张决策报表合并导出至一个文件呢&#xff1f; 1.实现思路 用户通过安装…

静态时序分析简明教程(七)]端口延迟

端口延迟一、写在前面1.1 快速导航链接二、端口延迟2.1 输入有效2.2 输出有效2.3 set_input_delay2.3.1 -clock clock_name2.3.2 -clock_fall2.3.3 -level_sensitive2.3.4 -rise/fall2.3.5 min/max2.3.6 -add_delay2.3.7 时钟延迟2.4 set_output_delay三、总结一、写在前面 一…

点击化学FAM荧光素:6-FAM-alkyne,FAM alkyne 6-isomer,6-炔基-羧基荧光素

【中文名称】6-炔基-羧基荧光素 【英文名称】 FAM alkyne,6-isomer&#xff0c;6-FAM-alkyne 【CAS】478801-49-9 【分子式】C24H15NO6 【分子量】413.39 【纯度标准】95% 【包装规格】25mg&#xff0c;50mg&#xff0c;100mg 【是否接受定制】可进行定制&#xff0c;定制时间周…

Kubernetes安装可视化界面

安装可视化界面编写配置文件安装kubernetes-dashboard创建访问账号访问可视化界面dashboard是kubernetes官方提供的可视化界面。 https://github.com/kubernetes/dashboard编写配置文件 创建配置文件存放目录并切换到其中&#xff1a; mkdir /usr/local/kubernetes-dashboard…

java面试强基(10)

Exception 和 Error 有什么区别&#xff1f; 在 Java 中&#xff0c;所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类: Exception :程序本身可以处理的异常&#xff0c;可以通过 catch 来进行捕获。Exception 又可以分为 Checke…

Twitter网红账号营销,一定不能做的事

做社媒运营&#xff0c;我们都会创建一个官方账号与粉丝进行互动沟通&#xff0c;及时通知我们的新活动、产品&#xff0c;也是我们与粉丝建立联系的一个渠道方法。 推特群推王提示&#xff0c;虽然有这么多的好处&#xff0c;但是&#xff0c;也是有很多事项需要注意的&#…

服务器抓包简介

1、微服务服务器上抓包 2、在nginx服务器上抓包 1、服务器安装抓包软件 yum install -y tcpdump 2、服务器抓包命令 tcpdump -i any -s 0 -vvv -w /opt/qqgh.cap port 8080&#xff08;本服务器该服务的实际ip地址&#xff09; tcpdump -i eth0 host 10.30.224.170 -w result.…

14.函数的使用

函数的概念 函数是c语言的功能单位&#xff0c;实现一个功能可以封装成一个函数来实现。 定义函数的时候一切以功能为目的&#xff0c;根据功能去定函数的参数和返回值。 函数的分类 1.从定义角度分类&#xff08;即函数是谁实现的&#xff09; 库函数&#xff08;c库实现的…

Fedora怎么设置主菜单快捷键? Fedora快捷键的设置方法

Fedora主菜单可以设置打开快捷键&#xff0c;该怎么设置呢&#xff1f;下面我们就来看看Fedora快捷键的操作方法。 同时按【ALTF2】&#xff0c;输入gnome-terminal&#xff0c;打开终端。 单击右上角的主菜单按钮。 单击【配置文件首选项】。 单击【快捷键】。 单击【显示主菜…

使用DIV+CSS进行网页布局设计【HTML节日介绍网站——二十四节气】

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

【linux】进程概念

文章目录前言进程状态一、普遍的操作系统1、运行状态2、阻塞状态小结&#xff08;重要知识点&#xff09;3、新建/就绪状态4、挂起状态小结二、linux操作系统Linux内核源代码1、运行状态&#xff08;R&#xff09;2、&#xff08;浅度&#xff09;睡眠状态&#xff08;S&#x…

HTML CSS JS 网页设计作业「我的家乡吉林」

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

HTML小游戏12 —— 汽车赛道飙车游戏(附完整源码)

&#x1f482; 网站推荐:【神级源码资源网】【摸鱼小游戏】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 想寻找共同学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】&#x1f4ac; 免费且实用的计…

【WSL】【Opencv】【C++】在windows中使用WSL开发C++程序的环境搭建

文章目录基本环境Ubuntu安装Opencv从源码安装opencv-4.x从源码安装opencv-3.x直接pkg包安装CLion工程cmake文件写法demo基本环境 &#xff08;1&#xff09;安装WSL&#xff1b; &#xff08;2&#xff09;安装cmake&#xff1a; wget https://github.com/Kitware/CMake/rele…

作为运维你还在想要不要学Python?听听运维老司机怎么说

概述 今天闲聊下为什么我比较建议运维人员去学python… 建议运维一定要会开发 &#xff08;文末送读者福利&#xff09; 现阶段&#xff0c;掌握一门开发语言已经成为高级运维工程师的必备技能&#xff0c;不会开发&#xff0c;你就不能充分理解你们系统的业务流程&#xff…

SpringSecurity(十六)---OAuth2的运行机制(中)-密码、客户端凭据授权类型以及刷新令牌

一、前言 本篇将讨论剩余的授权类型以及使用刷新令牌重新获得令牌等内容&#xff0c;仍然以概念为主。下一节我们将通过一个SSO实例让大家对授权码授权类型更加熟悉。 二、实现密码授权类型 此授权类型也被称为资源所有者凭据授权类型。使用此流程的应用程序会假定客户端收集…

Spark 3.0 - 6.ML 自定义 Transformer 踩坑大全

目录 一.新征程开始 - 道阻且长 二.从源码入手 - 一探究竟 1.Tokenizer 2.UnaryTransformer 三.取源码精髓 - 照猫画虎 四.从实际出发 - 小试牛刀 五.扫重重障碍 - 补阙挂漏 1.Task not serializable 2.jsonEncode only supports string 3.Invisibility Error 4.Non…

补充(三)完善保密性三种定义的通俗表述及等价性的证明

目录 DEFINITION 2.3 完美保密加密方案的定义 LEMMA 2.5 完美保密方案的等价定义(一) LEMMA 2.7 完美保密方案的等价定义(二) 三个等价定义的通俗描述 等价性的证明&#xff08;手写过程&#xff09; DEFINITION 2.3 完美保密加密方案的定义 一个在明文空间M上的加密方案…