当前位置: 首页 > news >正文

22.Netty源码之解码器


highlight: arduino-light

抽象解码类

https://mp.weixin.qq.com/s/526p5f9fgtZu7yYq5j7LiQ

解码器

Netty 常用解码器类型:

  • ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象;
  • MessageToMessageDecoder 将一种消息类型解码为另外一种消息类型。

自定义一次解码器ByteToMessageDecoder解码器,如果读到的字节大小为4,那么认为读取到了1个完整的数据包。

java class VersionDecoder extends ByteToMessageDecoder {    @Override    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {        //此处不需要while循环        if( in.readableBytes()>=4 ){            out.add(in.readInt());       }   } }

自定义二次解码器,用于将String转换为Integer

java class StringToIntegerDecoder extends MessageToMessageDecoder<String> {    @Override    public void decode(ChannelHandlerContext ctx, String message,List<Object> out) throws Exception {        out.add(message.length());   } }

此时使用一次解码器+二次解码器完成了Byte到String、String到Integer的转换。

为什么要粘包拆包

为什么要粘包

首先你得了解一下TCP/IP协议,在用户数据量非常小的情况下,极端情况下,一个字节,该TCP数据包的有效载荷非常低,传递100字节的数据,需要100次TCP传送,100次ACK,在应用及时性要求不高的情况下,将这100个有效数据拼接成一个数据包,那会缩短到一个TCP数据包,以及一个ack,有效载荷提高了,带宽也节省了。

非极端情况,有可能两个数据包拼接成一个数据包,也有可能一个半的数据包拼接成一个数据包,也有可能两个半的数据包拼接成一个数据包。

为什么要拆包

拆包和粘包是相对的,一端粘了包,另外一端就需要将粘过的包拆开。

举个栗子,发送端将三个数据包粘成两个TCP数据包发送到接收端,接收端就需要根据应用协议将三个数据包拆分成两个数据包

还有一种情况就是用户数据包超过了mss(最大报文长度),那么这个数据包在发送的时候必须拆分成几个数据包,接收端收到之后需要将这些数据包粘合起来之后,再拆开。

拆包的原理

在没有netty的情况下,用户如果自己需要拆包,基本原理就是不断从TCP缓冲区中读取数据,每次读取完都需要判断是否是一个完整的数据包

1.如果当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从tcp缓冲区中读取,直到得到一个完整的数据包

2.如果当前读到的数据加上已经读取的数据足够拼接成一个数据包,那就将已经读取的数据拼接上本次读取的数据,够成一个完整的业务数据包传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接。

netty 中的拆包也是如上这个原理,内部会有一个累加器,每次读取到数据都会不断累加,然后尝试对累加到的数据进行拆包,拆成一个完整的业务数据包,这个基类叫做 ByteToMessageDecoder,下面我们先详细分析下这个类

同样,我们先看下抽象解码类的继承关系图。

解码类是 ChanneInboundHandler 的抽象类实现,操作的是 Inbound 入站数据。

解码器实现的难度要远大于编码器,因为解码器需要考虑拆包/粘包问题。

由于接收方有可能没有接收到完整的消息,所以解码框架需要对入站的数据做缓冲操作,直至获取到完整的消息。

Drawing 1.png

一次解码器ByteToMessageDecoder

ByteToMessageDecoder 中定义了两个累加器

2种累加器

Cumulator

每次将读取到的数据累加。

方式1:默认是内存复制的方式累加.如果内存不够先扩容。MERGE_CUMULATOR

方式2:组合的方式,避免内存复制。

图片

MERGE_CUMULATOR

默认情况下,会使用 MERGE_CUMULATOR。

MERGE_CUMULATOR 的原理是每次都将读取到的数据通过内存拷贝的方式,拼接到一个大的字节容器中,这个字节容器在 ByteToMessageDecoder中叫做 cumulation。

下面我们看一下 MERGE_CUMULATOR 是如何将新读取到的数据累加到字节容器里的

java public static final Cumulator MERGE_CUMULATOR = new Cumulator() {        @Override        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {            try {                final ByteBuf buffer;                if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()                    || cumulation.refCnt() > 1                    || cumulation.isReadOnly()) {                    //按需扩容                    buffer = expandCumulation(alloc, cumulation, in.readableBytes());               } else {                    buffer = cumulation;               }                buffer.writeBytes(in);                return buffer;           } finally {                in.release();           }       }   };

netty 中ByteBuf的抽象,使得累加非常简单。通过一个简单的api调用 buffer.writeBytes(in);

便将新数据累加到字节容器中,为了防止字节容器大小不够,在累加之前还进行了扩容处理

java static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {        ByteBuf oldCumulation = cumulation;        cumulation = alloc.buffer(oldCumulation.readableBytes() + readable);        cumulation.writeBytes(oldCumulation);        oldCumulation.release();        return cumulation;   }

扩容也是一个内存拷贝操作,新增的大小即是新读取数据的大小。

ByteToMessageDecoder:拆包原理

利用NIO进行网络编程时,往往需要将读取到的字节数或者字节缓冲区解码为业务可以使用的POJO对象。

Netty提供了ByteToMessageDecoder抽象工具解码类。

用户的解码器继承ByteToMessageDecoder,只需要实现decode()方法,即可完成ByteBuf到POJO对象的解码。 不过ByteToMessageDecoder没有考虑TCP粘包和组包等场景,读半包需要用户自己处理,因此我们可以继承更高级的解码器进行半包处理。

首先,我们看下ByteToMessageDecoder的子类FixedLengthFrameDecoder定义的方法:

```java public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {    /*    channelRead方法是每次从TCP缓冲区读到数据都会调用的方法    触发点在AbstractNioByteChannel的read方法中    里面有个while循环不断读取,读取到一次就触发一次channelRead。

   1.累加数据到字节容器cumulation。 2.将累加到的数据的字节容器传递给业务进行业务拆包 3.清理字节容器 4.传递业务数据包给业务解码器处理    */        @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        //if开始 判断类型是否匹配        if (msg instanceof ByteBuf) {            CodecOutputList out = CodecOutputList.newInstance();            try {                ByteBuf data = (ByteBuf) msg;                //1.累加数据                //if:当前累加器没有数据,就直接跳过内存拷贝,直接将字节容器的指针指向新读取的数据。                //else:调用累加器累加数据至字节容器                first = cumulation == null;                if (first) {                    //数据累加器                    cumulation = data;               } else {                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);               } //调用decode方法                //2.将累加到的数据传递给业务进行拆包                //将尝试将字节容器的数据拆分成业务数据包塞到业务数据容器out中                callDecode(ctx, cumulation, out);           } catch (DecoderException e) {                throw e;           } } catch (Exception e) {                throw new DecoderException(e);           } finally {          
          //何为可读:writerIndex > readerIndex           //何为不可读:writerIndex <= readerIndex           //不可读说明已经读完了!           //如果累加器不等于空 也不可读           //那么执行清理逻辑             if (cumulation != null && !cumulation.isReadable()) { //3.清理字节容器 //业务拆包完成之后,只是从字节容器中取走了数据。                //但是这部分空间对于字节容器来说依然保留着。                //而字节容器每次累加字节数据的时候都是将字节数据追加到尾部                //如果不对字节容器做清理,那么时间一长就会OOM。 //正常情况下,其实每次读取完数据,netty都会在下面这个discardSomeReadBytes方法中                //将字节容器清理                //只不过,当发送端发送数据过快,channelReadComplete可能会很久才被调用一次 //如果一次数据读取完毕之后,可能接收端一边收,发送端一边发。                //这里的读取完毕指的是接收端在某个时间不再接受到数据为止。                //发现仍然没有拆到一个完整的用户数据包,即使该channel的设置为非自动读取                //也会触发一次读取操作 ctx.read(),该操作会重新向selector注册op_read事件                //以便于下一次能读到数据之后拼接成一个完整的数据包 //所以为了防止发送端发送数据过快,netty会在每次读取到一次数据                //业务拆包之后对字节字节容器做清理,清理部分的代码如下                    numReads = 0;                    cumulation.release();                    cumulation = null;               } else if (++ numReads >= discardAfterReads) {         //如果字节容器当前已无数据可读取,直接销毁字节容器                //并且标注一下当前字节容器一次数据也没读取 //如果连续16次,discardAfterReads的默认值为16                //字节容器中仍然有未被业务拆包器读取的数据,         //那就做一次压缩,有效数据段整体移到容器首部                    numReads = 0;                                        discardSomeReadBytes();               } ​                int size = out.size();                firedChannelRead |= out.insertSinceRecycled();           //4.传递业务数据包给业务解码器处理           //触发channelRead事件 将拆到的业务数据包都传递到后续的handler           //这样就可以把一个个完整的业务数据包传递到后续的业务解码器进行解码,随后处理业务逻辑                fireChannelRead(ctx, out, size);                out.recycle();           }        //if开始对应的else判断类型是否匹配       } else {            ctx.fireChannelRead(msg);       }     }   } ​ ​ //frameLength=4,如果先发送2字节再发送2字节 //那么是否存在解码出现异常的情况? //答案:不会,因为有一个死循环 //比如发送方先发送了2字节的数据,然后发送方又发来了2字节 //首先原子累加器累加2字节传入callDecode方法的in,in是累加器cumulation //in.isReadable()判断可读,调用decode方法,decode方法会判断如果不够4字节 直接return跳出死循环 //然后发送方又发来2字节,然后继续累加到原子累加器 //判断可读调用decode方法。

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List out) {        try {            while (in.isReadable()) {                int outSize = out.size(); //判断out有数据就触发fireChannelRead                //out什么时候有的数据?                //在子类的decode方法中                if (outSize > 0) {                    fireChannelRead(ctx, out, outSize);                    out.clear();                    if (ctx.isRemoved()) {                        break;                   }                    outSize = 0;               } //decode在这里被调用                //decode中时,不能执行完handler remove清理操作。                //那decode完之后需要清理数据。                int oldInputLength = in.readableBytes();                decodeRemovalReentryProtection(ctx, in, out);                if (ctx.isRemoved()) {                    break;               } ​                if (outSize == out.size()) {                    if (oldInputLength == in.readableBytes()) {                        break;                   } else {                        continue;                   }               } ​                if (oldInputLength == in.readableBytes()) {                    throw new DecoderException(                            StringUtil.simpleClassName(getClass()) +                                  ".decode() did not read anything but decoded a message.");               } ​                if (isSingleDecode()) {                    break;               }           }       } catch (DecoderException e) {            throw e;       } catch (Exception cause) {            throw new DecoderException(cause);       }   }            final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List out)            throws Exception {        decodeState = STATECALLINGCHILDDECODE;        try {            //模板模式            decode(ctx, in, out);       } finally {            boolean removePending = decodeState == STATEHANDLERREMOVEDPENDING;            decodeState = STATE_INIT;            if (removePending) {                handlerRemoved(ctx);           }       }   }   //模板模式   //netty中对各种用户协议的支持就体现在这个抽象函数中   //传进去的in是累加器累加的数据 //是当前读取到的未被消费的所有的数据,以及业务协议包容器,所有的拆包器最终都实现了该抽象方法   //业务拆包完成之后,如果发现并没有拆到一个完整的数据包,这个时候又分两种情况   //1.一个是拆包器什么数据也没读取,可能数据还不够业务拆包器处理,直接break等待新的数据   //2.拆包器已读取部分数据,说明解码器仍然在工作,继续解码  protected abstract void decode     (ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception; ​ ​ //我们看下子类FixedLengthFrameDecoder#decode方法  @Override    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception {        //判断返回的字节不为空就加入 out        Object decoded = decode(ctx, in);        if (decoded != null) {            out.add(decoded);       }   } ​ ​ protected Object decode(            @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {     //判断累加器中的字节数小于固定长度的字节长度        if (in.readableBytes() < frameLength) {            //返回空            return null;       } else {            //否则返回可读的字节数 这里很重要            return in.readRetainedSlice(frameLength);       }   } ​ ​  protected void decodeLast       (ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception {        if (in.isReadable()) {            decodeRemovalReentryProtection(ctx, in, out);       }   } } ```

decode() 是用户必须实现的抽象方法,在该方法在调用时需要传入接收的数据 ByteBuf,及用来添加编码后消息的 List。

由于 TCP 粘包问题,ByteBuf 中可能包含多个有效的报文,或者不够一个完整的报文。

Netty 会重复回调 decode() 方法,直到没有解码出新的完整报文可以添加到 List 当中,或者 ByteBuf 没有更多可读取的数据为止。

如果此时 List 的内容不为空,那么会传递给 ChannelPipeline 中的下一个ChannelInboundHandler。触发channelRead方法。

此外 ByteToMessageDecoder 还定义了 decodeLast() 方法。

为什么抽象解码器要比编码器多一个 decodeLast() 方法呢?

因为 decodeLast 在 Channel 关闭后会被调用一次,主要用于处理 ByteBuf 最后剩余的字节数据。

Netty 中 decodeLast 的默认实现只是简单调用了 decode() 方法。如果有特殊的业务需求,则可以通过重写 decodeLast() 方法扩展自定义逻辑。

ByteToMessageDecoder 还有一个抽象子类是 ReplayingDecoder。

它封装了缓冲区的管理,在读取缓冲区数据时,你无须再对字节长度进行检查。因为如果没有足够长度的字节数据,ReplayingDecoder 将终止解码操作。

ReplayingDecoder 的性能相比直接使用 ByteToMessageDecoder 要慢,大部分情况下并不推荐使用 ReplayingDecoder。

二次解码器MessageToMessageDecoder

MessageToMessageDecoder实际上是Nety的二次解码器,从SocketChannel读取到的TCP数据报是ByteBuffer,先将解码为Java对象,再二次解码为POJO对象,因此称之为二次解码器。 以HTTP+XML协议栈为例,第一次解码是将字节数组解码成HttpRequest对象,然后对HttpRequest消息中的消息体字符串进行二次解码,将XML格式的字符串解码为POJO对象。 由于二次解码器是将一个POJO解码为另一个POJO,一般不涉及半包处理。

MessageToMessageDecoder 与 ByteToMessageDecoder 作用类似。

都是将一种消息类型的编码成另外一种消息类型。

与 ByteToMessageDecoder 不同的是 MessageToMessageDecoder 并不会对数据报文进行缓存,它主要用作转换消息模型。

比较推荐的做法是使用 ByteToMessageDecoder 解析 TCP 协议,解决拆包/粘包问题。解析得到有效的 ByteBuf 数据,然后传递给后续的 MessageToMessageDecoder 做数据对象的转换,具体流程如下图所示。

Lark20201109-102121.png

三种常用的解码器

FixedLengthFrameDecoder

DelimiterBasedFrameDecoder

LengthFieldBasedFrameDecoder

固定长度:FixedLengthFrameDecoder

public class FixedLengthFrameDecoder extends ByteToMessageDecoder { ​    private final int frameLength; ​    public FixedLengthFrameDecoder(int frameLength) {        checkPositive(frameLength, "frameLength");        this.frameLength = frameLength;   } ​    @Override    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {        Object decoded = decode(ctx, in);        if (decoded != null) {            out.add(decoded);       }   } ​ ​    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {        //判断读取到的数据是否小于定义数据的固定长度        if (in.readableBytes() < frameLength) {            //小于不处理            return null;       } else {         //否则只处理frameLength个长度的数据            return in.readRetainedSlice(frameLength);       }   } } ​

通信协议实战★

在之前通信协议设计中我们提到了协议的基本要素并给出了一个较为通用的协议示例。

下面我们通过 Netty 的编辑码框架实现该协议的解码器,加深我们对 Netty 编解码框架的理解。

其实dubbo和rocketMq都是这种方式。

在实现协议编码器之前,我们首先需要清楚一个问题:如何判断 ByteBuf 是否存在完整的报文?

最常用的做法就是通过读取消息长度 dataLength 进行判断。

如果 ByteBuf 的可读数据长度小于 dataLength,说明 ByteBuf 还不够获取一个完整的报文。

在该协议前面的消息头部分包含了魔数、协议版本号、数据长度等固定字段,共 14 个字节。

固定字段长度和数据长度可以作为我们判断消息完整性的依据,具体编码器实现逻辑示例如下:

java /* +---------------------------------------------------------------+ ​ | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | ​ +---------------------------------------------------------------+ ​ | 状态 1byte |       保留字段 4byte     |     数据长度 4byte     | ​ +---------------------------------------------------------------+ ​ |                   数据内容 (长度不定)                         | ​ +---------------------------------------------------------------+ */ ​ @Override ​ public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { ​    // 判断 ByteBuf 可读取字节    if (in.readableBytes() < 14) {        return;   } ​    in.markReaderIndex(); // 标记 ByteBuf 读指针位置    in.skipBytes(2); // 跳过魔数    in.skipBytes(1); // 跳过协议版本号    byte serializeType = in.readByte();    in.skipBytes(1); // 跳过报文类型    in.skipBytes(1); // 跳过状态字段    in.skipBytes(4); // 跳过保留字段    int dataLength = in.readInt();    if (in.readableBytes() < dataLength) {        in.resetReaderIndex(); // 重置 ByteBuf 读指针位置        return;   } ​    byte[] data = new byte[dataLength];    in.readBytes(data);    SerializeService serializeService = getSerializeServiceByType(serializeType);    Object obj = serializeService.deserialize(data);    if (obj != null) {        out.add(obj);   } } ​

总结

Netty 提供了一组 ChannelHandler 实现的抽象类,在项目开发中基于这些抽象类实现自定义的编解码器具备较好的可扩展性,最后我们通过具体示例协议的实战加深了对编解码器的理解。

当然 Netty 在编解码方面所做的工作远不止于此。它还提供了丰富的开箱即用的编解码器,下节课我们便一起探索实用的编解码技巧。

相关文章:

22.Netty源码之解码器

highlight: arduino-light 抽象解码类 https://mp.weixin.qq.com/s/526p5f9fgtZu7yYq5j7LiQ 解码器 Netty 常用解码器类型&#xff1a; ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象&#xff1b;MessageToMessageDecoder 将一种消息类型解码为另外一种消息类…...

R语言【Tidyverse、Tidymodel】的机器学习方法

机器学习已经成为继理论、实验和数值计算之后的科研“第四范式”&#xff0c;是发现新规律&#xff0c;总结和分析实验结果的利器。机器学习涉及的理论和方法繁多&#xff0c;编程相当复杂&#xff0c;一直是阻碍机器学习大范围应用的主要困难之一&#xff0c;由此诞生了Python…...

vscode 第一个文件夹在上一层文件夹同行,怎么处理

我的是这样的 打开终端特别麻烦 解决方法就是 打开vscode里边的首选项 进入设置 把Compact Folders下边对勾给勾掉...

[JavaScript游戏开发] 绘制冰宫宝藏地图、人物鼠标点击移动、障碍检测

系列文章目录 第一章 2D二维地图绘制、人物移动、障碍检测 第二章 跟随人物二维动态地图绘制、自动寻径、小地图显示(人物红点显示) 第三章 绘制冰宫宝藏地图、人物鼠标点击移动、障碍检测 第四章 绘制Q版地图、键盘上下左右地图场景切换 文章目录 系列文章目录前言一、本章节…...

【NLP概念源和流】 01-稀疏文档表示(第 1/20 部分)

一、介绍 自然语言处理(NLP)是计算方法的应用,不仅可以从文本中提取信息,还可以在其上对不同的应用程序进行建模。所有基于语言的文本都有系统的结构或规则,通常被称为形态学,例如“跳跃”的过去时总是“跳跃”。对于人类来说,这种形态学的理解是显而易见的。 在这篇介…...

服务器运行python程序的使用说明

服务器的使用与说明 文章目录 服务器的使用与说明1.登录2.Python的使用2.1 服务器已安装python32.2 往自己的用户目录安装python31.首先下载安装包2.解压缩3.编译与安装 2.3 新建环境变量2.4 测试 3 创建PBS作业并提交 1.登录 windowsr打开运行命令窗口&#xff0c;在运行框中…...

8.2一日总结

1.记录更新&#xff1a; untracked&#xff1a; 未追踪&#xff08;新增的文件&#xff09; unmodefied&#xff1a; 未修改 modefied&#xff1a; 已修改 staged&#xff1a; 已暂存 2、添加指定文件到暂存区&#xff1a; git add 文件名 gi…...

JavaScript(四)DOM及CSS操作

1、DOM简介 DocumentType: Html的声明标签 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Docume…...

window中,关闭java占用端口的进程

查看端口被占用的情况 netstat -ano|findstr "端口号"使用Tasklist查看对于 PID 的进程名 tasklist|findstr "PID号"通过 taskkill 命令方式结束进程 taskkill /f /t /im Pid...

【Python】PySpark 数据计算 ⑤ ( RDD#sortBy方法 - 排序 RDD 中的元素 )

文章目录 一、RDD#sortBy 方法1、RDD#sortBy 语法简介2、RDD#sortBy 传入的函数参数分析 二、代码示例 - RDD#sortBy 示例1、需求分析2、代码示例3、执行结果 一、RDD#sortBy 方法 1、RDD#sortBy 语法简介 RDD#sortBy 方法 用于 按照 指定的 键 对 RDD 中的元素进行排序 , 该方…...

Elasticsearch官方测试数据导入

一、数据准备 百度网盘链接 链接&#xff1a;https://pan.baidu.com/s/1rPZBvH-J0367yQDg9qHiwQ?pwd7n5n 提取码&#xff1a;7n5n文档格式 {"index":{"_id":"1"}} {"account_number":1,"balance":39225,"firstnam…...

uniapp项目的pdf文件下载与打开查看

最近写的uniapp项目需要新增一个pdf下载和打开查看功能&#xff0c;摸索了半天终于写了出来&#xff0c;现分享出来供有需要的同行参考&#xff0c;欢迎指正 async function DownloadSignature() {//请求后端接口&#xff0c;返回值为一个url地址let resawait req.flow.flowDo…...

DeepVO 论文阅读

论文信息 题目&#xff1a;DeepVO Towards End-to-End Visual Odometry with Deep Recurrent Convolutional Neural Networks 作者&#xff1a;Sen Wang, Ronald Clark, Hongkai Wen and Niki Trigoni 代码地址&#xff1a;http://senwang.gitlab.io/DeepVO/ (原作者并没有开源…...

HOT71-字符串解码

leetcode原题链接: 字符串解码 题目描述 给定一个经过编码的字符串&#xff0c;返回它解码后的字符串。 编码规则为: k[encoded_string]&#xff0c;表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。你可以认为输入字符串总是有效的&#xff1b;输…...

redis-server进程无法关闭终极解决方案

先使用命令查看6379端口情况&#xff1a; sudo lsof -i :6379 发现redis进程在占用&#xff0c;redis-server进程无论什么手段都杀不死&#xff0c;使用kill -9 pid杀掉pid后又卷土重来&#xff0c;最后找到了下面这个命令 sudo /etc/init.d/redis-server stop ok&#xff0c…...

(5)将固件加载到没有ArduPilot固件的主板上

文章目录 前言 5.1 下载驱动程序和烧录工具 5.2 下载ArduPilot固件 5.3 使用测试版和开发版 5.3.1 测试版 5.3.2 最新开发版本 5.4 将固件上传到自动驾驶仪 5.5 替代方法 5.6 将固件加载到带有外部闪存的主板上 前言 ArduPilot 的最新版本&#xff08;Copter-3.6, Pl…...

wpf画刷学习1

在这2篇博文有提到wpf画刷&#xff0c; https://blog.csdn.net/bcbobo21cn/article/details/109699703 https://blog.csdn.net/bcbobo21cn/article/details/107133703 下面单独学习一下画刷&#xff1b; wpf有五种画刷&#xff0c;也可以自定义画刷&#xff0c;画刷的基类都…...

Opencv C++实现yolov5部署onnx模型完成目标检测

代码分析&#xff1a; 头文件 #include <fstream> //文件 #include <sstream> //流 #include <iostream> #include <opencv2/dnn.hpp> //深度学习模块-仅提供推理功能 #include <opencv2/imgproc.hpp> //图像处理模块 #include &l…...

django bootstrap html实现左右布局,带折叠按钮,左侧可折叠隐藏

一、实现的效果 在django项目中,需要使用bootstrap 实现一个左右分布的布局,左侧区域可以折叠隐藏起来,使得右侧的显示区域变大。(为了区分区域,左右加了配色,不好看的修改颜色即可) 点击折叠按钮,左侧区域隐藏,右侧区域铺满: 二、实现思路 1、使用col-md属性,让左…...

Mapping温度分布验证选择数据记录仪时需要考虑的13件事

01 什么是温度分布验证&#xff1f; 温度分布验证是通过在规定的研究时间内测量定义区域内的多个点来确定特定温度控制环境或过程&#xff08;如冷冻柜、冰箱、培养箱、稳定室、仓库或高压灭菌器&#xff09;的温度分布的过程。温度分布验证的目标是确定每个测量点之间的差异&…...

RestClient

什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端&#xff0c;它允许HTTP与Elasticsearch 集群通信&#xff0c;而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级&#xff…...

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘

美国西海岸的夏天&#xff0c;再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至&#xff0c;这不仅是开发者的盛宴&#xff0c;更是全球数亿苹果用户翘首以盼的科技春晚。今年&#xff0c;苹果依旧为我们带来了全家桶式的系统更新&#xff0c;包括 iOS 26、iPadOS 26…...

Golang dig框架与GraphQL的完美结合

将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用&#xff0c;可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器&#xff0c;能够帮助开发者更好地管理复杂的依赖关系&#xff0c;而 GraphQL 则是一种用于 API 的查询语言&#xff0c;能够提…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力

引言&#xff1a; 在人工智能快速发展的浪潮中&#xff0c;快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型&#xff08;LLM&#xff09;。该模型代表着该领域的重大突破&#xff0c;通过独特方式融合思考与非思考…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明

AI 领域的快速发展正在催生一个新时代&#xff0c;智能代理&#xff08;agents&#xff09;不再是孤立的个体&#xff0c;而是能够像一个数字团队一样协作。然而&#xff0c;当前 AI 生态系统的碎片化阻碍了这一愿景的实现&#xff0c;导致了“AI 巴别塔问题”——不同代理之间…...

【python异步多线程】异步多线程爬虫代码示例

claude生成的python多线程、异步代码示例&#xff0c;模拟20个网页的爬取&#xff0c;每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程&#xff1a;允许程序同时执行多个任务&#xff0c;提高IO密集型任务&#xff08;如网络请求&#xff09;的效率…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。

1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj&#xff0c;再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列&#xff0c;以便知晓哪些列包含有价值的数据&#xff0c;…...

Python ROS2【机器人中间件框架】 简介

销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...