Netty-2-数据编解码

news2025/1/16 13:41:13

解析编解码支持的原理

以编码为例,要将对象序列化成字节流,你可以使用MessageToByteEncoder或MessageToMessageEncoder类。

在这里插入图片描述
这两个类都继承自ChannelOutboundHandlerAdapter适配器类,用于进行数据的转换。

其中,对于MessageToMessageEncoder来说,如果把口标设置为ByteBuf,那么效果等同于使用MessageToByteEncodero这就是它们都可以进行数据编码的原因。

//MessageToMessageEncoder

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        // 创建一个CodecOutputList对象,并将其初始化为null
        CodecOutputList out = null;
        try {
            // 检查消息是否满足输出条件
            if (acceptOutboundMessage(msg)) {
                // 创建一个CodecOutputList对象,并将其赋值给out变量
                out = CodecOutputList.newInstance();
                // 将msg强制转换为I类型,并赋值给cast变量
                @SuppressWarnings("unchecked")
                I cast = (I) msg;
                try {
                    // 调用encode方法,将ctx、cast和out作为参数传入
                    encode(ctx, cast, out);
                } catch (Throwable th) {
                    // 释放cast的引用计数
                    ReferenceCountUtil.safeRelease(cast);
                    // 抛出异常
                    PlatformDependent.throwException(th);
                }
                // 释放cast的引用计数
                ReferenceCountUtil.release(cast);

                // 检查out是否为空
                if (out.isEmpty()) {
                    // 抛出编码异常
                    throw new EncoderException(
                            StringUtil.simpleClassName(this) + " must produce at least one message.");
                }
            } else {
                // 直接将msg写入通道
                ctx.write(msg, promise);
            }
        } catch (EncoderException e) {
            // 抛出编码异常
            throw e;
        } catch (Throwable t) {
            // 抛出编码异常
            throw new EncoderException(t);
        } finally {
            // 最终,释放out的引用计数
            if (out != null) {
                try {
                    // 获取out的元素个数
                    final int sizeMinusOne = out.size() - 1;
                    if (sizeMinusOne == 0) {
                        // 将out的第一个元素直接写入通道
                        ctx.write(out.getUnsafe(0), promise);
                    } else if (sizeMinusOne > 0) {
                        // 检查promise是否为voidPromise
                        if (promise == ctx.voidPromise()) {
                            // 使用voidPromise来减少GC压力
                            writeVoidPromise(ctx, out);
                        } else {
                            // 使用writePromiseCombiner方法来减少GC压力
                            writePromiseCombiner(ctx, out, promise);
                        }
                    }
                } finally {
                    // 释放out的资源
                    out.recycle();
                }
            }
        }
    }

 protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;

最终的目标是把对象转换为ByteBuf,具体的转换代码则委托子类继承的encode方法来实现。

Netty提供了很多子类来支持前面提及的各种数据编码方式。
在这里插入图片描述

解析典型Netty数据编解码的实现

HttpObjectEncoder编码器

//HttpObjectEncoder编码器
    @Override
    @SuppressWarnings("ConditionCoveredByFurtherCondition")
    protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
        // 为了处理不需要类检查的常见模式的fast-path
        if (msg == Unpooled.EMPTY_BUFFER) {
            out.add(Unpooled.EMPTY_BUFFER);
            return;
        }
        // 以这种顺序进行instanceof检查的原因是,不依赖于ReferenceCountUtil::release作为一种通用释放机制,
        // 参见https://bugs.openjdk.org/browse/JDK-8180450。
        // https://github.com/netty/netty/issues/12708包含有关先前版本的此代码如何与JIT instanceof优化交互的更多详细信息。
        if (msg instanceof FullHttpMessage) {
            encodeFullHttpMessage(ctx, msg, out);
            return;
        }
        // 判断msg是否为HttpMessage的实例
        if (msg instanceof HttpMessage) {
            final H m;
            try {
                // 将msg转换为H类型
                m = (H) msg;
            } catch (Exception rethrow) {
                // 出现异常时,释放msg的引用计数并抛出异常
                ReferenceCountUtil.release(msg);
                throw rethrow;
            }
            // 判断m是否为LastHttpContent的实例
            if (m instanceof LastHttpContent) {
                // 调用encodeHttpMessageLastContent方法对LastHttpContent进行编码
                encodeHttpMessageLastContent(ctx, m, out);
            } 
            // 判断m是否为HttpContent的实例
            else if (m instanceof HttpContent) {
                // 调用encodeHttpMessageNotLastContent方法对HttpContent进行编码
                encodeHttpMessageNotLastContent(ctx, m, out);
            } 
            // m既不是LastHttpContent也不是HttpContent的实例
            else {
                // 调用encodeJustHttpMessage方法对m进行编码
                encodeJustHttpMessage(ctx, m, out);
            }
        } 
        // msg不是HttpMessage的实例
        else {
            // 调用encodeNotHttpMessageContentTypes方法对非HttpMessage的内容类型进行编码
            encodeNotHttpMessageContentTypes(ctx, msg, out);
        }

    }

