[Netty源码] 编码和解码相关问题 (十二)

news2025/1/24 22:49:42

文章目录

      • 1.编码和解码的介绍
      • 2.相关继承
      • 3.解码器分析
        • 3.1 ByteToMessageDecoder基类
        • 3.2 FixedLengthFrameDecoder
        • 3.3 LineBasedFrameDecoder
        • 3.4 DelimiterBasedFrameDecoder
        • 3.5 LengthFieldBasedFrameDecoder
      • 4.编码器分析
        • 4.1 解码过程分析
        • 4.2 writeAndFlush方法分析
        • 4.3 MessageToByteEncoder抽象类

1.编码和解码的介绍

  • 解码器:负责将消息从字节或其他序列形式转成指定的消息对象。数据流 IO转为ByteBuf转为业务逻辑
  • 编码器:将消息对象转成字节或其他序列形式在网络上传输。对象转为数据流
  • Netty里面的编解码: 解码器:负责处理“入站 InboundHandler”数据。 编码器:负责“出站OutboundHandler” 数据。

Netty 的编(解)码器实现了 ChannelHandlerAdapter,也是一种特殊的 ChannelHandler,所以依赖于 ChannelPipeline,可以将多个编(解)码器链接在一起,以实现复杂的转换逻辑。

2.相关继承

解码器(Decoder)

解码器负责 解码“入站”数据从一种格式到另一种格式,解码器处理入站数据是抽象ChannelInboundHandler的实现。需要将解码器放在ChannelPipeline中。对于解码器,Netty中主要提供了抽象基类ByteToMessageDecoderMessageToMessageDecoder

在这里插入图片描述

  • ByteToMessageDecoder: 用于将字节转为消息,需要检查缓冲区是否有足够的字节
  • MessageToMessageDecoder: 用于从一种消息解码为另外一种消息(例如POJO到POJO)
decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out)

编码器(Encoder)

Netty提供了对应的编码器实现MessageToByteEncoder和MessageToMessageEncoder,二者都实现ChannelOutboundHandler接口。

在这里插入图片描述

  • MessageToByteEncoder: 将消息转化成字节
  • MessageToMessageEncoder: 用于从一种消息编码为另外一种消息(例如POJO到POJO)
encode(ChannelHandlerContext ctx, String msg, List<Object> out)

编码解码器Codec

同时具有编码与解码功能,特点同时实现了ChannelInboundHandler和ChannelOutboundHandler接口,因此在数据输入和输出时都能进行处理。

在这里插入图片描述

Netty提供提供了一个ChannelDuplexHandler适配器类,编码解码器的抽象基类ByteToMessageCodec ,MessageToMessageCodec都继承与此类。

3.解码器分析

ByteToMessageDecoder相关类

在这里插入图片描述

  • ByteToMessageDecoder: 基类
  • FixedLengthFrameDecoder: 基于固定长度解码器
  • LineBasedFrameDecoder: 基于行解码器
  • DelimiterBasedFrameDecoder: 基于分隔符解码器
  • LengthFieldBasedFrameDecoder: 基于长度域解码器

3.1 ByteToMessageDecoder基类

  1. 累加字节流
  2. 调用子类的decode方法解析
  3. 解析的ByteBuf向下传播
    //1,读信息
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //如果msg为ByteBuf,则进行解码,否则直接透传
        if (msg instanceof ByteBuf) {
            RecyclableArrayList out = RecyclableArrayList.newInstance();
            try {
                ByteBuf data = (ByteBuf) msg;
                //判断cumulation是否为空,来判断是否缓存了半包消息。
                first = cumulation == null;
                if (first) {
                    //为空则是没有半包消息,直接复制
                    cumulation = data;
                } else {
                    //有半包消息,则需要将data复制进行,进行组合。
                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
                }
                // 子类进行不同的解码
                callDecode(ctx, cumulation, out);
            } catch (DecoderException e) {
                throw e;
            } catch (Throwable t) {
                throw new DecoderException(t);
            } finally {
                if (cumulation != null && !cumulation.isReadable()) {
                    cumulation.release();
                    cumulation = null;
                }
                int size = out.size();
 
                for (int i = 0; i < size; i ++) {
                	// 传播ByteBuf
                    ctx.fireChannelRead(out.get(i));
                }
                // 回收对象
                out.recycle();
            }
        } else {
        	// 传播ByteBuf
            ctx.fireChannelRead(msg);
        }
    }
 
    protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        try {
            while (in.isReadable()) {
                int outSize = out.size();
                int oldInputLength = in.readableBytes();
                //调用用户实现的decode方法
                decode(ctx, in, out);
 
                if (ctx.isRemoved()) {
                    break;
                }
                //1,如果用户解码器没有消费ByteBuf,则说明是半包消息,需要继续读取后续的数据,直接退出循环;2,如果用户解码器消费了ByteBuf,说明可以继续进行;
                if (outSize == out.size()) {
                    if (oldInputLength == in.readableBytes()) {
                        break;
                    } else {
                        continue;
                    }
                }
                //3,如果用户解码器没有消费ByteBuf,但是却多解码出一个或多个对象,则为异常
                if (oldInputLength == in.readableBytes()) {
                    throw new DecoderException(
                            StringUtil.simpleClassName(getClass()) +
                            ".decode() did not read anything but decoded a message.");
                }
                //4,如果是单条消息解码器,第一次解码完成之后直接退出循环。
                if (isSingleDecode()) {
                    break;
                }
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable cause) {
            throw new DecoderException(cause);
        }
    }

