Read book Netty in action(Chapter VI)--ByteBuf

news2024/10/6 7:03:10

序言

之前学习了传输,通过前面的学习我们都知道,网络数据的基本单位是字节。JDK中提供了ByteBuffer作为字节的容器,但是过于繁琐复杂,Netty中提供了ByteBuf作为替代品。学习一下。

API

Netty的数据处理API通过两个组件暴露 ------ abstract class byteBuf 和 interface ByteBufHolder。
优点如下:
1.它可以被用户自定义的缓冲区类型拓展。
2.通过内置的复合缓冲区类型实现了透明的零拷贝。
3.容量可以按需增长。
4.切换读写形态不需要调用flip()方法。
5.读和写使用不同的索引。
6.支持方法链式调用。
7.支持引用计数。
8.支持池化。

ByteBuf类 - Netty的数据容器

因为所有的网络通信都设计字节系列的移动,所以高效易用的数据结构是不可少的。Netty的ByteBuf实现满足并超越了这些需求。

HOW TO WORK

ByteBuf维护了两个不同的索引,一个用于读,一个用于写入。当你从ByteBuf中读取的时候。他的readerIndex将会被递增已经被读取的字节数。同样的写入的时候,writerindex也会被递增。read和wirte的索引起始位置都为位置0。
如果打算读取字节直到readerIndex达到和writerIndex同样的值会发送什么。在那是,你将会达到 可以读取的 数据的末尾。类似于数据的索引溢出。
名字以read或者write开头的ByteBuf方法,将会推进器对应的索引,而名称以set或者get开头的操作则不会。后面这些方法只是会以索引为操作来执行想做的操作。
可以指定ByteBuf最大容量。试图移动写索引超过这个值就会触发一个异常!
你完全可以理解为数组,这样更好理解,因为大家对数组比较熟悉,可以借鉴一下其思想

ByteBuf的使用模式

堆缓冲区

最常用的ByteBuf模式是将数据存储在JVM的堆空间中,这种模式被称为 支撑数组,它能在没有使用池化的情况下提供快速的分配和释放。

  public static void heapBuffer() {
        ByteBuf heapBuf = Unpooled.buffer(1024);; //get reference form somewhere
        // check array
        if (heapBuf.hasArray()) {
            // get the array
            byte[] array = heapBuf.array();
            int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
            int length = heapBuf.readableBytes();
            // doSomething
            doSomething(array, offset, length);
        }
    }

适合有遗留的数据需要处理的请求。

直接缓冲区

直接缓冲区是另外一种ByteBuf模式。我们期望用于对象创建的内存分配永远都来于堆中,但是这并不是必须的-NIO在jdk1.4中引入的ByteBuffer类允许jvm通过本地调用来分配内存。这主要是避免每次在调用本地IO之前将缓冲区里的内容都复制到一个中间缓冲区。

ByteBuf在javadoc中曾指出:直接缓冲区的内容将会被驻留在 会被垃圾回收器回收的堆 之外。说白了,gc干不掉它。如果你的数据包含在一个堆上分配的缓冲区上中,在套接字发送它之前,JVM会将它复制到一个直接缓冲区中。

直接缓冲区的主要缺点是:相对于堆缓冲区,分配和释放比较昂贵,处理遗留代码时如果数据不在堆上,那么还得进行一次复制。

public static void directBuffer() {
        ByteBuf directBuf =  Unpooled.buffer(1024);; //get reference form somewhere
        if (!directBuf.hasArray()){
            int length = directBuf.readableBytes();
            byte[] bytes = new byte[length];
            directBuf.getBytes(directBuf.readerIndex(),bytes);
            doSomething(bytes,0,length);
        }
    }
复合缓冲区

第三种也是最后一种模式使用的是复合缓冲区,它为多个ByteBuf提供一个聚合视图。在这里可以根据需要添加或者删除ByteBuf实例,JDK中的ByteBuffer完全没有这个东西。Netty是通过一个ByteBuf子类 – CompositeByteBuf 为了实现这个模式,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示。