HttpObjectDecoder解码器

//HttpObjectDecoder.java

    /**
     * 定义了一个私有枚举类型State,表示不同的状态
     */
    private enum State {
        /**
         * 用于跳过控制字符
         */
        SKIP_CONTROL_CHARS,
        
        /**
         * 读取初始内容
         */
        READ_INITIAL,
        
        /**
         * 读取头部信息
         */
        READ_HEADER,
        
        /**
         * 读取可变长度的内容
         */
        READ_VARIABLE_LENGTH_CONTENT,
        
        /**
         * 读取固定长度的内容
         */
        READ_FIXED_LENGTH_CONTENT,
        
        /**
         * 读取分块大小
         */
        READ_CHUNK_SIZE,
        
        /**
         * 读取分块内容
         */
        READ_CHUNKED_CONTENT,
        
        /**
         * 读取分块分隔符
         */
        READ_CHUNK_DELIMITER,
        
        /**
         * 读取分块脚注
         */
        READ_CHUNK_FOOTER,
        
        /**
         * 错误消息
         */
        BAD_MESSAGE,
        
        /**
         * 升级协议
         */
        UPGRADED
    }

//解码器相应实现
 @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
        // 如果 resetRequested 为真
        if (resetRequested) {
            // 调用 resetNow() 方法
            resetNow();
        }

        switch (currentState) {
        case SKIP_CONTROL_CHARS:
            // 跳过控制字符
        case READ_INITIAL: try {
            // 解析缓冲区中的数据
            AppendableCharSequence line = lineParser.parse(buffer);
            if (line == null) {
                return;
            }
            // 拆分初始行
            String[] initialLine = splitInitialLine(line);
            if (initialLine.length < 3) {
                // 初始行无效 - 忽略
                currentState = State.SKIP_CONTROL_CHARS;
                return;
            }

            // 创建消息对象
            message = createMessage(initialLine);
            currentState = State.READ_HEADER;
            // 继续读取头部
        } catch (Exception e) {
            // 处理异常情况
            out.add(invalidMessage(buffer, e));
            return;
        }
        case READ_HEADER: try {
            State nextState = readHeaders(buffer);
            if (nextState == null) {
                return;
            }
            currentState = nextState;
            switch (nextState) {
            case SKIP_CONTROL_CHARS:
                // 快速路径
                // 无需期望任何内容
                out.add(message);
                out.add(LastHttpContent.EMPTY_LAST_CONTENT);
                resetNow();
                return;
            case READ_CHUNK_SIZE:
                if (!chunkedSupported) {
                    throw new IllegalArgumentException("不支持分块消息");
                }
                // 分块编码 - 首先生成HttpMessage。后续将跟随HttpChunks。
                out.add(message);
                return;
            default:
                /**
                 * <a href="https://tools.ietf.org/html/rfc7230#section-3.3.3">RFC 7230, 3.3.3</a> 规定,如果请求没有传输编码头或内容长度头,则消息体长度为0。
                 * 但是对于响应,body长度是在服务器关闭连接之前接收到的字节数目。因此我们将此情况视为可变长度的分块编码。
                 */
                long contentLength = contentLength();
                if (contentLength == 0 || contentLength == -1 && isDecodingRequest()) {
                    out.add(message);
                    out.add(LastHttpContent.EMPTY_LAST_CONTENT);
                    resetNow();
                    return;
                }

                assert nextState == State.READ_FIXED_LENGTH_CONTENT ||
                        nextState == State.READ_VARIABLE_LENGTH_CONTENT;

                out.add(message);

                if (nextState == State.READ_FIXED_LENGTH_CONTENT) {
                    // 随着READ_FIXED_LENGTH_CONTENT状态逐块读取数据,分块大小将减小。
                    chunkSize = contentLength;
                }

                // 在这里返回,这将强制再次调用解码方法,在那里我们将解码内容
                return;
            }
        } catch (Exception e) {
            out.add(invalidMessage(buffer, e));
            return;
        }
        case READ_VARIABLE_LENGTH_CONTENT: {
            // 一直读取数据直到连接结束。
            int toRead = Math.min(buffer.readableBytes(), maxChunkSize);
            if (toRead > 0) {
                // 从缓冲区中读取指定长度的数据,并以保留引用的形式分割成多个片段
                ByteBuf content = buffer.readRetainedSlice(toRead);
                out.add(new DefaultHttpContent(content));
            }
            return;
        }
        case READ_FIXED_LENGTH_CONTENT: {
            int readLimit = buffer.readableBytes();

            // 首先检查缓冲区是否可读,因为我们使用可读字节计数来创建HttpChunk。需要这样做,以防止创建包含空缓冲区的HttpChunk,从而被当作最后一个HttpChunk进行处理。
            // 参见:https://github.com/netty/netty/issues/433
            if (readLimit == 0) {
                return;
            }

            int toRead = Math.min(readLimit, maxChunkSize);
            if (toRead > chunkSize) {
                toRead = (int) chunkSize;
            }
            ByteBuf content = buffer.readRetainedSlice(toRead);
            chunkSize -= toRead;

            if (chunkSize == 0) {
                // 读取所有内容。
                out.add(new DefaultLastHttpContent(content, validateHeaders));
                resetNow();
            } else {
                out.add(new DefaultHttpContent(content));
            }
            return;
        }
        /**
         * 从这里开始处理读取分块的内容。基本上,读取分块大小,读取分块,忽略CRLF,然后重复直到分块大小为0
         */
        case READ_CHUNK_SIZE: try {
            AppendableCharSequence line = lineParser.parse(buffer);
            if (line == null) {
                return;
            }
            int chunkSize = getChunkSize(line.toString());
            this.chunkSize = chunkSize;
            if (chunkSize == 0) {
                currentState = State.READ_CHUNK_FOOTER;
                return;
            }
            currentState = State.READ_CHUNKED_CONTENT;
            // fall-through
        } catch (Exception e) {
            out.add(invalidChunk(buffer, e));
            return;
        }
        case READ_CHUNKED_CONTENT: {
            // 判断chunkSize是否小于等于Integer的最大值
            assert chunkSize <= Integer.MAX_VALUE;
            // 计算本次需要读取的字节数,取chunkSize和maxChunkSize中的较小值
            int toRead = Math.min((int) chunkSize, maxChunkSize);
            // 如果不允许部分chunk,且buffer中可读取的字节数小于toRead,则返回
            if (!allowPartialChunks && buffer.readableBytes() < toRead) {
                return;
            }
            // 如果buffer中可读取的字节数小于toRead,则将toRead更新为buffer中可读取的字节数
            toRead = Math.min(toRead, buffer.readableBytes());
            // 如果toRead为0,则返回
            if (toRead == 0) {
                return;
            }
            // 从buffer中获取长度为toRead的slice,并用其创建HttpContent对象
            HttpContent chunk = new DefaultHttpContent(buffer.readRetainedSlice(toRead));
            // 更新剩余的chunkSize
            chunkSize -= toRead;

            // 将chunk添加到out中

            // 如果chunkSize不为0,则返回
            if (chunkSize != 0) {
                return;
            }
            // 设置当前状态为READ_CHUNK_DELIMITER
            currentState = State.READ_CHUNK_DELIMITER;
            // 继续执行下一个case语句
            // fall-through
        }
        case READ_CHUNK_DELIMITER: {
            // 读取分隔符
            final int wIdx = buffer.writerIndex();
            int rIdx = buffer.readerIndex();
            while (wIdx > rIdx) {
                byte next = buffer.getByte(rIdx++);
                if (next == HttpConstants.LF) {
                    currentState = State.READ_CHUNK_SIZE;
                    break;
                }
            }
            buffer.readerIndex(rIdx);
            return;
        }
        case READ_CHUNK_FOOTER: {
            try {
                // 读取尾部的Http头部信息
                LastHttpContent trailer = readTrailingHeaders(buffer);
                if (trailer == null) {
                    return;
                }
                out.add(trailer);
                resetNow();
                return;
            } catch (Exception e) {
                // 发生异常时,将异常信息和当前buffer一起添加到输出channel
                out.add(invalidChunk(buffer, e));
                return;
            }
        }
        case BAD_MESSAGE: {
            // 直到断开连接为止,丢弃消息
            buffer.skipBytes(buffer.readableBytes());
            break;
        }
        case UPGRADED: {
            int readableBytes = buffer.readableBytes();
            if (readableBytes > 0) {
                // 读取可读字节数,如果大于0,则执行以下操作
                // 由于否则可能会触发一个DecoderException异常,其他处理器会在某个时刻替换此codec为升级的协议codec来接管流量。
                // 参见 https://github.com/netty/netty/issues/2173
                out.add(buffer.readBytes(readableBytes));
            }
            break;
        }
        default:
            break;
        }



}