callDecode(ctx, cumulation, out)

调用子类的decode解析

在这里插入图片描述

在这里插入图片描述

根据子类不同的实现去实现decode解析数据。

3.2 FixedLengthFrameDecoder

基于固定长度解码器

在这里插入图片描述

如果基于3的长度去解析的话, 那么 A|BC|DEFG|HI 解析为 ABC|DEF|GHI

    protected Object decode(
            @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        if (in.readableBytes() < frameLength) {
            return null;
        } else {
            return in.readRetainedSlice(frameLength);
        }
    }

3.3 LineBasedFrameDecoder

行解析器: 看行是否有 \n 和 \r 的换行符号, 如果全部有的话看 \r

是否是丢弃模式, discarding = true, 为丢失模式, 为false表示不可以丢失

    private boolean discarding;
	// 找到行的最后的位置: \n结尾 或是 \r\n结尾
    private int findEndOfLine(final ByteBuf buffer) {
        int totalLength = buffer.readableBytes();
        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset,  new IndexOfProcessor((byte) '\n'));
        if (i >= 0) {
            offset = 0;
            if (i > 0 && buffer.getByte(i - 1) == '\r') {
                i--;
            }
        } else {
            offset = totalLength;
        }
        return i;
    }
   protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        final int eol = findEndOfLine(buffer);
        // 判断如果当前是非丢弃模式
        if (!discarding) {
        	// 非丢弃模式下的找到换行符,  当前存在 \n 或 \r\n
            if (eol >= 0) {
                final ByteBuf frame;
                final int length = eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;

				// 如果解析的长度大于最大长度, 报错
                if (length > maxLength) {
                    buffer.readerIndex(eol + delimLength);
                    fail(ctx, length);
                    return null;
                }

				// 是否将分隔符也加入到数据包中
                if (stripDelimiter) {
                    frame = buffer.readRetainedSlice(length);
                    buffer.skipBytes(delimLength);
                } else {
                    frame = buffer.readRetainedSlice(length + delimLength);
                }

                return frame;
            } else {
            	// 非丢弃模式下的没有分隔符的情况
                final int length = buffer.readableBytes();
                if (length > maxLength) {
                    discardedBytes = length;
                    buffer.readerIndex(buffer.writerIndex());
                    discarding = true;
                    offset = 0;
                    if (failFast) {
                        fail(ctx, "over " + discardedBytes);
                    }
                }
                return null;
            }
         // 丢弃模式
        } else {
        	// 丢弃模式下的找到换行符
            if (eol >= 0) {
                final int length = discardedBytes + eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
                buffer.readerIndex(eol + delimLength);
                discardedBytes = 0;
                discarding = false;
                if (!failFast) {
                    fail(ctx, length);
                }
            // 丢弃模式下的没有找到修饰符
            } else {
                discardedBytes += buffer.readableBytes();
                buffer.readerIndex(buffer.writerIndex());
                // We skip everything in the buffer, we need to set the offset to 0 again.
                offset = 0;
            }
            return null;
        }
    }

非丢弃模式下的找到换行符情况

在这里插入图片描述

非丢弃模式下的没有找到换行符

在这里插入图片描述

丢弃模式下, 找到换行符: 需要丢弃一些数据, 然后变为非丢弃模式的逻辑

在这里插入图片描述

丢弃模式下, 没有找到换行符: 需要先丢弃数据, 然后变为非丢弃模式的逻辑