CompositeByteBuf实例可以同时包含直接内存分配和非直接内存分配。

以http为例:一个由两部分 —头部和主体— 组成的将通过HTTP协议传输的消息。这两部分由应用程序不同的模块产生,将会在消息将要发生的时候再行组装。该应用程序可以选择多个重复相同的消息主体。当这种情况发生的时候,对于每个消息都会创建一个新的头部。
因为我们不想为每个消息重新分配这两个缓冲区,所以使用CompositeByteBuf是一个完美的选择。它在消除了没必要复制的同时,暴露了ByteBuf的API。下面看一段代码,如何通过使用JDK的ByteBuffer来实现这个需求,创建了一个包含两个ByteBuffer数组来保存这些消息组件。同时创建了第三个ByteBuffer来保存所有这些数据的副本。

    public static void byteBufComposite() {
        CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
        ByteBuf headerBuf = Unpooled.buffer(1024); // can be backing or direct
        ByteBuf bodyBuf =  Unpooled.buffer(1024);  // can be backing or direct
        messageBuf.addComponents(headerBuf, bodyBuf);
        messageBuf.removeComponent(0); // remove the header
        for (ByteBuf buf : messageBuf) {
            System.out.println(buf.toString());
        }
    }

Netty使用了CompositeByteBuf来优化套接字的IO操作,尽可能消除由JDK缓冲区实现带来的性能问题和内存的问题,这种优化细节不会暴露。存在于netty核心代码中。

字节级操作

随机访问索引

和Java字节数组一样,ByteBuf的索引是从零开始的:第一个字节的索引是0,最后一个总是capacity() - 1.我们可以像遍历数组一样遍历ByteBuf

public static void main(String[] args) {
        byteBufRelativeAccess();
    }

    public static void byteBufRelativeAccess() {
        CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
        messageBuf.writeBytes("ok le".getBytes());
        for (int b = 0; b < messageBuf.capacity(); b++) {
            byte aByte = messageBuf.getByte(b);
            System.out.println(aByte);
        }
    }

顺序访问索引

虽然ByteBuf同时具有读索引和写索引,但是JDK的ByteBuffer却只有一个索引。所以需要flip()方法在两种模式之间进行来回切换的原因。

ByteBuf被两个索引切成三个部门,第一部分是已经读过的部分,可以被丢弃。第二部分自然是没有被读过的字节,可以读取。那么第三部分肯定就是可以写的空间了,可写字节。

可丢弃字节

已丢弃字节的分段包括了已经被读过的字节。通风调用 discardReadBytes()方法,可以丢弃他们并回收空间。这个分段的初始大小为0,存储在readerIndex中,会随着read的造作执行而增加。调用了这个方法之后,可丢弃的空间现在变成可写的了。这个操作只移动了可读的字节和写索引,而没有对所有可写入的字节进行擦除操作。所以这种操作虽然可以扩充写区域,但是内存的复制不可避免,总得移动读字节到开始位置吧,如果不是内存宝贵,我相信不会这么做的。

可读字节

ByteBuf的可读字节分段存储了实际数据。新分配的,包装的或者复制的缓冲区的默认readerIndex值为0.任何名称以read或者skip开头的操作都将检索或者跳过当前可读索引,并且增加已读索引。
如果被调用的方法需要一个ByteBuf参数作为写入的目标。并且没有指定目标索引参数,那么该缓冲区的写索引也会被增加,例如:readBytes(ByteBuf dest)
如果在缓冲区的可读字节数已经耗尽时次中读取数据,那么会引发一个IndexOutOfBoundsException。
如何从读取所有可以读取的字节:

  public static void readAllData() {
        ByteBuf buffer = Unpooled.compositeBuffer();
        while (buffer.isReadable()) {
            System.out.println(buffer.readByte());
        }
    }