自定义编解码

下面先实现一个Netty编码处理程序。

public class OrderProtocolEncoder extends MessageToMessageEncoder<ResponseMessage> {
    /**
     * 编码器类,用于将ResponseMessage对象编码为ByteBuf对象并添加到输出列表中
     */
    @Override
    protected void encode(ChannelHandlerContext ctx, ResponseMessage responseMessage, List<Object> out) throws Exception {
        /**
         * 获取一个ByteBuf对象用于存储编码后的数据
         */
        ByteBuf buffer = ctx.alloc().buffer();
        /**
         * 对ResponseMessage对象进行编码,并将编码后的数据写入ByteBuf对象中
         */
        responseMessage.encode(buffer);
        /**
         * 将编码后的ByteBuf对象添加到输出列表中
         */
        out.add(buffer);
    }
}

接下来,再实现对应的Netty解码处理程序。

/**
 * 订单协议解码器
 */
public class OrderProtocolDecoder extends MessageToMessageDecoder<ByteBuf> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out) throws Exception {
        // 创建一个请求消息对象
        RequestMessage requestMessage = new RequestMessage();
        // 对字节缓冲区进行解码,将解码后的消息填充到请求消息对象中
        requestMessage.decode(byteBuf);
        // 将请求消息对象添加到输出列表中
        out.add(requestMessage);
    }
}