3.4 DelimiterBasedFrameDecoder

DelimiterBasedFrameDecoder, 基于分隔符的解码器

  • 判断是否是行解码器: 分隔符为换行符
    在这里插入图片描述

  • 找到最小分隔符
    在这里插入图片描述

  • 如果找到最小分隔符的话

            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 (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 {
                // 丢弃模式
                tooLongFrameLength += buffer.readableBytes();
                buffer.skipBytes(buffer.readableBytes());
            }
            return null;

判断是否是行解码器: 分隔符为换行符

在这里插入图片描述

拿到最小分隔符的信息

在这里插入图片描述

最新分隔符为A

3.5 LengthFieldBasedFrameDecoder

LengthFieldBasedFrameDecoder 为基于长度域的解码器

  • lengthFieldOffset: 读取的标记开始
  • lengthFieldLength: 读取的长度
  • lengthAdjustment: 读取长度的加减数据
  • initialBytesToStrip: 从哪里开始读取
    在这里插入图片描述

在这里插入图片描述
0 - 2字节为后面读的数据的长度, 为12个, 最后减少0个字节还是12个, 减少0个字节读起数据

在这里插入图片描述

0 - 2字节为后面读的数据的长度, 为12个, 最后减少0个字节是12个, 从减少2个字节读起数据

在这里插入图片描述

0 - 2字节为后面读的数据的长度, 为14个, 最后减少2个字节是12个, 从减少0个字节读起数据

在这里插入图片描述

2 - 5字节为后面读的数据的长度, 为12个, 最后减少0个字节是12个, 从减少0个字节读起数据

在这里插入图片描述

1 - 3字节为后面读的数据的长度, 为12个, 最后加上1个字节是13个, 从减少3个字节读起数据

在这里插入图片描述

1 - 3字节为后面读的数据的长度, 为16个, 最后减少3个字节是13个, 从减少3个字节读起数据

  1. 计算需要抽取的数据包长度
  2. 跳过字节逻辑处理
  3. 丢弃模式下的处理
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    	// 丢弃模式下的处理
        if (discardingTooLongFrame) {
            discardingTooLongFrame(in);
        }

        if (in.readableBytes() < lengthFieldEndOffset) {
            return null;
        }
		
		// 计算需要抽取的数据包长度
        int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
        long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);

		// 计算需要抽取的数据包长度
        frameLength += lengthAdjustment + lengthFieldEndOffset;

        int frameLengthInt = (int) frameLength;
        if (in.readableBytes() < frameLengthInt) {
            return null;
        }

        if (initialBytesToStrip > frameLengthInt) {
            failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip);
        }
        in.skipBytes(initialBytesToStrip);

        // 跳过字节逻辑处理
        int readerIndex = in.readerIndex();
        int actualFrameLength = frameLengthInt - initialBytesToStrip;
        ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
        in.readerIndex(readerIndex + actualFrameLength);
        return frame;
    }

	// 计算需要抽取的数据包长度
    protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) {
        buf = buf.order(order);
        long frameLength;
        switch (length) {
        case 1:
            frameLength = buf.getUnsignedByte(offset);
            break;
        case 2:
            frameLength = buf.getUnsignedShort(offset);
            break;
        case 3:
            frameLength = buf.getUnsignedMedium(offset);
            break;
        case 4:
            frameLength = buf.getUnsignedInt(offset);
            break;
        case 8:
            frameLength = buf.getLong(offset);
            break;
        default:
            throw new DecoderException(
                    "unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)");
        }
        return frameLength;
    }

4.编码器分析

4.1 解码过程分析

在这里插入图片描述

在这里插入图片描述

public class BizHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        User user = new User("1", 1);

        ctx.channel().writeAndFlush(user);
    }
}
public class Encoder extends MessageToByteEncoder<User> {
    @Override
    protected void encode(ChannelHandlerContext ctx, User msg, ByteBuf out) throws Exception {
        byte[] bytes = msg.getName().getBytes();
        out.writeInt(4 + bytes.length);
        out.writeInt(msg.getAge());
        out.writeBytes(bytes);
    }
}

重写encode方法

4.2 writeAndFlush方法分析

  1. 从tail节点开始往前传播
  2. 逐个调用channelHandler的write方法
  3. 逐个调用channelHandler的flush方法

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.3 MessageToByteEncoder抽象类

在这里插入图片描述