可写字节

可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的写索引默认值为0。任何名称以write开头的操作都是从当前写操作中开始写数据,并将它增加以及写入的字节数。如果写操作的目标也是ByteBuf,并且没有指定源索引的值,则源缓冲区的readerIndex也同样会被增加相同的大小。调用如下:
writeBytes(ByteBuf dest)。如果我那个目标中写入超过目标容量的数据,将会已发一个索引越界异常。


    public static void write() {
        // Fills the writable bytes of a buffer with random integers.
        ByteBuf buffer =Unpooled.compositeBuffer(); //get reference form somewhere
        while (buffer.writableBytes() >= 4) {
            buffer.writeInt(1024);
        }
    }

索引管理

JDK的InputStream定义了mark(int readlimit)和reset()方法,这些方法分别被用来将流中的当前位置指定,以及将流重置到该位置。同样,可以通过调用markReaderIndex()、markWriterIndex()、和resetReaderIndex()来标记和重置ByteBuf的readerIndex和writeIndex。这些和InputStream中的调用别无二致,只是没有什么readlimit来指定标记何时失效。

和数组一样,我们可以通过调用readerIndex(int)或者writerIndex(int)来讲索引移动到指定位置。试图将任何一个索引设置到无效的位置将会导致索引越界异常。可以通过clear()方法将readerIndex和writerIndex都设置为0,但是并不会清理内存。clear()比discardReadBytes()清的多,他只会重置索引而非复制处理内存。

查找操作

在ByteBuf中有多种可以用来确定索引的方法。最简单的是使用IndexOf方法。较复杂的查找可以通过哪些需要一个ByteProcessor作为参数的方法达成。这个接口只定义了一个方法:
boolean process(byte value)
他将检查输入值是否是正在查找的值。
例子:

  public static void byteProcessor() {
        ByteBuf buffer = Unpooled.compositeBuffer(); //get reference form somewhere
        buffer.writeBytes("测测测测测测测测,\r".getBytes());
        int index = buffer.forEachByte(ByteProcessor.FIND_CR);
        System.out.println(index);
    }

查找回车符。

派生缓冲区

派生缓冲区为ByteBuf提供了专门的方式来呈现其内容的视图。这些类视图是通过以下方法被创建的:
duplicate();
slice();
slice(int,int);
Unpooled.unmodifiableBuffer(xxx)
order(ByteOrder)
readSlice(int)
这个方法都会返回一个新的实例,它具有自己的读索引,写索引和标记索引。其存储内部和JDK的ByteBuffer一样,都是共享的。这使得创建一个派生缓存区的代价低廉,但是这也意味着,如果你修改了它的内容,同时也修改了对应的实例。这个类似于 浅拷贝,从某种意义上来说,他们是差不多的从设计层面上和使用层面。但是又可以切片,反正有这个概念就可以。

   public static void byteBufSlice() {
        Charset utf8 = Charset.forName("UTF-8");
        ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
        ByteBuf sliced = buf.slice(0, 15);
        System.out.println(sliced.toString(utf8));
        buf.setByte(0, (byte)'J');
        System.out.println(buf.toString(utf8));
        System.out.println(sliced.toString(utf8));
        assert buf.getByte(0) == sliced.getByte(0);
    }

你会发现改了sliced,原来的buf也会改变,不妨debuf跑一下。

再对比一下copy,我刚刚以浅拷贝为例,这个就像深拷贝。

  public static void byteBufCopy() {
        Charset utf8 = Charset.forName("UTF-8");
        ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
        ByteBuf copy = buf.copy(0, 15);
        System.out.println(copy.toString(utf8));
        buf.setByte(0, (byte)'J');
        System.out.println(buf.toString(utf8));
        System.out.println(copy.toString(utf8));
        assert buf.getByte(0) != copy.getByte(0);
    }

