Netty读数据源码解析

2024-03-05 01:59
文章标签 源码 解析 netty 读数据

本文主要是介绍Netty读数据源码解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一.接收读就绪事件

io.netty.channel.nio.NioEventLoop#processSelectedKey(java.nio.channels.SelectionKey, io.netty.channel.nio.AbstractNioChannel)

/*** 处理就绪的IO事件* @param k 就绪的IO事件* @param ch 该就绪事件对应的channel(NioServerSocketChannel / NioSocketChannel)*/
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {// 对于服务端来说,这里的unsafe就是NioMessageUnsafe// 对于客户端来说,这里的unsafe就是NioSocketChannelUnsafefinal AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();if (!k.isValid()) {final EventLoop eventLoop;try {eventLoop = ch.eventLoop();} catch (Throwable ignored) {// If the channel implementation throws an exception because there is no event loop, we ignore this// because we are only trying to determine if ch is registered to this event loop and thus has authority// to close ch.return;}// Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop// and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is// still healthy and should not be closed.// See https://github.com/netty/netty/issues/5125if (eventLoop == this) {// close the channel if the key is not valid anymoreunsafe.close(unsafe.voidPromise());}return;}try {// 获取就绪的IO事件int readyOps = k.readyOps();// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise// the NIO JDK channel implementation may throw a NotYetConnectedException.// 条件成立:说明发生了连接就绪事件if ((readyOps & SelectionKey.OP_CONNECT) != 0) {// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking// See https://github.com/netty/netty/issues/924int ops = k.interestOps();ops &= ~SelectionKey.OP_CONNECT;k.interestOps(ops);unsafe.finishConnect();}// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.// 条件成立:说明发生了写就绪事件if ((readyOps & SelectionKey.OP_WRITE) != 0) {// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to writech.unsafe().forceFlush();}// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead to a spin loop// 处理就绪的事件是读事件或者接收事件// 读就绪事件,调用NioSocketChannelUnsafe.read()// 接收就绪事件,调用NioMessageUnsafe.read()// 条件成立:说明发生了读就绪事件或者接收连接就绪事件if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {unsafe.read();}} catch (CancelledKeyException ignored) {unsafe.close(unsafe.voidPromise());}
}

可以看到processSelectedKey方法就是处理select上的就绪事件的,我们关注最下面的处理读就绪事件和接收连接就绪事件,因为读就绪事件是发生在客户端channel中的,而接收连接就绪事件是发生在服务端channel中,所以调用的read方法是不一样的,我们这里看处理读就绪事件的read方法

io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read

/*** 读取客户端发送过来的数据*/@Overridepublic final void read() {// 得到客户端NioSocketChannel的配置类NioSocketChannelConfigfinal ChannelConfig config = config();if (shouldBreakReadReady(config)) {clearReadPending();return;}// 得到客户端NioSocketChannel的pipelinefinal ChannelPipeline pipeline = pipeline();// 得到缓冲区内存分配器,它是专门用来分配内存缓冲的,如果不是android平台,默认使用PooledByteBufAllocatorfinal ByteBufAllocator allocator = config.getAllocator();// 默认返回的实例是AdaptiveRecvByteBufAllocator,AdaptiveRecvByteBufAllocator是一个能够预测下次分配多大内存缓冲区的分配处理器,根据里面的分配算法去指定需要分配内存的大小final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();// 重置读循环最大次数,重置已读取数据的总大小为0,重置已经读循环的次数为0allocHandle.reset(config);ByteBuf byteBuf = null;boolean close = false;try {do {// 在分配处理器的辅助下,得到一个评估后大小的内存缓冲区byteBuf = allocHandle.allocate(allocator);// 在这行代码中会去从channel中读取数据到byteBuf,然后根据已读到的数据大小去对下一次需要分配的byteBuf大小进行动态尺寸伸缩allocHandle.lastBytesRead(doReadBytes(byteBuf));// 条件成立:这种情况说明本次读循坏已经从客户端channel读取不到数据了if (allocHandle.lastBytesRead() <= 0) {// 释放缓冲区byteBuf.release();byteBuf = null;close = allocHandle.lastBytesRead() < 0;if (close) {// There is nothing left to read as we received an EOF.readPending = false;}// 跳出读循环break;}// 已经读循环的次数加1allocHandle.incMessagesRead(1);readPending = false;// 向pipeline中传递一个channelRead事件,并且带上这次读循坏读到的数据pipeline.fireChannelRead(byteBuf);byteBuf = null;} while (allocHandle.continueReading());// 对这次已读取的总数据大小进行一个评估,以此来判断下一次读数据的时候分配多大的缓冲区(缓冲区容量尺寸伸缩)allocHandle.readComplete();// 向pipeline中传递一个channelReadComplete事件pipeline.fireChannelReadComplete();if (close) {closeOnRead(pipeline);}} catch (Throwable t) {handleReadException(pipeline, byteBuf, t, close, allocHandle);} finally {// Check if there is a readPending which was not processed yet.// This could be for two reasons:// * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method// * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method//// See https://github.com/netty/netty/issues/2254if (!readPending && !config.isAutoRead()) {removeReadOp();}}}
}

