Netty-2-数据编解码

解析编解码支持的原理

以编码为例,要将对象序列化成字节流,你可以使用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.mfbz.cn/a/266738.html

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

相关文章

3dsmax渲染太慢,用云渲染农场多少钱?

对于许多从事计算机图形设计的创作者来说&#xff0c;渲染速度慢是一个常见问题&#xff0c;尤其是对于那些追求极致出图效果的室内设计师和建筑可视化师&#xff0c;他们通常使用3ds Max这样的工具&#xff0c;而高质量的渲染经常意味着长时间的等待。场景复杂、细节丰富&…

案例144:基于微信小程序的自修室预约系统

文末获取源码 开发语言&#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 小程序…

在Excel中,如何简单快速地删除重复项,这里提供详细步骤

当你在Microsoft Excel中使用电子表格时&#xff0c;意外地复制了行&#xff0c;或者如果你正在制作其他几个电子表格的合成电子表格&#xff0c;你将遇到需要删除的重复行。这可能是一项非常无脑、重复、耗时的任务&#xff0c;但有几个技巧可以让它变得更简单。 删除重复项 …

华为vrrp+mstp+ospf+dhcp+dhcp relay配置案例

1、左边是vlan 10主桥&#xff0c;右边是vlan 20的主桥&#xff0c;并且互为备桥 2、 vlan 10 vrrp网关默认用左边&#xff0c;vlan 20的vrrp 网关默认用右边&#xff0c;对应mstp生成树 3、两边都track检测&#xff0c;不通就把vrrp减掉60&#xff0c;这样就会自动切另一边了 …

循环神经⽹络中的梯度算法GRU

1. 什么是GRU 在循环神经⽹络中的梯度计算⽅法中&#xff0c;我们发现&#xff0c;当时间步数较⼤或者时间步较小时&#xff0c;**循环神经⽹络的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸&#xff0c;但⽆法解决梯度衰减的问题。**通常由于这个原因&#xff0…

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

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

使用vue-qr,报错in ./node_modules/vue-qr/dist/vue-qr.js

找到node_modules—>vue-qr/dist/vue-qr.js文件&#xff0c;搜…e,将…去掉&#xff0c;然后重新运行项目。

2023的AI工具集合,google和claude被禁用解决和edge的copilot

一、前言 AI工具集合 首先&#xff0c;OpenAI的ChatGPT以其深度学习模型和强大的语言处理能力引领了AI聊天机器人的潮流。自2022年11月30日上线以来&#xff0c;它创下了100万用户的注册记录&#xff0c;并被广泛应用于全球财富500强公司。为了实现盈利&#xff0c;OpenAI发布…

AOSP源码下载方法,解决repo sync错误:android-13.0.0_r82

篇头 最近写文章&#xff0c;反复多次折腾AOSP代码&#xff0c;因通过网络repo sync aosp代码&#xff0c;能一次顺利下载的概率很低&#xff0c;以前就经常遇到&#xff0c;但从未总结&#xff0c;导致自己也要回头检索方法&#xff0c;所以觉得可以总结一下&#xff0c;涉及…

网络基础知识制作网线了解、集线器、交换机与路由器

目录 一、网线的制作 1.1、材料 1.2、网线的标准类别 二、集线器、交换机介绍 2.1、概念&#xff1a; 2.2、OSI七层模型 2.3、TCP/IP四层 三、路由器的配置 3.1、概念 3.2、四个模块 1、 网络状态 2、设备管理 3、应用管理 无人设备接入控制 无线桥接 信号调节…

代码随想录算法训练营Day7 | 344.反转字符串、541.反转字符串||、替换数字、151.反转字符串中的单词、右旋字符串

LeetCode 344 反转字符串 本题思路&#xff1a;反转字符串比较简单&#xff0c;定义两个指针&#xff0c;一个 i 0, 一个 j s.length-1。然后定义一个临时变量 tmp&#xff0c;进行交换 s[i] 和 s[j]。 class Solution {public void reverseString(char[] s) {int i 0;int …

python分割字符串 split函数

split函数用于字符串的分割&#xff0c;可以完成基于特定字符将字符串分割成若干部分并存于列表中 url "http://localhost:5000/static/images/DICOMDIR 5.26-6.1/10283674_GOUT_5_0_2.png" # 获取最后一个_的前一个数字 parts url.split(/) print(parts)在这里讲…

Python----静态Web服务器-返回指定页面数据

1. 静态Web服务器的问题 目前的Web服务器&#xff0c;不管用户访问什么页面&#xff0c;返回的都是固定页面的数据&#xff0c;接下来需要根据用户的请求返回指定页面的数据 返回指定页面数据的实现步骤: 获取用户请求资源的路径根据请求资源的路径&#xff0c;读取指定文件…

Dockerfile ENTRYPOINT 执行shell脚本后自动退出

在Dockerfile文件中&#xff0c;最后一步是在入口处启动服务或执行一些部署脚本&#xff0c;例如&#xff1a; # 运行启动脚本 ENTRYPOINT ["/bin/bash","/home/deploy/run_admin_server.sh"]脚本是这样写的&#xff1a; rm -f /home/workspace/*.jar cd…

基于Java (spring-boot)的在线考试管理系统

一、项目介绍 系统功能说明 1、系统共有管理员、老师、学生三个角色&#xff0c;管理员拥有系统最高权限。 2、老师拥有考试管理、题库管理、成绩管理、学生管理四个模块。 3、学生可以参与考试、查看成绩、试题练习、留言等功能 二、作品包含 三、项目技术 后端语言&#xff1…

【错误记录/js】保存octet-stream为文件后数据错乱

目录 说在前面场景解决方式其他 说在前面 后端&#xff1a;go、gin浏览器&#xff1a;Microsoft Edge 120.0.2210.77 (正式版本) (64 位) 场景 前端通过点击按钮来下载一些文件&#xff0c;但是文件内容是一些非文件形式存储的二进制数据。 后端代码 r : gin.Default()r.Stat…

ubuntu20.04安装timeshift最新方法

总结&#xff1a; 现在可以使用如下代码安装 sudo apt-get update sudo apt-get install timeshift原因&#xff1a; 在尝试Timeshift系统备份与还原中的方法时&#xff0c; sudo apt-add-repository -y ppa:teejee2008/ppa运行失败。 更改为以下代码&#xff1a; sudo a…

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

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

Android UID相关知识

一、UID/PID/GID/GIDS的含义和作用 UID : android中uid用于标识一个应用程序&#xff0c;uid在应用安装时被分配&#xff0c;并且在应用存在于手机上期间&#xff0c;都不会改变。一个应用程序只能有一个uid&#xff0c;多个应用可以使用sharedUserId 方式共享同一个uid&#…

Torchvision中的Transforms的使用

一、transforms结构及用法 查看tansforms.py说明文档&#xff1a; ToTensor类作用是&#xff1a;将一个PIL图片或numpy形式转换成tensor的数据类型 python的用法-》tensor数据类型 通过 transforms.ToTensor去看两个问题 1、transforms该如何使用(python) 2、为什么我们需要Te…
最新文章