可以自行debug看结果不同,我举深浅拷贝的例子,就可以很好理解了。

深拷贝: User id User1 id1 id改 id1不变
浅拷贝: User id User1 id1 id改 id1变

读/写操作

前面曾经提到,有两种类别的读写操作:
set / get 索引不变,从给定的索引开始
read / write 从给定的索引开始,索引变化。
API就不放出来了。直接放个书上的案例:
get 和 set

    public static void byteBufSetGet() {
        Charset utf8 = StandardCharsets.UTF_8;
        ByteBuf byteBuf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
        char char1 = (char) byteBuf.getByte(0);
        System.out.println(char1);
        int readerIndex = byteBuf.readerIndex();
        int writerIndex = byteBuf.writerIndex();
        System.out.println(readerIndex);
        System.out.println(writerIndex);
        byteBuf.setByte(0, (byte) 'W');
        System.out.println((char) byteBuf.getByte(0));
        readerIndex = byteBuf.readerIndex();
        writerIndex = byteBuf.writerIndex();
        System.out.println(readerIndex);
        System.out.println(writerIndex);
    }

read 和 write

   public static void byteBufWriteRead() {
        Charset utf8 = StandardCharsets.UTF_8;
        ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
        System.out.println((char)buf.readByte());
        int readerIndex = buf.readerIndex();
        System.out.println(readerIndex);
        int writerIndex = buf.writerIndex();
        System.out.println(writerIndex);
        buf.writeByte((byte)'?');
        System.out.println((char)buf.readByte());
        readerIndex = buf.readerIndex();
        writerIndex = buf.writerIndex();
        System.out.println(readerIndex);
        System.out.println(writerIndex);
        System.out.println(buf.toString(utf8));
    }

每次read之后,索引后移,前面数据就是废物数据了。自行debuf,一看便知。还有很多方法,使用方法自行查看API

ByteBufHolder接口

在平时开发中不难发现,处理实际的数据负载以外,我们还需要存储各种属性值。HTTP响应便是一个很好的例子,有什么Cookie,状态码之类的额外的东西。
为了处理这种常用的用例,Netty提供了ByteBufHolder接口。ByteBufHolder也为Netty的高级特性提供了支持,如缓冲区池化,和所有的池一样,从池中拿,自行释放。ByteBufHolder只有几种用于访问底层数据和引用技术的方法。

content() 返回这个ByteBufHolder所持的ByteBuf
copy() 返回ByteBufHolder的一个深拷贝,其中带一个copy副本
duplicate() 返回ByteBufHolder的一个浅拷贝,其中带一个共享副本
如果要实现一个将其有效负载储存在ByteBuf中的消息对象。可以使用ByteBufHolder。

ByteBuf分配

按需分配:ByteBufAllocator接口

为了降低分配和释放内存的开销。通过ByteBufAllocator实现了(ByteBuf)的池化。它可以用来分配我们所描述过得任何类型的ByteBuf。使用池化特定于应用程序的决定,它不会改变ByteBuf的API。
ByteBufAllocator的方法有

buffer()返回一个基于堆或者直接内存的ByteBuf
heapBuffer()返回一个基于堆内存存储的ByteBuf
directBuffer()返回一个基于直接内存的ByteByf
compostiteBuffer()返回一个可以通过添加最大到指定数目的基于堆或者直接内存存储的缓冲区来拓展的compostiteByteBuf
toBuffer()返回一个用于套接字的IO操作的ByteBuf
可以通过Channel或者绑定到ChannelHandler的ChannelHandlerContext获取到一个到ByteBufAllocator的引用。

    public static void obtainingByteBufAllocatorReference(){
        Channel channel = ...; //get reference form somewhere
        ByteBufAllocator allocator = channel.alloc();
        ChannelHandlerContext ctx = ...; //get reference form somewhere
        ByteBufAllocator allocator2 = ctx.alloc();
    }