首先可以看到会拿到一个ByteBufAllocator,也就是内存分配器,顾名思义它就是用来分配得到一个内存缓冲区的,然后具体这个内存缓冲区的容量需要分配多大就通过内存分配处理器去进行计算了,所以也就是说这里的内存分配处理器就是一个关键,我们下面具体来看下它是如何工作计算出要分配多大内存的

二.接收内存分配处理器

我们先来看下接收内存分配器的继承结构图

 其中红色线代表的是内部类,所以可以看到接收内存分配器netty提供了两个实现,分别是FixedRecvByteBufAllocator和AdaptiveRecvByteBufAllocator,这里我们关键看一下AdaptiveRecvByteBufAllocator,根据名字这个接收内存分配器的主要作用其实就是能够根据读取的消息大小去推断接收这些消息需要分配的内存大小,达到一个自适应的效果

1.自适应接收内存分配器的创建

我们先看一下接收内存分配器是怎么创建的

public RecvByteBufAllocator.Handle recvBufAllocHandle() {if (recvHandle == null) {recvHandle = config().getRecvByteBufAllocator().newHandle();}return recvHandle;
}
public <T extends RecvByteBufAllocator> T getRecvByteBufAllocator() {return (T) rcvBufAllocator;
}

默认的,netty在读数据的时候就是使用AdaptiveRecvByteBufAllocator,当然我们也可以通过option方法去配置使用其他的内存分配器,而创建了AdaptiveRecvByteBufAllocator实例之后,还要通过newHandle方法去创建出一个HandleImpl实例,从上面的继承关系图可以看出,这个HandleImpl是AdaptiveRecvByteBufAllocator的内部类,而它又是继承于AdaptiveRecvByteBufAllocator的父类DefaultMaxMessagesRecvByteBufAllocator中的内部类MaxMessageHandle。其实这个AdaptiveRecvByteBufAllocator中的这个内部类HandleImpl就是负责实现内存分配自适应的功能,先看它的构造方法

public Handle newHandle() {return new HandleImpl(minIndex, maxIndex, initial);
}

分别传入了三个参数,这三个参数其实是外部类AdaptiveRecvByteBufAllocator的三个属性,并且在AdaptiveRecvByteBufAllocator实例被创建的时候会对这三个参数进行初始化,代码如下:

/*** 使用默认参数创建一个新的缓冲区分配器,使用默认参数,预期的缓冲区大小从1024开始,不会低于64,也不会高于65536。*/
public AdaptiveRecvByteBufAllocator() {this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);
}

默认值如下:

static final int DEFAULT_MINIMUM = 64;
static final int DEFAULT_INITIAL = 2048;
static final int DEFAULT_MAXIMUM = 65536;
/*** 使用指定的参数创建一个新的缓冲区分配器** @param minimum  the inclusive lower bound of the expected buffer size* @param initial  the initial buffer size when no feed back was received* @param maximum  the inclusive upper bound of the expected buffer size*/
public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {checkPositive(minimum, "minimum");if (initial < minimum) {throw new IllegalArgumentException("initial: " + initial);}if (maximum < initial) {throw new IllegalArgumentException("maximum: " + maximum);}// 通过二分查找法在SIZE_TABLE找出与minimum相等或者最接近的值的下标int minIndex = getSizeTableIndex(minimum);// 如果找到的值小于minimum,那么minIndex就等于这个值在SIZE_TABLE数组的下标+1if (SIZE_TABLE[minIndex] < minimum) {this.minIndex = minIndex + 1;}// 如果找到的值等于minimum,那么minIndex就直接等于这个值在SIZE_TABLE数组的下标else {this.minIndex = minIndex;}// 通过二分查找法在SIZE_TABLE找出与maximum相等或者最接近的值的下标int maxIndex = getSizeTableIndex(maximum);// 如果找到的值大于maximum,那么minIndex就等于这个值在SIZE_TABLE数组的下标-1if (SIZE_TABLE[maxIndex] > maximum) {this.maxIndex = maxIndex - 1;}// 如果找到的值等于minimum,那么maxIndex就直接等于这个值在SIZE_TABLE数组的下标else {this.maxIndex = maxIndex;}this.initial = initial;
}

可以看到上面的方法其实就是根据传入的minimum和maximum去从SIZE_TABLE数组中分别通过二分查找法找到最接近对应值的元素下标,并分别赋值了给minIndex和maxIndex,那么SIZE_TABLE这个数组又是从哪里初始化的呢?答案是在静态代码块中,代码如下:

static {List<Integer> sizeTable = new ArrayList<Integer>();// 从16开始,每一次增加16放到sizeTable中直到增加的数到496为止// 例如: 16,32,48,64...496for (int i = 16; i < 512; i += 16) {sizeTable.add(i);}// 从512开始,每一次都把对应的二进制左移1位,也就是说每一次都把自身*2再放到sizeTable中,直到int类型溢出变成负数for (int i = 512; i > 0; i <<= 1) { // lgtm[java/constant-comparison]sizeTable.add(i);}// 把初始化好的集合放到SIZE_TABLE数组中SIZE_TABLE = new int[sizeTable.size()];for (int i = 0; i < SIZE_TABLE.length; i ++) {SIZE_TABLE[i] = sizeTable.get(i);}
}

初始化的规则就是在512之前,最小从16开始,每一个数都比前一个数大16,而从512开始,之后的每一个数都会比前一个数增大一倍,直到int类型溢出变成负数结束。初始化完了minimum,initial,maximum和SIZE_TABLE之后,HandleImpl实例就需要使用这几个参数了

HandleImpl(int minIndex, int maxIndex, int initial) {this.minIndex = minIndex;this.maxIndex = maxIndex;// 通过二分查找法在SIZE_TABLE找出与initial相等或者最接近的值的下标index = getSizeTableIndex(initial);nextReceiveBufferSize = SIZE_TABLE[index];
}

通过二分查找法去给initial这个值从SIZE_TABLE中找到对应的元素下标,然后把这个元素下标对应的值拿出来并赋值给nextReceiveBufferSize,因为默认地initial就等于2048,而SIZE_TABLE中也有一个元素是2048,所以默认地nextReceiveBufferSize就等于2048

2.创建预估大小的ByteBuf

上面说了HandleImpl的创建以及参数的初始化,那么这些参数对于自适应创建ByteBuf有什么用呢,我们看到allocate方法

/*** 返回一个根据guess()方法指定大小的内存缓冲区* @param alloc* @return*/
@Override
public ByteBuf allocate(ByteBufAllocator alloc) {return alloc.ioBuffer(guess());
}

该方法是HandleImpl的父类MaxMessageHandle实现的,作用就是根据指定的大小从内存分配器中创建出一个内存缓冲区,而这个缓冲区的大小是通过guess方法返回的,guess方法是一个接口方法,MaxMessageHandle并没有实现,所以这里需要HandleImpl实现,代码如下:

/*** 返回下一次分配的内存缓冲区的大小* @return*/
@Override
public int guess() {return nextReceiveBufferSize;
}

可以看到返回的就是我们上面从SIZE_TABLE中获取到的nextReceiveBufferSize,也就是说首次通过自适应接收内存分配器创建出来的内存大小就是2048个字节

3.如何进行自适应分配内存

