Netty学习——高级篇2 Netty解码技术

接上篇:Netty学习——高级篇1 拆包 、粘包与编解码技术,本章继续介绍Netty的其他解码器

1 DelimiterBasedFrameDecoder分隔符解码器

        DelimiterBasedFrameDecoder 分隔符解码器是按照指定分隔符进行解码的解码器,通过分隔符可以将二进制流拆分成完整的数据包。回车换行符解码器实际上是一种特殊的DelimiterBasedFrameDecoder解码器。

        分隔符解码器在实际工作中有很广泛的应用,很多简单的文本私有协议都以特殊的分隔符作为消息结束的标识,特别是那些使用长连接的基于文本的私有协议。关于分隔符的指定,并非以Char或者String作为构造参数,而是以Bytebuf。下面就结合实际例子给出它的用法。例如消息是以“$_”作为分隔符,服务端或者客户端初始化ChannelPipeline的代码实例如下:

ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
ch.pipeline().addLast(new StringDecoder());

        DelimiterBasedFrameDecoder同样继承了ByteToMessageDecoder并重写了decode方法,来看其中的一个构造方法。

public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
    this(maxFrameLength, true, delimiter);
}

         其中,参数 maxFrameLength 代表最大长度,delimiter是个可变参数,可以支持多个分隔符进行解码。跟进decode方法。

@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    Object decoded = decode(ctx, in);
    if (decoded != null) {
        out.add(decoded);
    }
}

             

        这里同样调用了重载的decode方法并将解析好的数据添加到集合List中,其父类就可以遍历Out,并将内容传播。重载的decode方法代码如下:

protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        //行处理器(1)  
        if (lineBasedDecoder != null) {
            return lineBasedDecoder.decode(ctx, buffer);
        }
        
        int minFrameLength = Integer.MAX_VALUE;
        ByteBuf minDelim = null;
        //找到最小长度的分隔符
        for (ByteBuf delim: delimiters) {
            //每个分隔符分隔的数据包长度
            int frameLength = indexOf(buffer, delim);
            if (frameLength >= 0 && frameLength < minFrameLength) {
                minFrameLength = frameLength;
                minDelim = delim;
            }
        }
        //解码(3)
        //已经找到分隔符
        if (minDelim != null) {
            int minDelimLength = minDelim.capacity();
            ByteBuf frame;
            //当前分隔符是否处于丢弃模式
            if (discardingTooLongFrame) {
                // 首先设置为非丢弃模式
                discardingTooLongFrame = false;
                //丢弃
                buffer.skipBytes(minFrameLength + minDelimLength);

                int tooLongFrameLength = this.tooLongFrameLength;
                this.tooLongFrameLength = 0;
                if (!failFast) {
                    fail(tooLongFrameLength);
                }
                return null;
            }
            //处于非丢弃模式
            //当前找到的数据包,大于允许的数据包
            if (minFrameLength > maxFrameLength) {
                // 当前数据包+最小分隔符的长度 全部丢弃.
                buffer.skipBytes(minFrameLength + minDelimLength);
                //传递异常事件
                fail(minFrameLength);
                return null;
            }
            //如果是正常的长度
            //解析出来的数据包是否忽略分隔符
            if (stripDelimiter) {
                //如果不包含分隔符
                //截取
                frame = buffer.readRetainedSlice(minFrameLength);
                //跳过分隔符
                buffer.skipBytes(minDelimLength);
            } else {
                //截取包含分隔符的长度
                frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
            }

            return frame;
        } else {
            //如果没有找到分隔符
            //飞丢弃模式
            if (!discardingTooLongFrame) {
                //可读字节大于允许的解析出来的长度
                if (buffer.readableBytes() > maxFrameLength) {
                    // 将这个长度记录下
                    tooLongFrameLength = buffer.readableBytes();
                    //跳过这段长度
                    buffer.skipBytes(buffer.readableBytes());
                    //标记当前处于丢弃状态
                    discardingTooLongFrame = true;
                    if (failFast) {
                        fail(tooLongFrameLength);
                    }
                }
            } else {
                // Still discarding the buffer since a delimiter is not found.
                tooLongFrameLength += buffer.readableBytes();
                buffer.skipBytes(buffer.readableBytes());
            }
            return null;
        }
    }

        这个方法较长,通过拆分进行剖析:

        1、行处理器;2、找到最小长度分隔符;3、解码。首先看第一步,行处理器的代码:

if (lineBasedDecoder != null) {
    return lineBasedDecoder.decode(ctx, buffer);
}

        首先判断成员变量linebasedDecoder 是否为空,如果不为空则直接调用lineBasedDecoder的decode方法进行解析,lineBasedDecoder实际上就是LineBasedFrameDecoder解码器。这个成员变量会在分隔符是\r\n的时候进行初始化。看一下初始化该属性的构造方法:

public DelimiterBasedFrameDecoder(
        int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf... delimiters) {
    validateMaxFrameLength(maxFrameLength);
    if (delimiters == null) {
        throw new NullPointerException("delimiters");
    }
    if (delimiters.length == 0) {
        throw new IllegalArgumentException("empty delimiters");
    }
    //如果是基于行的分隔
    if (isLineBased(delimiters) && !isSubclass()) {
        //初始化行处理器
        lineBasedDecoder = new LineBasedFrameDecoder(maxFrameLength, stripDelimiter, failFast);
        this.delimiters = null;
    } else {
        this.delimiters = new ByteBuf[delimiters.length];
        for (int i = 0; i < delimiters.length; i ++) {
            ByteBuf d = delimiters[i];
            validateDelimiter(d);
            this.delimiters[i] = d.slice(d.readerIndex(), d.readableBytes());
        }
        lineBasedDecoder = null;
    }
    this.maxFrameLength = maxFrameLength;
    this.stripDelimiter = stripDelimiter;
    this.failFast = failFast;
}

        这里的 isLineBased(delimiters) 会判断是否是基于行的分隔,代码如下:

private static boolean isLineBased(final ByteBuf[] delimiters) {
    //分隔符长度不为2
    if (delimiters.length != 2) {
        return false;
    }
    //获得第一个分隔符
    ByteBuf a = delimiters[0];
    //获得第二个分隔符
    ByteBuf b = delimiters[1];
    if (a.capacity() < b.capacity()) {
        a = delimiters[1];
        b = delimiters[0];
    }
    //确保a是\r\n分隔符,确保b是\n分隔符
    return a.capacity() == 2 && b.capacity() == 1
            && a.getByte(0) == '\r' && a.getByte(1) == '\n'
            && b.getByte(0) == '\n';
}

        首先判断长度不等于2,直接返回false。然后获取第一个ByteBuf和第二个ByteBuf,判断a的第一个分隔符是不是\r,a第二个分隔符是不是\n,b的第一个分隔符是不是\n,如果都为true,则条件成立。回到decode中,看第二步,找到最小长度的分隔符。这里最小长度的分隔符,就是从读指针开始找到最近的分隔符,代码如下:

for (ByteBuf delim: delimiters) {
    //每个分隔符分隔的数据包长度
    int frameLength = indexOf(buffer, delim);
    if (frameLength >= 0 && frameLength < minFrameLength) {
        minFrameLength = frameLength;
        minDelim = delim;
    }
}

        这里会遍历所有的分隔符,找到每个分隔符到读指针的数据包长度。在通过if判断,找到长度最小的数据包的长度,然后保存当前数据包的分隔符,如下图所示:

        假设A和B同为分隔符,因为A分隔符到读指针的长度小于B分隔符到读指针的长度,所以会找到最小的分隔符A,分隔符的最小长度就是readIndex到A的长度。继续看第三步,解码。

if (minDelim != null) 表示已经找到最小长度分隔符,继续看if语句块中的逻辑,代码如下:
int minDelimLength = minDelim.capacity();
ByteBuf frame;

if (discardingTooLongFrame) {
    // We've just finished discarding a very large frame.
    // Go back to the initial state.
    discardingTooLongFrame = false;
    buffer.skipBytes(minFrameLength + minDelimLength);

    int tooLongFrameLength = this.tooLongFrameLength;
    this.tooLongFrameLength = 0;
    if (!failFast) {
        fail(tooLongFrameLength);
    }
    return null;
}