差不多就这样。Netty实现了两种ByteBufAllocator的实现,PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的实例以提升性能并最大限度的减少内存碎片。而后者不池化,每次都会返回一个新的实例,和连接池还有线程池别无二例,池化就进池,不池化自己管理。当然了,Netty的实现是默认池化的。

Unpooled缓冲区

有时候,你无法获得一个ByteBufAllocator的引用,那么,Netty给我们提供了一个小工具类叫做:Unpooled,它提供了静态的辅助方法,来获取没有池化的ByteByf实例。方法有如下几个:
buffer() 返回一个没有池化的基于堆内存存储的ByteBuf
directBuffer() 返回一个未池化的基于直接内存存储的ByteBuf
wrappedBuffer() 返回一个包装了给定数据的ByteBuf
copiedBuffer() 返回了一个复制了给定数据的ByteBuf
我们案例中经常使用的代码你一定非常熟悉。

ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);

Unpooled类还能使得ByteBuf同样可以用于哪些并不需要netty介入的非网络项目,使得其能得益于高性能可扩展的缓冲区API。(后续考虑将其引入naruku)。

ByteBufUtil类

看名字就知道是个缓冲区的工具类。这个API是同样的,并且和池化无关。
这些方法中有个叫做 hexdump()的方法,它以十六进制的表现形式打印ByteBuf里的内容。这很有用,他可以轻易地转换回来实际的字节表示。
还有equals方法,可以比较两个ByteBuf实例的相等性。如果需要实现自己的ByteBuf子类,可能会发现其他用途。

引用计数

引用计数是通过正某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术,在netty 第四版中引入到了ByteBuf和ByteBufHolder中,他们均实现了ReferenceCounted接口

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
// ...
}
public interface ReferenceCounted {
    /**
     * Returns the reference count of this object.  If {@code 0}, it means this object has been deallocated.
     */
    int refCnt();

    /**
     * Increases the reference count by {@code 1}.
     */
    ReferenceCounted retain();

    /**
     * Increases the reference count by the specified {@code increment}.
     */
    ReferenceCounted retain(int increment);

    /**
     * Records the current access location of this object for debugging purposes.
     * If this object is determined to be leaked, the information recorded by this operation will be provided to you
     * via {@link ResourceLeakDetector}.  This method is a shortcut to {@link #touch(Object) touch(null)}.
     */
    ReferenceCounted touch();

    /**
     * Records the current access location of this object with an additional arbitrary information for debugging
     * purposes.  If this object is determined to be leaked, the information recorded by this operation will be
     * provided to you via {@link ResourceLeakDetector}.
     */
    ReferenceCounted touch(Object hint);

    /**
     * Decreases the reference count by {@code 1} and deallocates this object if the reference count reaches at
     * {@code 0}.
     *
     * @return {@code true} if and only if the reference count became {@code 0} and this object has been deallocated
     */
    boolean release();

    /**
     * Decreases the reference count by the specified {@code decrement} and deallocates this object if the reference
     * count reaches at {@code 0}.
     *
     * @return {@code true} if and only if the reference count became {@code 0} and this object has been deallocated
     */
    boolean release(int decrement);
}

其实通过名字计数就知道这个东西不会太复杂,它主要涉及跟踪某发哥特定对象活动引用的数量。一个ReferenceCounted 实现的实例将会通常以活动的引用计数1作为开始,只要引用数大于0。就能保证对象不会被释放。减少到0的时候,就会释放,释放的对象就没有办法使用了。这个小玩意对于池化技术来说,至关重要。

我也很好奇这东西是怎么实现的,我曾经自诩为JAVA性能开发工程师,自吹自擂说任何JAVA技术,我都能追其源码,这个东西不知道的话,我面子挂不住,我决定探险,去看看他的源码。顺便看看 Unpooled.copiedBuffer的实现,我决定用断点去一探究竟。前方高能!!!