最后,将这对编解码处理程序添加到处理程序流水线(pipeline)中就可以完成集成工作了。

这是我们第一次提及处理程序流水线这个概念。在这里,只需要将它理解成"一串”有序的处理程序集合并有一个初步印象即可,后续会详细介绍相关内容。

为了完成处理程序流水线的设置,还要构建ServerBootstrap这个“启动”对象。

        ServerBootstrap serverBootstrap = new ServerBootstrap();  // 创建一个ServerBootstrap对象

        serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {  // 为子通道设置ChannelInitializer处理器
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {  // 初始化连接通道
                ChannelPipeline pipeline = ch.pipeline();  // 获取通道的编排器
                // 省略其他非核心代码
                pipeline.addLast("protocolDecoder", new OrderProtocolDecoder());  // 添加一个解码器到通道的最后
                pipeline.addLast("protocolEncoder", new OrderProtocolEncoder());  // 添加一个编码器到通道的最后
                // 省略其他非核心代码
            }
        });

常见疑问解析

为什么Netty自带的编解码方案很少有人使用

其中个很重要的因素就是历史原因,但实际上,除历史原因之外,更重要的原因在于Netty自带的编解码方案大多是具有封帧和解帧功能的编解码器,并且融两层编码于一体,因此从结构上看并不清晰。

另外,Netty自带的编解码方案在使用方式上不够灵活。

在进行序列化和反序列时,字段的顺序弄反了