if (minFrameLength > maxFrameLength) {
    // Discard read frame.
    buffer.skipBytes(minFrameLength + minDelimLength);
    fail(minFrameLength);
    return null;
}

if (stripDelimiter) {
    frame = buffer.readRetainedSlice(minFrameLength);
    buffer.skipBytes(minDelimLength);
} else {
    frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
}

return frame;

         if (discardingTooLongFrame) 表示当前是否处于非丢弃模式,如果是丢弃模式,则进入if块中。因为第一个不是丢弃哦是,所以先分析if后面的逻辑。if (minFrameLength > maxFrameLength) 判断当前找到的数据包长度大于最大长度,这个最大长度是创建解码器的时候设置的,如果超过了最大长度,就通过 buffer.skipBytes(minFrameLength + minDelimLength);方式跳过数据包+分隔符的长度,也就是将这部分数据进行完全丢弃。继续往下看,如果长度不大于最大允许长度,则通过 if (stripDelimiter) 判断解析出来的数据包是否包含分隔符,如果不包含分隔符,则截取数据包的长度后,跳过分隔符。

        再回看 if (discardingTooLongFrame)中if里的逻辑,也就是丢弃模式。首先将discardingTooLongFrame 设置为false,标记为飞丢弃模式,然后通过buffer.skipBytes(minFrameLength + minDelimLength) 将数据包+分隔符长度的字节数跳过,也就是进行丢弃,之后抛出异常。在分析没有找到分隔符的逻辑处理,也就是if (minDelim != null) 的else语句,代码如下:

else {
    if (!discardingTooLongFrame) {
        if (buffer.readableBytes() > maxFrameLength) {
            // Discard the content of the buffer until a delimiter is found.
            tooLongFrameLength = buffer.readableBytes();
            buffer.skipBytes(buffer.readableBytes());
            discardingTooLongFrame = true;
            if (failFast) {
                fail(tooLongFrameLength);
            }
        }
    } else {
        // Still discarding the buffer since a delimiter is not found.
        tooLongFrameLength += buffer.readableBytes();
        buffer.skipBytes(buffer.readableBytes());
    }
    return null;
}

        首先通过  if (!discardingTooLongFrame) 判断是否为非丢弃模式,如果是非丢弃模式,则进入if里。在if里,通过 if (buffer.readableBytes() > maxFrameLength) 判断当前可读字节数是否大于最大允许的长度,如果大于最大允许的长度,则将可读字节数设置到tooLongFrameLength 的属性中,代表丢弃的字节数,然后通过 buffer.skipBytes(buffer.readableBytes()) 将累加器中所有的可读字节进行丢弃,最后将 discardingTooLongFrame 设置为true,也就是丢弃模式,之后抛出异常。如果 if (!discardingTooLongFrame) 结果为false,也就是当前处于丢弃模式,则追加tooLongFrameLength 也就是丢弃的字节数的长度,并通过buffer.skipBytes(buffer.readableBytes()) 将所有的字节继续进行丢弃。

2 FixedLengthFrameDecoder固定长度解码器

        FixedLengthFrameDecoder 固定长度解码器能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包和拆包等问题,非常实用。

        对于定长消息,如果消息实际长度小于定长,则往往会进行补位操作,它在一定程度长导致了空间和资源的浪费。但是它的优先也非常明显,编解码比较简单,因此在实际项目中有一定的应用场景。

        利用FixedLengthFrameDecoder 解码器,无论一次接收到多少数据报文,它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。假如单条消息的长度是20字节,使用FixedLengthFrameDecoder 解码器的效果如下:

        类的定义代码如下:

public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
    
    //长度大小
    private final int frameLength;

    public FixedLengthFrameDecoder(int frameLength) {
        if (frameLength <= 0) {
            throw new IllegalArgumentException(
                    "frameLength must be a positive integer: " + frameLength);
        }
        //保存当前的frameLength
        this.frameLength = frameLength;
    }

    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        //通过ByteBuf解码,解码到对象之后添加到Out上
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }

    
    protected Object decode(
            @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        //字节是否小于这个固定长度
        if (in.readableBytes() < frameLength) {
            return null;
        } else {
            return in.readRetainedSlice(frameLength);
        }
    }
}

        通过代码发现,FixedLengthFrameDecoder继续ByteToMessageDecoder,重写了decode方法,这个类只有一个叫 frameLength 的属性,并在构造方法中初始化了该属性。再看decode方法,在decode方法中又调用了自身另一个重载的decode方法进行解析,解析出来之后将解析后的数据放在集合Out中。再看重载的decode方法,重载的decode方法中首先判断累加器的字节数是否小于固定长度,如果小于固定长度则返回null,代表不是一个完整的数据包。如果大于等于固定长度,则直接从累加器中截取这个长度的数值,in.readRetainedSlice(frameLength) 会返回一个新的截取后的ByteBuf,并将原来的累加器读指针后移frameLength个字节。如果累加器中还有数据,则通过ByteToMessageDecoder中callDecode()方法里的while循环方式,继续进行解码。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据的插入、修改和删除

一、 插入数据 1. 向表中所有字段插入数据 &#xff08;1&#xff09; 指定所有字段及其相对应的值 insert into 表名(字段1&#xff0c;字段2&#xff0c;……) values(字段值1&#xff0c;字段值2&#xff0c;……);**【案例】**向goods表中插入一条新记录 步骤1&#xff…

密码学 | 椭圆曲线数字签名方法 ECDSA(上)

目录 1 ECDSA 是什么&#xff1f; 2 理解基础知识 3 为什么使用 ECDSA&#xff1f; 4 基础数学和二进制 5 哈希 6 ECDSA 方程 7 点加法 8 点乘法 9 陷阱门函数&#xff01; ⚠️ 原文&#xff1a;Understanding How ECDSA Protects Your Data. ⚠️ 写在前面…

Java+saas模式 智慧校园系统源码Java Android +MySQL+ IDEA 多校运营数字化校园云平台源码

Javasaas模式 智慧校园系统源码Java Android MySQL IDEA 多校运营数字化校园云平台源码 智慧校园即智慧化的校园&#xff0c;也指按智慧化标准进行的校园建设&#xff0c;按标准《智慧校园总体框架》中对智慧校园的标准定义是&#xff1a;物理空间和信息空间的有机衔接&#…

第七周学习笔记DAY.1-封装

学完本次课程后&#xff0c;你能够&#xff1a; 理解封装的作用 会使用封装 会使用Java中的包组织类 掌握访问修饰符&#xff0c;理解访问权限 没有封装的话属性访问随意&#xff0c;赋值也可能不合理&#xff0c;为了解决这些代码设计缺陷&#xff0c;可以使用封装。 面向…

【Linux系统编程】第四弹---基本指令(二)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、echo指令 2、cat指令 3、more指令 4、less指令 4、head指令 5、tail指令 6、时间相关的指令 7、cal指令 8、find指…

若依从0到1部署

服务器安装 MySQL8 Ubuntu 在 20.04 版本中&#xff0c;源仓库中 MySQL 的默认版本已经更新到 8.0&#xff0c;因此可以直接使用 apt-get 安装。 设置 apt 国内代理 打开 https://developer.aliyun.com/mirror/ 阿里云镜像站&#xff0c;找到适合自己的系统&#xff1a; 找…

记【k8s】:访问 Prometheus UI界面:kubernetes-etcd (0/1 up) Error : out of bounds

记【k8s】&#xff1a;访问 Prometheus UI界面&#xff1a;kubernetes-etcd &#xff08;0/1 up&#xff09; Error &#xff1a; out of bounds 1、报错详情2、解决方法 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 出现 “out of bound…

软件项目实施方案(Word原件2024)

软件实施方案 二、 项目介绍 三、 项目实施 四、 项目实施计划 五、 人员培训 六、 项目验收 七、 售后服务 八、 项目保障措施软件开发管理全套资料包清单&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#xff0c;产品需求规格说明书&am…

抖音IP打造品牌规划流量运营方案推广计划书

【干货资料持续更新&#xff0c;以防走丢】 抖音IP打造品牌规划流量运营方案推广计划书 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 50页可编辑&#xff08;完整资料包含以下内容&#xff09; 目录 详细的抖音运营方案&#xff0c;帮助品牌在抖音平台上提升…

MATLAB 点到平面距离的简易计算 (61)