开始

Charset utf8 = StandardCharsets.UTF_8;
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);

在这里插入图片描述

刚进来,就看到了想看到的,我们追过去看看。release应该表示是一个是否释放的状态。 ByteBuf buffer = ALLOC.heapBuffer(ByteBufUtil.utf8Bytes(string));应该表示是一个基于堆的一个缓冲区。里面的逻辑我不看,我目的是看释放。是怎么释放的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
说实话这个方法具体干嘛我不知道,但是他肯定是在获取引用次数,如果这个数是2的话,那么就会尝试这个方法。

在这里插入图片描述
这个看懂了,cas操作嘛,把这个2换成1。如果这个失败了,就执行这个方法

 private boolean retryRelease0(T instance, int decrement) {
        for (;;) {
            int rawCnt = updater().get(instance), realCnt = toLiveRealRefCnt(rawCnt, decrement);
            if (decrement == realCnt) {
                if (tryFinalRelease0(instance, rawCnt)) {
                    return true;
                }
            } else if (decrement < realCnt) {
                // all changes to the raw count are 2x the "real" change
                if (updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) {
                    return false;
                }
            } else {
                throw new IllegalReferenceCountException(realCnt, -decrement);
            }
            Thread.yield(); // this benefits throughput under high contention
        }
    }

这段代码的意思是:如果真实计数是1,回到tryFinalRelease0,返回true,如果真实计数大于1,那么减1,就不能释放,如果小于1,就报错。如果不是2。进入nonFinalRelease0方法。

 private boolean nonFinalRelease0(T instance, int decrement, int rawCnt, int realCnt) {
        if (decrement < realCnt
                // all changes to the raw count are 2x the "real" change - overflow is OK
                && updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) {
            return false;
        }
        return retryRelease0(instance, decrement);
    }

意思是更新引用计数,失败调用retryRelease0方法。

这释放连起来就是用nonVolatileRawCnt获得引用计数,然后判断引用计数是否是2或者减的值就是真实引用计数值,是的话就可以尝试直接设置的方法tryFinalRelease0,如果失败会去尝试释放方法retryRelease0,这个是自旋,直到成功为止。如果不是的话就普通的引用计数器值的修改即可nonFinalRelease0。

再往下跑就是

 private boolean handleRelease(boolean result) {
        if (result) {
            deallocate();
        }
        return result;
    }

如果可以释放调用deallocate方法,进去看看
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
将Long(Heap)置为负数之后,直接释放内存。

这就是释放,那么增加引用,还有什么其他的逻辑应该都差不多,每次看到一些有趣的或者设计到工作内容的代码,我都忍不住进去看看源码。看大神是怎么实现的。以后我们遇到了,能不能用一样的手法,这就是阅读源码的附赠品,获得power是主产品。这个power是非常重要的,是关乎职业进阶的命脉,power够了,则早晚引发质变的。

回来,一个ReferenceCounted 我们不用关心当前引用值,只需在我们想释放的时候,让所有活动都失效就足够了。

结束语

这一把学习了ByteBuf容器,知道了优于JDK的地方,还搞了一些变体,并且何时去使用的例子,我们知道了
使用不同的读索引和写索引控制数据访问,
使用内存的不同方式 基于 heap 和 direct
通过CompositeByteBuf生成多个ByteBuf的聚合视图
数据访问方法,切片,搜索和复制
读写和设置API
ByteBufAllocate池化和引用计数
从源码上走了一下释放,满足了一下求知欲。

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

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

相关文章

STM32开发(15)----芯片内部温度传感器

芯片内部温度传感器前言一、什么是内部温度传感器&#xff1f;二、实验过程1.STM32CubeMX配置2.代码实现3.实验结果总结前言 本章介绍STM32芯片温度传感器的使用方法和获取方法。 一、什么是内部温度传感器&#xff1f; STM32 有一个内部的温度传感器&#xff0c;可以用来测…