MessageToByteEncoder.write()

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        ByteBuf buf = null;
        try {
        	// 匹配对象
            if (acceptOutboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                I cast = (I) msg;
                // 分配内存
                buf = allocateBuffer(ctx, cast, preferDirect);
                try {
                	// 编码实现
                    encode(ctx, cast, buf);
                } finally {
                	// 释放对象
                    ReferenceCountUtil.release(cast);
                }

                if (buf.isReadable()) {
                	// 传播数据
                    ctx.write(buf, promise);
                } else {
                    buf.release();
                    // 传播数据
                    ctx.write(Unpooled.EMPTY_BUFFER, promise);
                }
                buf = null;
            } else {
                ctx.write(msg, promise);
            }
        } catch (EncoderException e) {
            throw e;
        } catch (Throwable e) {
            throw new EncoderException(e);
        } finally {
        	// 释放内存
            if (buf != null) {
                buf.release();
            }
        }
    }

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

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

相关文章

2007-2020年国际产权指数InternationalPropertyRightsIndex(IPRI)IPRI

2007-2020年国际产权指数InternationalPropertyRightsIndex(IPRI)IPRI 1、来源&#xff1a;国际产权联合会 International Poverty Right Alliance 2、时间&#xff1a;2007-2020 3、范围&#xff1a;全球 4、指标说明&#xff1a; country、region、score、annual change…

提高软件测试效率的6大注意事项

1、测试策略非常重要 测试策略的基础是风险评估&#xff0c;我们需要通过失效概率和失效影响两个维度&#xff0c;对风险进行高、中、低的区分和可能性的判断。如CoCode开发云能够根据风险影响等级&#xff0c;自动计算出风险系数&#xff0c;并对风险进行优先级划分。而系数超…

项目管理:我们每个人都是管理者

项目管理的技能在生活中时时能用到、处处可锻炼。只要有心&#xff0c;项目成员一样可以学习和实践项目管理知识&#xff0c;也可以说&#xff0c;我们每个人都是管理者。 1、管理是职能而不是职位 有的人认为项目管理只是管理者应该学的&#xff0c;其实不是这样的&#xf…

卷积神经网络底层原理

1.卷积神经网络底层原理 声明&#xff1a;以下为《大话计算机》作者冬瓜哥课程视频截图&#xff0c;仅供学习 1.1卷积 一张图经过一种卷积核&#xff08;核函数&#xff09;滑动窗口进行卷积运算后得到一张特征图&#xff0c;这只是这种卷积核视角下看到的特征。所以我们需要多…

一起学 WebGL:图形变形以及矩阵变换

之前绘制了三角形&#xff0c;我们现在给它做一个变形操作。 对一个三角形进行变形&#xff0c;其实就是重新这个三角形的三个顶点的位置&#xff0c;计算完后再绘制出来&#xff0c;相比原来就发生了变形。 变形常见的有位移、选择、缩放。位移&#xff0c;其实就是给每个顶…

中国版ChatGPT即将来袭-国内版ChatGPT入口

必应chatGPT入口 目前并不存在“必应ChatGPT”这个概念。必应&#xff08;Bing&#xff09;是Microsoft公司推出的一款搜索引擎&#xff0c;而ChatGPT是OpenAI开发的自然语言处理技术&#xff0c;它们是两个不同的产品品牌。 不过&#xff0c;Microsoft也在自然语言处理领域里…

Microsoft 365管理和报告工具

在管理 Microsoft 365 设置的过程中&#xff0c;本机Microsoft 365 功能可能无法满足你的需求。M365 Manager Plus 具有复杂的功能&#xff0c;使 Microsoft 365 管理毫不费力。它提供基于功能的管理&#xff0c;因此你可以单独管理 Microsoft 365 组件。 使用 M365 Manager P…

进程的概念以及PCB的概念

在linux上进程是非常重要的知识点&#xff0c;今天我自我发表浅见。 可执行程序与进程 当在linux上编译完毕一个源文件生成可执行程序&#xff0c;这个时候这可执行程序只能称为普通文件&#xff0c;还不能定义为进程&#xff0c;在加载在内存中后才可称为进程&#xff0c;那…

次优二叉查找树(次优查找树)_递归和非递归实现_20230414

次优二叉查找树&#xff08;次优查找树)-递归和非递归实现 前言 当有序表中的各记录的查找概率相等的时候&#xff0c;采用折半查找效率可以提升查找性能&#xff1b;如果有序表中的各记录的查找概率不相等&#xff0c;那么折半查找就不再适用。 如果只考虑查找成功的情况&a…

Robocup 仿真2D 学习笔记(四)阵型编辑