do {// 在分配处理器的辅助下,得到一个评估后大小的内存缓冲区byteBuf = allocHandle.allocate(allocator);// 在这行代码中会去从channel中读取数据到byteBuf,然后根据已读到的数据大小去对下一次需要分配的byteBuf大小进行动态尺寸伸缩allocHandle.lastBytesRead(doReadBytes(byteBuf));// 条件成立:这种情况说明本次读循坏已经从客户端channel读取不到数据了if (allocHandle.lastBytesRead() <= 0) {// 释放缓冲区byteBuf.release();byteBuf = null;close = allocHandle.lastBytesRead() < 0;if (close) {// There is nothing left to read as we received an EOF.readPending = false;}// 跳出读循环break;}// 已经读循环的次数加1allocHandle.incMessagesRead(1);readPending = false;// 向pipeline中传递一个channelRead事件,并且带上这次读循坏读到的数据pipeline.fireChannelRead(byteBuf);byteBuf = null;
} while (allocHandle.continueReading());

第一次循环通过allocate方法分配出一个ByteBuf,大小是2048,然后调用doReadBytes方法,该方法是一个抽象方法,在NioSocketChannel中的实现如下:

@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();// 设置想要读取的数据大小等于ByteBuf的容量大小allocHandle.attemptedBytesRead(byteBuf.writableBytes());// 读取客户端channel读缓冲区的数据放到ByteBuf中,读取长度最大为ByteBuf分配的大小,并且返回读取数据的大小return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

调用ByteBuf的writableBytes方法获取到这个ByteBuf的可写大小,然后传给了DefaultMaxMessagesRecvByteBufAllocator接收内存分配器的attemptedBytesRead方法:

@Override
public void attemptedBytesRead(int bytes) {attemptedBytesRead = bytes;
}

赋值给attemptedBytesRead变量,这个变量表示预估这一次要读取的消息大小,然后就调用ByteBuf的writeBytes方法把SocketChannel的读缓冲区的数据写入到ByteBuf中,并且读取的消息大小不能大于ByteBuf的可写大小,最后把读取到的消息大小返回出去,在返回出去之后就把读取到的消息大小传入到AdaptiveRecvByteBufAllocator的lastByteRead方法中

@Override
public void lastBytesRead(int bytes) {// If we read as much as we asked for we should check if we need to ramp up the size of our next guess.// This helps adjust more quickly when large amounts of data is pending and can avoid going back to// the selector to check for more data. Going back to the selector can add significant latency for large// data transfers.// 如果实际读取的数据大小等于想要读取的数据大小if (bytes == attemptedBytesRead()) {record(bytes);}super.lastBytesRead(bytes);
}

此时会判断从SocketChannel中读取到的消息大小是否等于在读消息之前设置的预估读取的消息大小,如果相等,那么就调用record方法

/*** 根据实际读取的数据大小去评估下一次读数据的缓冲区容量是需要缩小还是扩大* @param actualReadBytes*/
private void record(int actualReadBytes) {// 当本次实际已读取的数据大小 小于或等于 SIZE_TABL[index-1-1]的时候,说明可能需要读取的数据并不多,所以对缓冲区容量进行缩小// 当第二次实际已读取的数据大小 小于或等于 SIZE_TABL[index-1-1]的时候,说明需要读取的数据并不多,所以对缓冲区容量直接进行缩小if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {if (decreaseNow) {// 缩小到SIZE_TABL[index-1]index = max(index - INDEX_DECREMENT, minIndex);nextReceiveBufferSize = SIZE_TABLE[index];decreaseNow = false;} else {decreaseNow = true;}}// 当本次实际已读取的数据大小 大于或等于下一次读取的数据大小的时候,直接对缓冲区容量进行扩容else if (actualReadBytes >= nextReceiveBufferSize) {index = min(index + INDEX_INCREMENT, maxIndex);nextReceiveBufferSize = SIZE_TABLE[index];decreaseNow = false;}
}

这个方法就是实现自适应功能的核心了,如果从SocketChannel读取的数据大小=分配的内存大小,就需要对内存缓冲区容量进行扩容,扩容规则就是扩容到原本容量大小值对应在SIZE_TABLE的元素下标 +4对应的值。在对内存缓冲区的下一次需要分配的大小进行了扩容或者缩小之后,最后还调用了父类DefaultMaxMessagesRecvByteBufAllocator的lastBytesRead方法