竣达技术 | 巡检触摸屏配合电池柜,电池安全放首位!

机房蓄电池常见的故障 1.机房电池着火和爆炸 目前在数据机房蓄电池爆炸着火事故频发&#xff0c;导致业主损失严重。一般机房电池是由于其中一节电池裂化后未妥善管理&#xff0c;电池急剧恶化导致爆炸着火。由于电池是串联及并联在使用&#xff0c;只要一节着火燃烧整片瞬间…

RK3399平台开发系列讲解(LED子系统篇)LED子系统详解

🚀返回专栏总目录 文章目录 一、设备树编写二、LED子系统2.1、用户态2.2、内核驱动三、驱动代码3.1、平台设备驱动的注册3.2、平台设备驱动的probe四、使用方法沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将详细介绍LED子系统。 一、设备树编写 节点属性添加…

免费领取丨精算与金融建模行业解决方案白皮书,不要错过!

一、我国精算行业现状 精算学是对人类社会所面临的各种风险及其他客观事务进行量化分析和处理的一门科学。在保险、金融、投资和各类风险管理等许多领域得到广泛应用&#xff0c;尤其在保险和社会保障领域&#xff0c;已成为不可或缺的科学和技术&#xff0c;以保险公司为例&a…

直播间的2个小感悟

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 在线人数固定 最近直播间出现了很多新面孔&#xff0c;有的是偶然刷到的&#xff0c;有的是关注互联网找到的。而直播间的人数一直没什么变化&#xff0c;卢松松在抖音直播较少&#xff0c;主播间…

mysql 8.0.22安装

mysql8.0.22安装1. 配置my.ini2. 添加环境变量3. 安装mysql3.1 mysql初始化3.2 安装mysql服务3.3 启动mysql服务4. 连接数据库修改连接数据库的密码前提&#xff1a;已经从官网下载mysql8.0.22安装包并解压&#xff08;下载地址&#xff1a;https://dev.mysql.com/downloads/in…

设计模式学习笔记 - 外观模式

设计模式学习笔记 - 外观模式一、影院管理问题二、传统方式解决影院管理问题三、外观模式介绍1、基本介绍2、原理类图四、外观模式解决影院管理问题五、外观模式在MyBatis框架应用的源码分析六、外观模式的注意事项和细节一、影院管理问题 组建一个家庭影院&#xff1a;DVD 播放…

ur3+robotiq ft sensor+robotiq 2f 140+realsense d435i配置rviz,gazebo仿真环境

ur3robotiq ft sensorrobotiq 2f 140realsense d435i配置rviz&#xff0c;gazebo仿真环境 搭建环境&#xff1a; ubuntu: 20.04 ros: Nonetic sensor: robotiq_ft300 gripper: robotiq_2f_140_gripper UR: UR3 reasense&#xff1a; D435i 通过下面几篇博客配置好了ur3、力传…

带你轻松实现通讯录(C语言版)

文章目录前言通讯录初始化通讯录运行的基本框架和菜单增添联系人删除联系人查找联系人修改联系人信息展示通讯录通讯录联系人个数排序通讯录文件操作储存通讯录信息销毁通讯录整体代码Contacts.hContacts.ctest.c写在最后前言 学习C语言的小伙伴&#xff0c;相信都要经历实现通…

Web网页测试全流程解析论Web自动化测试

1、功能测试 web网页测试中的功能测试&#xff0c;主要测试网页中的所有链接、数据库连接、用于在网页中提交或获取用户信息的表单、Cookie 测试等。 &#xff08;1&#xff09;查看所有链接&#xff1a; 测试从所有页面到被测特定域的传出链接。 测试所有内部链接。 测试链…

开学准备哪些电容笔?ipad触控笔推荐平价