一、阵型文件介绍 阵型文件里设置的是球员在比赛中的跑位点 基于helios base的阵型文件&#xff0c;在目录/src/formations-dt中 阵型的调用在/src/strategy.cpp 文件&#xff1a; before-kick-off.conf 是球员上场之后的阵型 &#xff08;或进球等待开球&#xff09; no…

有限元基础编程-何晓明老师课件-一维程序实现matlab

文章目录前言一、主程序二、一维有限元求解程序-框架三、组装刚度矩阵assemble_matrix_from_1D_integral.m2.1 算法2.2 get_standard_gauss_1D.m2.3 get_Gauss_local_1D.m前言 只是为方便学习&#xff0c;不做其他用途&#xff0c;课程理论学习来自b站视频有限元基础编程-何晓明…

RT-Thread线程管理以及内核裁剪

RT-Thread线程管理以及内核裁剪 1. RTOS概述 1.1 RTOS的定义 实时操作系统&#xff08;Real-time operating system, RTOS&#xff09;&#xff0c;又称即时操作系统&#xff0c;它会按照排序运行、管理系统资源&#xff0c;并为开发应用程序提供一致的基础。 实时操作系统与…

核心业务2:借款人申请借款额度

核心业务2&#xff1a;借款人申请借款额度 1.业务流程图 ------------截止提交个人信息部分-------- 2.借款人申请借款额度数据库设计 3.借款人申请额度流程 4.前端代码逻辑 5.后端代码逻辑 ------------截止提交个人信息部分-------- 核心业务2&#xff1a;借款人申请借…

Linux从命令行管理文件

目录 一、创建链接文件 二、目录操作命令 1. 创建目录&#xff08;make directory&#xff09; 2. 统计目录及文件的空间占用情况 3. 删除目录文件 三、创建、删除普通文件 文件命名规则&#xff1a; &#xff08;1&#xff09;不能使用/来当文件名&#xff0c;/是用来做…

【WCH】CH32F203软件I2C驱动SSD1306 OLED

【WCH】CH32F203软件I2C驱动SSD1306 OLED&#x1f4cc;相关篇《【WCH】CH32F203硬件I2C驱动SSD1306 OLED》&#x1f4fa;驱动显示效果&#xff1a; &#x1f33f;OLED屏幕&#xff1a;i2c ssd1306 oled&#x1f516;驱动单片机型号&#xff1a;CH32F203 ✨由于CH32F203主频为96…

wordpress下载插件,安装失败,无法创建目录问题

刚开始安装这个wordpress&#xff0c;在发表文章时候想要在其中加上图片&#xff0c;不想一个个手动上传媒体库&#xff0c;耽误时间&#xff0c;然后就去下了个imagepaste这个复制粘贴的插件&#xff0c;当我打开安装插件搜索到的时候准备安装&#xff0c;尼玛出现“安装失败&…

若一个单词被拆分成多少token, word_ids得到的序号是相同的?还是序号累加的?

目录 问题描述&#xff1a; 问题实现&#xff1a; 方法一&#xff1a; 方法二&#xff1a; 问题描述&#xff1a; 在使用tokenizer进行编码的时候&#xff0c;经常会存在word被拆分成多个token的情况&#xff0c;不同的参数设置&#xff0c;会得到不同的结果。总的来说&…

redis——使用

session缓存缓存更新方式删除缓存vs更新缓存缓存和数据库操作原子性缓存和数据库操作顺序结论缓存问题缓存穿透缓存雪崩缓存击穿全局唯一ID数据并发线程安全单体分布式redis分布式锁的问题redis消息队列listpubsubstream消息推送session 问题&#xff1a;session存在tomcat服务…

【Linux驱动开发】023 platform设备驱动

一、前言 驱动分离目的&#xff1a;提高Linux代码重用性和可移植性。 二、驱动的分隔与分离 百度看了很多&#xff0c;大多都没讲清楚为什么使用platform驱动&#xff0c;为什么驱动分隔与分离可以提高代码重用性&#xff0c;只是在讲实现的结构体、函数接口等等&#xff0c…

npm、pnpm、yarn的常用命令

npm、pnpm、yarn的常用命令 文章目录npm、pnpm、yarn的常用命令一、常用命令1、npm命令2、pnpm命令&#xff1a;3、yarn命令二、对比一、常用命令 1、npm命令 npm init: 初始化一个新的npm包。 npm install: 安装项目依赖项。 npm install : 安装指定的包。 npm install --sa…