我们在序列化对象的字段时,使用的顺序是a b c;但是,等到我们解析时,顺序可能不小心写成了 c b a, 因此,我们一定要完全对照好顺序才行。

编解码的顺序问题

有时候,我们往往采用多层编解码。
例如,在得到可传输的字节流之后,我们可能想压缩一下以进一步减少所传输内容占用的空间。
此时,多级编解码就可以派上用场了:对于发送者, 先编码后压缩;而对于接收者,先解压后解码。

但是,代码的添加顺序和我们想要的顺序不一定完全匹配。如果顺序错了,那么代码可能无法工作。

if (compressor != null) {
    pipeline.addLast("frameDecompressorn", new Frame.Decompressor(compressor));
    pipeline.addLast("frameCompressor", new Frame.Compressor(compressor));

    pipeline.addLast("messageDecoder", messageDecoder);
    pipeline.addLast("messageEncoder", messageEncoderFor(protocolversion));
}

在这里插入图片描述
处理程序对于读取操作和写出操作的执行顺序刚好是相反的。

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

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

相关文章

数据结构-如何巧妙实现一个栈?逐步解析与代码示例

文章目录 引言1.栈的基本概念2.选择数组还是链表&#xff1f;3. 定义栈结构4.初始化栈5.压栈操作6.弹栈操作7.查看栈顶和判断栈空9.销毁栈操作10.测试并且打印栈内容栈的实际应用结论 引言 栈是一种基本但强大的数据结构&#xff0c;它在许多算法和系统功能中扮演着关键角色。…

智能优化算法应用:基于天鹰算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于天鹰算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于天鹰算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.天鹰算法4.实验参数设定5.算法结果6.参考文献7.MA…

oracle恢复分片和非分片备份?

分片备份命令参考&#xff1a;适合大数据库并行备份提高备份速度 对于超大数据库&#xff0c;混合有小文件和大文件表空间&#xff0c;section size 表示分片&#xff0c;大小一般大于32G&#xff0c;可结合通道数量设置最佳值。 run { allocate channel t1 type disk; alloc…

PostGreSQL:货币类型

货币类型&#xff1a;money money类型存储固定小数精度的货币数字&#xff0c;小数的精度由数据库的lc_monetary设置决定。windows系统下&#xff0c;该配置项位于/data/postgresql.conf文件中&#xff0c;默认配置如下&#xff0c; lc_monetary Chinese (Simplified)_Chi…