在现代&#xff0c;数码产品的发展受到高技术的驱动。不管是在工作上&#xff0c;还是在学习上&#xff0c;大的显示屏可以使图像更加清晰。Ipad将成为我们日常生活中不可或缺的一部分&#xff0c;无论现在或将来。如果ipad配上一款方便操作的电容笔&#xff0c;将极大地提高我…

Unity性能优化:如何优化Drawcall

前言 降低游戏的Drawcall&#xff0c;是渲染优化很重要的手段&#xff0c;接下来从以下4个方面来分析如何降低DrawCall: 对惹&#xff0c;这里有一个游戏开发交流小组&#xff0c;希望大家可以点击进来一起交流一下开发经验呀 降低Drawcall的意义是什么?如何查看游戏的Drawca…

C++继承、构造函数和析构函数

构造函数 与 析构函数 构造函数代表一个对象的生成&#xff0c;主要作用是初始化类的成员变量&#xff0c;可以被重载 如果没有显式构造函数&#xff0c;则实例化对象时&#xff0c;系统会自动生成一个无参的构造函数 构造函数的名称与类名相同 析构函数代表的是一个对象的销…

初识Python——“Python”

各位CSDN的uu们你们好呀&#xff0c;今天进入到了我们的新专栏噢&#xff0c;Python是小雅兰的专业课&#xff0c;那么现在&#xff0c;就让我们进入Python的世界吧 计算机基础概念 什么是计算机&#xff1f; 什么是编程&#xff1f; 编程语言有哪些&#xff1f; Python背景知…

MySQL的安装(详解)

文章目录前言一、yum方式安装1、下载并安装MySQL2、 启动MySQL数据库3、查看MySQL初始密码4、登录数据库5、修改MySQL默认密码6、授予root用户远程管理权限7、输入exit退出数据库二、rpm安装方式1、检查2、卸载mariadb3、安装4、启动5、密码总结前言 本教程为Linux下安装mysql的…

若依配置教程(九)若依前后端分离版部署到服务器Nginx(Windows版)

搭建若依环境 要部署到服务器上&#xff0c;首先要在本地运行若依系统 文章目录搭建若依环境后端部署1.在application.yml中修改后台端口&#xff0c;这里默认是8080。2.在application-druid.yml中修改正式环境数据库。3.后端打包部署前端部署下载安装NginxNginx代理配置启动N…

UnityEditor编辑器扩展代码实现Project搜索的实现功能和切换Component等

反射实现切换Gameobjecect-Comp之前介绍过Kinematic Character Controller这个插件这个插件很容易和另外一个插件混淆&#xff0c;两个作者头像比较相像&#xff0c;而且这个插件的作者不太喜欢露脸&#xff08;他现在做Dot-CharacterControl去了&#xff09;&#xff0c;几乎网…

人人能读懂redux原理剖析

一、Redux是什么&#xff1f; 众所周知&#xff0c;Redux最早运用于React框架中&#xff0c;是一个全局状态管理器。Redux解决了在开发过程中数据无限层层传递而引发的一系列问题&#xff0c;因此我们有必要来了解一下Redux到底是如何实现的&#xff1f; 二、Redux的核心思想…

计算机网络之IP协议(详解

网络层主管地址管理与路由选择。而IP协议就是网络层中一个非常重要的协议。它的作用就是在复杂的网络环境中确定一个合适的路径。IP协议头格式4位版本号(version) 指定IP协议的版本&#xff0c;目前只有两个版本&#xff1a;IP v4和IP v6.对于IP v4来说&#xff0c;这个值就是4…

边缘云是什么?

涂鸦边缘云服务 旨在解决物联网边缘位置的连接需求和提高设备自主管理能力。并与涂鸦 IoT 云服务和 IoT 终端形成云边端三位一体的端到端产品架构。使用涂鸦边缘云&#xff0c;能极大降低设备响应延时、降低网络带宽压力、提高算力分发能力&#xff0c;并构建以下技术优势&…