@Override
public void lastBytesRead(int bytes) {lastBytesRead = bytes;if (bytes > 0) {totalBytesRead += bytes;}
}

该方法就是使用lastBytesRead这个变量记录这一次读取的消息大小,以及把读取的消息大小累加到totalBytesRead变量。此时一次读循环就结束了,然后会在while中判断是否需要继续读循环

/*** 是否继续读循环* @return  true=>继续,false=>不继续*/
@Override
public boolean continueReading() {return continueReading(defaultMaybeMoreSupplier);
}@Override
public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {// 继续读循环条件成立:// 1.config.isAutoRead(),默认等于true// 2.maybeMoreDataSupplier.get(),想要去读取的数据的大小 等于 上一次读取的数据大小,说明可能会还有很多数据没有读取// 3.此时经过的读循环次数 小于 可读循环的最大次数// 4.此时已读取的数据大小 大于 0// 上面的4个条件都成立那么读循环就可以继续return config.isAutoRead() &&(!respectMaybeMoreData || maybeMoreDataSupplier.get()) &&totalMessages < maxMessagePerRead &&totalBytesRead > 0;
}private final UncheckedBooleanSupplier defaultMaybeMoreSupplier = new UncheckedBooleanSupplier() {@Overridepublic boolean get() {return attemptedBytesRead == lastBytesRead;}
};

判断是否继续读循环的有4个条件判断,其中关键的一个就是我们设置的预估要读取的消息大小和本次实际读取的数据相等,表示还有可能SocketChannel读缓冲区中还有数据没有读取到,需要继续读循环去读取。当第二次及以上进行读循环的时候,有一个if判断

if (allocHandle.lastBytesRead() <= 0) {// 释放缓冲区byteBuf.release();byteBuf = null;close = allocHandle.lastBytesRead() < 0;if (close) {// There is nothing left to read as we received an EOF.readPending = false;}// 跳出读循环break;
}

这个if条件会去判断本次读循环的数据是否小于等于0,如果是等于0则说明上一次读循环刚好已经把SocketChannel读缓冲区的消息读取完了,所以就需要跳出读循环了,并且把ByteBuf释放掉。并且,在整个读循环都结束了之后,就会调用自适应接收内存分配器的readComplete方法

/*** 当读取channel数据完毕的时候调用,此时会通过record方法对本次读取的所有数据评估,作为下一次分配的缓冲区大小*/
@Override
public void readComplete() {record(totalBytesRead());
}

在自适应接收内存分配器中会重写这个方法,在里面会再次调用record方法,作用就是整个读数据过程完成之后,根据读取到的数据大小去调整下一次读数据的时候需要分配的内存大小,因为netty会认为你这一次读取到这么多的数据,那么下一次可能还会读取到这么多

三.传递pipeline事件

在上面的读循环代码中,每进行一次读循环,都会把读取到的数据通过pipeline传递出去,具体就是会调用到pipeline中每一个handler的channelRead方法,所以说如果客户端发送一个很大的消息,我们一次读循环不能完全读取的话,那么就会分多次读取,这也就会使得handler中的channelRead方法被多次调用。当然,在整个读循环跳出结束之后,还会向pipeline中传递一个channelReadComplete事件,从而会调用后所有handler的channelReadComplete方法

这篇关于Netty读数据源码解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/774968

相关文章

解析 XML 和 INI

XML 1.TinyXML库 TinyXML是一个C++的XML解析库  使用介绍: https://www.cnblogs.com/mythou/archive/2011/11/27/2265169.html    使用的时候,只要把 tinyxml.h、tinystr.h、tinystr.cpp、tinyxml.cpp、tinyxmlerror.cpp、tinyxmlparser.

SpringBoot集成Netty,Handler中@Autowired注解为空

最近建了个技术交流群,然后好多小伙伴都问关于Netty的问题,尤其今天的问题最特殊,功能大概是要在Netty接收消息时把数据写入数据库,那个小伙伴用的是 Spring Boot + MyBatis + Netty,所以就碰到了Handler中@Autowired注解为空的问题 参考了一些大神的博文,Spring Boot非controller使用@Autowired注解注入为null的问题,得到

springboot家政服务管理平台 LW +PPT+源码+讲解