redis基本用法学习(C#调用CSRedisCore操作redis)

除了NRedisStack包&#xff0c;csredis也是常用的redis操作模块&#xff08;从EasyCaching提供的常用redis操作包来看&#xff0c;CSRedis、freeredis、StackExchange.Redis应该都属于常用redis操作模块&#xff09;&#xff0c;本文学习使用C#调用CSRedis包操作redis的基本方式…

【Spring Security】打造安全无忧的Web应用--使用篇

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Spring Security的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Spring Security中的授权是…

阿贝云云服务器

最近&#xff0c;我有幸获得了阿贝云提供的免费云服务器&#xff0c;阿贝云_免费云服务器、高防服务器、虚拟主机、免费空间、免费vps主机服务商!并在使用过程中有了一些深刻的体验和感受。在这篇博客中&#xff0c;我将分享我对阿贝云免费云服务器的使用感受和评价。 首先&am…

【iOS】UICollectionView

文章目录 前言一、实现简单九宫格布局二、UICollectionView中的常用方法和属性1.UICollectionViewFlowLayout相关属性2.UICollectionView相关属性 三、协议和代理方法&#xff1a;四、九宫格式的布局进行升级五、实现瀑布流布局实现思路实现原理代码调用顺序实现步骤实现效果 总…

需求分析工程师岗位的职责描述(合集)

需求分析工程师岗位的职责描述1 职责&#xff1a; 1&#xff0c;负责需求调研&#xff0c;对需求进行分析&#xff0c;编写解决方案、需求规格说明书等 2&#xff0c;根据需求制作原型&#xff0c;并负责原型展示以及客户沟通等工作 3&#xff0c;负责向技术团队精确地传达业务…

排序算法——桶排序

把数据放进若干个桶&#xff0c;然后在桶里用其他排序&#xff0c;近乎分治思想。从数值的低位到高位依次排序&#xff0c;有几位就排序几次。例如二位数就排两次&#xff0c;三位数就排三次&#xff0c;依次按照个十百...的顺序来排序。 第一次排序&#xff1a;50 12 …

Unity手机移动设备重力感应

Unity手机移动设备重力感应 一、引入二、介绍三、测试成果X Y轴Z轴横屏的手机&#xff0c;如下图竖屏的手机&#xff0c;如下图 一、引入 大家对重力感应应该都不陌生&#xff0c;之前玩过的王者荣耀的资源更新界面就是使用了重力感应的概念&#xff0c;根据手机的晃动来给实体…

EPROM 作为存储器的 8 位单片机

一、基本概述 TX-P01I83 是以 EPROM 作为存储器的 8 位单片机&#xff0c;专为多 IO 产品的应用而设计&#xff0c;例如遥控器、风扇/灯光控制或是 玩具周边等等。采用 CMOS 制程并同时提供客户低成本、高性能等显着优势。TX-P01I83 核心建立在 RISC 精简指 令集架构可以很容易…

SQL优化-深入了解SQL处理流程原理(Server层与存储引擎交互、数据管理结构)

做SQL优化的前提就必须要清楚当一个SQL被发送到Mysql时&#xff0c;它的处理流程。下面通过一个SQL优化分析过程来详细了解Mysql对SQL执行流程原理。 1、Mysql架构 在上篇文章中已经做了简单架构介绍&#xff0c;Mysql架构分为两个大的组件&#xff1a;Server层、存储层 Mysq…

vue3项目 - Eslint 配置代码风格

Eslint 自定义配置 总结&#xff1a; Prettier &#xff08;代码规范的插件&#xff0c;格式化 &#xff09;---> 美观 Eslint &#xff08;规范、纠错、检验错误 &#xff09;-----> 纠错 首先&#xff0c;禁用 Prettier 插件&#xff0c;安装 ESLint 插件&#x…

【量化金融】证券投资学

韭菜的自我修养 第一章&#xff1a; 基本框架和概念1.1 大盘底部形成的技术条件1.2 牛市与熊市1.3 交易系统1.3.1 树懒型交易系统1.3.2 止损止损的4个技术 第二章&#xff1a;证券家族4兄弟2.1 债券&#xff08;1&#xff09;债券&#xff0c;是伟大的创新&#xff08;2&#x…

案例147:基于微信小程序的酒店管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

重构云计算,打造 AI 原生时代的云计算产品与技术体系,实现 AI 零距离

概述 自 ChatGPT 大模型横空出世以来&#xff0c;文心一言、通义千问等诸多大模型接踵而来&#xff0c;感觉这个世界每天都在发生着翻天覆地的变化。 今年很有幸&#xff0c;参与了云栖的盛宴&#xff0c;当时被震惊到瞠目结舌&#xff0c;12 月 20 日百度云智能云智算大会&a…

使用Python自己写了一个Renpy 汉化插件

之前看了很多教程都是Renpy怎么解包、怎么创建翻译文件&#xff0c;对翻译过程没有过多讲解&#xff0c;就根据翻译文件写了一个小程序&#xff0c;如果需要的可以自行下载使用。 使用方法&#xff1a; 1.按照正常unrpa的解包方式renpy进行解包&#xff1b; 2.使用renpy-sdk…

Bloom过滤器

Bloom过滤器 一、概述二、原理三、优缺点1. 优点2.缺点 四、Bloom过滤器在比特币中的应用五、项目应用步骤1. pom.xml引入依赖2. 样例代码 六、Java版简易实现 一、概述 Bloom过滤器是一个允许用户描述特定的关键词组合而不必精确表述的基于概率的过滤方法。它能让用户在有效搜…

新型智慧城市解决方案:PPT全文56页,附下载

关键词&#xff1a;智慧城市解决方案&#xff0c;智慧城市管理技术&#xff0c;智慧城市建设&#xff0c;数字城市建设 一、智慧城市宏观形势 1、政策支持&#xff1a;出台了一系列政策&#xff0c;鼓励和支持智慧城市的发展。这些政策为智慧城市的建设提供了政策保障和资金支…