MATLAB 点到平面的垂直距离 (61) 一、算法介绍二、算法实现1.代码2.结果一、算法介绍 一行代码计算点到平面的距离,下面是MATLAB版本的实现方法, 使用一组自定义的点和平面验证,结果表明计算正确: 二、算法实现 1.代码 代码如下(示例): % 定义点的坐标 point = …

李沐41_物体检测和数据集——自学笔记

边缘框 1.一个边缘框可以通过4个数字定义&#xff08;左上xy&#xff0c;右上xy&#xff0c;左下xy&#xff0c;右下xy&#xff09; 2.标注成本高 目标检测数据集 1.每行表示一个物体&#xff08;图片文件名、物体类别、边缘框&#xff09; 2.COCO&#xff1a;80物体、330…

ABeam德硕 | 大语言模型系列 (2) :大语言模型的商业模式及应用

上期我们分享了大语言模型的基本概念等内容&#xff0c;本期作为大语言模型系列的第二篇&#xff0c;我们将关注大语言模型的商业模式及商业价值&#xff0c;介绍MaaS (Model as a Service&#xff0c;模型即服务)的核心路径、国内外大语言模型收费方式情况及大语言模型在国内的…

串口接收第一个字节接收不到

问题描述: 我发 5 1 2 3 他接受的是1 2 3 5 全速运行第一个字节就没有了。但是把接受的数据再发出来&#xff0c;发现第一个字节到了最后一个字节。 看了网上说的&#xff0c;我一次只发一个字节不会出现这种情况&#xff0c;发多个字节全速运行就会出现这样情况&#xff0c;…

Qt 4 QPushButton

Qt 常用控件 QPushButton 实例 Push Button:命令按钮。 入口文件 main.cpp #include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);MainWindow w;w.show();return a.exec(); }头文件 mainwindow.h …

LeetCode 179 in Python. Largest Number (最大数)

寻找最大数的逻辑简单&#xff0c;但如何对两数比较组成更大的整数是一个重点。例如示例2中如何区分3与30谁放在前面以及3与34谁放在前面是一个难点&#xff0c;本文通过采用functools中的自定义排序规则cmp_to_key()来判断上述情况&#xff0c;并给出代码实现。 示例&#xf…

喜讯 | 同元软控荣获北京市科学技术奖

近日&#xff0c;2022年度北京市科学技术奖正式公布&#xff0c;由苏州同元软控信息技术有限公司&#xff08;以下简称“同元软控”&#xff09;总经理刘奇等人参与完成的“复杂航天器电气系统设计、建模与验证关键技术及应用”项目荣获北京市科学技术奖技术发明二等奖。 该项目…

element-ui container 组件源码分享

今日简单分享 container 组件的源码实现&#xff0c;从以下两个方面来讲解&#xff1a; 1、container 组件的页面结构 2、container 组件的属性 一、container 组件的页面结构 二、container 组件的属性 1、container 部分的 direction 属性&#xff0c;子元素的排列方向&am…

模仿银行系统的极简Java三层结构应用——存钱功能的实现

一&#xff0c;前提&#xff1a; 我们上次做了一个简易的银行系统&#xff0c;初步认识了java结构&#xff0c;目前该系统可以输入要用的数据并且输出。 二&#xff0c;目标&#xff1a; 我们这次的目标是实现一个简易的存钱功能&#xff0c;并输出存钱后的余额&#xff0c;…

【学习】jemter中如何高效使用正则表达式

在Jemter的世界里&#xff0c;正则表达式无疑是一把锐利的剑&#xff0c;它可以帮助我们轻松地解决许多问题。在Jemter的性能测试过程中&#xff0c;我们常常需要提取响应中的某些数据&#xff0c;以便在后续的请求中使用。这时&#xff0c;正则表达式就派上用场了。通过学习如…

MySql中truncate,delete有什么区别?什么情况下id会不会连续呢?

TRUNCATE和DELETE都是用来删除表中数据的SQL命令&#xff0c;但它们的工作方式和使用场景有所不同&#xff1a; DELETE命令&#xff1a;DELETE命令用于从表中删除一行、多行或所有行。你可以添加WHERE子句来指定要删除的行。例如&#xff0c;DELETE FROM table_name WHERE cond…