3系统的可行性研究及需求分析 3.1可行性研究 3.1.1技术可行性分析 经过大学四年的学习,已经掌握了JAVA、Mysql数据库等方面的编程技巧和方法,对于这些技术该有的软硬件配置也是齐全的,能够满足开发的需要。 本家政服务管理平台采用的是Mysql作为数据库,可以绝对地保证用户数据的安全;可以与Mysql数据库进行无缝连接。 所以,家政服务管理平台在技术上是可以实施的。 3.1

高仿精仿愤怒的小鸟android版游戏源码

这是一款很完美的高仿精仿愤怒的小鸟android版游戏源码,大家可以研究一下吧、 为了报复偷走鸟蛋的肥猪们,鸟儿以自己的身体为武器,仿佛炮弹一样去攻击肥猪们的堡垒。游戏是十分卡通的2D画面,看着愤怒的红色小鸟,奋不顾身的往绿色的肥猪的堡垒砸去,那种奇妙的感觉还真是令人感到很欢乐。而游戏的配乐同样充满了欢乐的感觉,轻松的节奏,欢快的风格。 源码下载

tf.split()函数解析

API原型(TensorFlow 1.8.0): tf.split(     value,     num_or_size_splits,     axis=0,     num=None,     name='split' ) 这个函数是用来切割张量的。输入切割的张量和参数,返回切割的结果。  value传入的就是需要切割的张量。  这个函数有两种切割的方式: 以三个维度的张量为例,比如说一

基于Java医院药品交易系统详细设计和实现(源码+LW+调试文档+讲解等)

💗博主介绍:✌全网粉丝10W+,CSDN作者、博客专家、全栈领域优质创作者,博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗 🌟文末获取源码+数据库🌟 感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人  Java精品实战案例《600套》 2023-2025年最值得选择的Java毕业设计选题大全:1000个热

美容美发店营销版微信小程序源码

打造线上生意新篇章 一、引言:微信小程序,开启美容美发行业新纪元 在数字化时代,微信小程序以其便捷、高效的特点,成为了美容美发行业营销的新宠。本文将带您深入了解美容美发营销微信小程序,探讨其独特优势及如何助力商家实现业务增长。 二、微信小程序:美容美发行业的得力助手 拓宽客源渠道:微信小程序基于微信社交平台,轻松实现线上线下融合,帮助商家快速吸引潜在客户,拓宽客源渠道。 提升用户体验:

风水研究会官网源码系统-可展示自己的领域内容-商品售卖等

一款用于展示风水行业,周易测算行业,玄学行业的系统,并支持售卖自己的商品。 整洁大气,非常漂亮,前端内容均可通过后台修改。 大致功能: 支持前端内容通过后端自定义支持开启关闭会员功能,会员等级设置支持对接官方支付支持添加商品类支持添加虚拟下载类支持自定义其他类型字段支持生成虚拟激活卡支持采集其他站点文章支持对接收益广告支持文章评论支持积分功能支持推广功能更多功能,搭建完成自行体验吧! 原文

陀螺仪LSM6DSV16X与AI集成(8)----MotionFX库解析空间坐标

陀螺仪LSM6DSV16X与AI集成.8--MotionFX库解析空间坐标 概述视频教学样品申请源码下载开启CRC串口设置开启X-CUBE-MEMS1设置加速度和角速度量程速率选择设置FIFO速率设置FIFO时间戳批处理速率配置过滤链初始化定义MotionFX文件卡尔曼滤波算法主程序执行流程lsm6dsv16x_motion_fx_determin欧拉角简介演示 概述 本文将探讨

【文末附gpt升级秘笈】腾讯元宝AI搜索解析能力升级:千万字超长文处理的新里程碑

腾讯元宝AI搜索解析能力升级:千万字超长文处理的新里程碑 一、引言 随着人工智能技术的飞速发展,自然语言处理(NLP)和机器学习(ML)在各行各业的应用日益广泛。其中,AI搜索解析能力作为信息检索和知识抽取的核心技术,受到了广泛的关注和研究。腾讯作为互联网行业的领军企业,其在AI领域的探索和创新一直走在前列。近日,腾讯旗下的AI大模型应用——腾讯元宝,迎来了1.1.7版本的升级,新版本在AI搜