Netty 源码分析系列(十八)一行简单的writeAndFlush都做了哪些事?

文章目录

    • 前言
    • 源码分析
      • ctx.writeAndFlush 的逻辑
      • writeAndFlush 源码
      • ChannelOutBoundBuff 类
      • addMessage 方法
      • addFlush 方法
      • AbstractNioByteChannel 类
    • 小结


前言

对于使用netty的小伙伴来说,我们想通过服务端往客户端发送数据,通常我们会调用ctx.writeAndFlush(数据)的方式。那么它都执行了那些行为呢,是怎么将消息发送出去的呢。

源码分析

下面的这个方法是用来接收客户端发送过来的数据,通常会使用ctx.writeAndFlush(数据)来向客户端发送数据。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    System.out.println(" 接收到消息:" + msg);
    String str = "服务端收到:" + new Date() + msg;
    ctx.writeAndFlush(str);
}

ctx.writeAndFlush 的逻辑

private void write(Object msg, boolean flush, ChannelPromise promise) {

    //...

    AbstractChannelHandlerContext next = this.findContextOutbound(flush ? 98304 : '耀');
    Object m = this.pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }
    } else {
        Object task;
        if (flush) {
            task = AbstractChannelHandlerContext.WriteAndFlushTask.newInstance(next, m, promise);
        } else {
            task = AbstractChannelHandlerContext.WriteTask.newInstance(next, m, promise);
        }

        if (!safeExecute(executor, (Runnable)task, promise, m)) {
            ((AbstractChannelHandlerContext.AbstractWriteTask)task).cancel();
        }
    }

}

从上述源码我们可以知道,WriteAndFlush()相对于Write(),它的flush字段是true。

write:将需要写的 ByteBuff 存储到 ChannelOutboundBuffer中。

flush:从ChannelOutboundBuffer中将需要发送的数据读出来,并通过 Channel 发送出去。

writeAndFlush 源码

public ChannelFuture writeAndFlush(Object msg) {
    return this.writeAndFlush(msg, this.newPromise());
}

public ChannelPromise newPromise() {
    return new DefaultChannelPromise(this.channel(), this.executor());
}

writeAndFlush方法里提供了一个默认的 newPromise()作为参数传递。在Netty中发送消息是一个异步操作,那么可以通过往hannelPromise中注册回调监听listener来得到该操作是否成功。

在发送消息时添加监听

ctx.writeAndFlush(str,ctx.newPromise().addListener(new ChannelFutureListener(){
    @Override
    public void operationComplete(ChannelFuture channelFuture) throws Exception{
        channelFuture.isSuccess();
    }
}));

继续向下一层跟进代码,AbstractChannelHandlerContext中的invokeWriteAndFlush的源码。

private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
    if (this.invokeHandler()) {
        this.invokeWrite0(msg, promise);
        this.invokeFlush0();
    } else {
        this.writeAndFlush(msg, promise);
    }

}

从上述源码我们可以能够知道:

1、首先通过invokeHandler()判断通道处理器是否已添加到管道中。

2、执行消息处理 invokeWrite0方法:

  • 首先将消息内容放入输出缓冲区中 invokeFlush0;
  • 然后将输出缓冲区中的数据通过socket发送到网络中。

分析invokeWrite0执行的内容,源码如下:

private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        ((ChannelOutboundHandler)this.handler()).write(this, msg, promise);
    } catch (Throwable var4) {
        notifyOutboundHandlerException(var4, promise);
    }

}

((ChannelOutboundHandler)this.handler()).write是一个出站事件ChannelOutboundHandler,会由ChannelOutboundHandlerAdapter处理。

@Skip
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ctx.write(msg, promise);
}

接下来会走到ChannelPipeline中,来执行网络数据发送;我们来看DefaultChannelPipeline 中HeadContextwrite方法源码

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
    this.unsafe.write(msg, promise);
}

unsafe是构建NioServerSocketChannelNioSocketChannel对象时,一并构建一个成员属性,它会完成底层真正的网络操作等。

我们跟进HenderContext的write() ,而HenderContext的中依赖的是unsafe.wirte()。所以直接去 AbstractChannel的Unsafe 源码如下:

public final void write(Object msg, ChannelPromise promise) {
    this.assertEventLoop();
    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null) {// 缓存 写进来的 buffer
        this.safeSetFailure(promise, this.newWriteException(AbstractChannel.this.initialCloseCause));
        ReferenceCountUtil.release(msg);
    } else {
        int size;
        try {
            // buffer Dirct化 , (我们查看 AbstractNioByteBuf的实现)
            msg = AbstractChannel.this.filterOutboundMessage(msg);
            size = AbstractChannel.this.pipeline.estimatorHandle().size(msg);
            if (size < 0) {
                size = 0;
            }
        } catch (Throwable var6) {
            this.safeSetFailure(promise, var6);
            ReferenceCountUtil.release(msg);
            return;
        }
        //  插入写队列  将 msg 插入到 outboundBuffer
        //  outboundBuffer 这个对象是 ChannelOutBoundBuff 类型的,它的作用就是起到一个容器的作用
        //  下面看, 是如何将 msg 添加进 ChannelOutBoundBuff 中的
        outboundBuffer.addMessage(msg, size, promise);
    }
}

从上述源码中,我们可以看出,首先调用 assertEventLoop 确保该方法的调用是在reactor线程中;然后,调用 filterOutboundMessage() 方法,将待写入的对象过滤。下面我们来看看filterOutboundMessage方法的源码。

protected final Object filterOutboundMessage(Object msg) {
    if (msg instanceof ByteBuf) {
        ByteBuf buf = (ByteBuf)msg;
        return buf.isDirect() ? msg : this.newDirectBuffer(buf);
    } else if (msg instanceof FileRegion) {
        return msg;
    } else {
        throw new UnsupportedOperationException("unsupported message type: " + StringUtil.simpleClassName(msg) + 																											EXPECTED_TYPES);
    }
}

从上述源码可以看出,只有ByteBuf以及 FileRegion可以进行最终的Socket网络传输,其他类型的数据是不支持的,会抛UnsupportedOperationException异常。并且会把堆 ByteBuf 转换为一个非堆的 ByteBuf 返回。也就说,最后会通过socket传输的对象时非堆的 ByteBuf 和 FileRegion。

在发送数据时,我们需要估算出需要写入的 ByteBuf 的size,我们来看看 DefaultMessageSizeEstimator 的HandleImpl类中的size()方法。

public final class DefaultMessageSizeEstimator implements MessageSizeEstimator {

    private static final class HandleImpl implements Handle {
        private final int unknownSize;

        private HandleImpl(int unknownSize) {
            this.unknownSize = unknownSize;
        }

        public int size(Object msg) {
            if (msg instanceof ByteBuf) {
                return ((ByteBuf)msg).readableBytes();
            } else if (msg instanceof ByteBufHolder) {
                return ((ByteBufHolder)msg).content().readableBytes();
            } else {
                return msg instanceof FileRegion ? 0 : this.unknownSize;
            }
        }
    }
}

通过ByteBuf.readableBytes()判断消息内容大小,估计待发送消息数据的大小,如果是FileRegion的话直接返回0,否则返回ByteBuf中可读取字节数。

接下来我们来看看是如何将 msg 添加进 ChannelOutBoundBuff 中的。

ChannelOutBoundBuff 类

ChannelOutboundBuffer类主要用于存储其待处理的出站写请求的内部数据。当 Netty 调用 write时数据不会真正地去发送而是写入到ChannelOutboundBuffer 缓存队列,直到调用 flush方法 Netty 才会从ChannelOutboundBuffer取数据发送。每个 Unsafe 都会绑定一个ChannelOutboundBuffer,也就是说每个客户端连接上服务端都会创建一个 ChannelOutboundBuffer 绑定客户端 Channel。

观察 ChannelOutBoundBuff 源码,可以看到以下四个属性:

public final class ChannelOutboundBuffer {

    //...
    
    private ChannelOutboundBuffer.Entry flushedEntry;
    private ChannelOutboundBuffer.Entry unflushedEntry;
    private ChannelOutboundBuffer.Entry tailEntry;
    private int flushed;
    
    //...
    
}
  1. flushedEntry :指针表示第一个被写到操作系统Socket缓冲区中的节点;

  2. unFlushedEntry:指针表示第一个未被写入到操作系统Socket缓冲区中的节点;

  3. tailEntry:指针表示ChannelOutboundBuffer缓冲区的最后一个节点。

  4. flushed:表示待发送数据个数。

下面分别是三个指针的作用,示意图如下:

image-20210922154632809

  1. flushedEntry 指针表示第一个被写到操作系统Socket缓冲区中的节点;
  2. unFlushedEntry指针表示第一个未被写入到操作系统Socket缓冲区中的节点;
  3. tailEntry指针表示ChannelOutboundBuffer缓冲区的最后一个节点。

初次调用 addMessage 之后,各个指针的情况为:

image-20210922155035351

fushedEntry指向空,unFushedEntrytailEntry 都指向新加入的节点。第二次调用 addMessage之后,各个指针的情况为:

image-20210922155156086

第n次调用 addMessage之后,各个指针的情况为:

image-20210922155428422

可以看到,调用n次addMessageflushedEntry指针一直指向NULL,表示现在还未有节点需要写出到Socket缓冲区。

ChannelOutboundBuffer 主要提供了以下方法:

  • addMessage方法:添加数据到对列的队尾;
  • addFlush方法:准备待发送的数据,在 flush 前需要调用;
  • nioBuffers方法:用于获取待发送的数据。在发送数据的时候,需要调用该方法以便拿到数据;
  • removeBytes方法:发送完成后需要调用该方法来删除已经成功写入TCP缓存的数据。

addMessage 方法

addMessage 方法是系统调用write方法时调用,源码如下。

public void addMessage(Object msg, int size, ChannelPromise promise) {
    ChannelOutboundBuffer.Entry entry = ChannelOutboundBuffer.Entry.newInstance(msg, size, total(msg), promise);
    if (this.tailEntry == null) {
        this.flushedEntry = null;
    } else {
        ChannelOutboundBuffer.Entry tail = this.tailEntry;
        tail.next = entry;
    }

    this.tailEntry = entry;
    if (this.unflushedEntry == null) {
        this.unflushedEntry = entry;
    }

    this.incrementPendingOutboundBytes((long)entry.pendingSize, false);
}

上述源码流程如下:

  • 将消息数据包装成 Entry 对象;
  • 如果对列为空,直接设置尾结点为当前节点,否则将新节点放尾部;
  • unflushedEntry为空说明不存在暂时不需要发送的节点,当前节点就是第一个暂时不需要发送的节点;
  • 将消息添加到未刷新的数组后,增加挂起的节点。

这里需要重点看看第一步将消息数据包装成 Entry 对象的方法。

static final class Entry {
    private static final Recycler<ChannelOutboundBuffer.Entry> RECYCLER = new Recycler<ChannelOutboundBuffer.Entry>() {
        protected ChannelOutboundBuffer.Entry newObject(Handle<ChannelOutboundBuffer.Entry> handle) {
            return new ChannelOutboundBuffer.Entry(handle);
        }
    };

    // ...

    static ChannelOutboundBuffer.Entry newInstance(Object msg, int size, long total, ChannelPromise promise) {
        ChannelOutboundBuffer.Entry entry = (ChannelOutboundBuffer.Entry)RECYCLER.get();
        entry.msg = msg;
        entry.pendingSize = size + ChannelOutboundBuffer.CHANNEL_OUTBOUND_BUFFER_ENTRY_OVERHEAD;
        entry.total = total;
        entry.promise = promise;
        return entry;
    }

    // ...

}

其中Recycler类是基于线程本地堆栈的轻量级对象池。这意味着调用newInstance方法时 ,并不是直接创建了一个 Entry 实例,而是通过对象池获取的。

下面我们看看incrementPendingOutboundBytes方法的源码。

private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
    if (size != 0L) {
        // TOTAL_PENDING_SIZE_UPDATER 当前缓存中 存在的代写的 字节
        // 累加
        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
        // 判断 新的将被写的 buffer的容量不能超过  getWriteBufferHighWaterMark() 默认是 64*1024  64字节
        if (newWriteBufferSize > (long)this.channel.config().getWriteBufferHighWaterMark()) {
            // 超过64 字节,进入这个方法
            this.setUnwritable(invokeLater);
        }

    }
}

在每次添加新的节点后都调用incrementPendingOutboundBytes((long)entry.pendingSize, false)方法,这个方法的作用是设置写状态,设置怎样的状态呢?我们看它的源码,可以看到,它会记录下累计的ByteBuf的容量,一旦超出了阈值,就会传播channel不可写的事件。

addFlush 方法

addFlush 方法是在系统调用 flush 方法时调用的,addFlush 方法的源码如下。

public void addFlush() {
    ChannelOutboundBuffer.Entry entry = this.unflushedEntry;
    if (entry != null) {
        if (this.flushedEntry == null) {
            this.flushedEntry = entry;
        }

        do {
            ++this.flushed;
            if (!entry.promise.setUncancellable()) {
                int pending = entry.cancel();
                this.decrementPendingOutboundBytes((long)pending, false, true);
            }

            entry = entry.next;
        } while(entry != null);

        this.unflushedEntry = null;
    }

}

以上方法的主要功能就是暂存数据节点变成待发送节点,即flushedEntry 指向的节点到unFlushedEntry指向的节点(不包含 unFlushedEntry)之间的数据。

上述源码的流程如下:

  • 先获取unFlushedEntry指向的暂存数据的起始节点;
  • 将待发送数据起始指针flushedEntry 指向暂存起始节点;
  • 通过promise.setUncancellable()锁定待发送数据,并在发送过程中取消,如果锁定过程中发现其节点已经取消,则调用entry.cancel()取消节点发送,并减少待发送的总字节数。

下面我们看看decrementPendingOutboundBytes方法的源码。

private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) {
    if (size != 0L) {
        // 每次 减去 -size
        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
            //  默认 getWriteBufferLowWaterMark() -32kb
    		//  newWriteBufferSize<32 就把不可写状态改为可写状态
        if (notifyWritability && newWriteBufferSize < (long)this.channel.config().getWriteBufferLowWaterMark()) {
            this.setWritable(invokeLater);
        }
    }
}

AbstractNioByteChannel 类

在这个类中,我们主要看doWrite(ChannelOutboundBuffer in)方法,源码如下。

protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    int writeSpinCount = this.config().getWriteSpinCount();

    do {
        Object msg = in.current();
        if (msg == null) {
            this.clearOpWrite();
            return;
        }

        writeSpinCount -= this.doWriteInternal(in, msg);
    } while(writeSpinCount > 0);

    this.incompleteWrite(writeSpinCount < 0);
}

通过一个无限循环,保证可以拿到所有的节点上的ByteBuf,通过这个函数获取节点,Object msg = in.current();
我们进一步看它的实现,如下,它只会取出我们标记的节点。

public Object current() {
    ChannelOutboundBuffer.Entry entry = this.flushedEntry;
    return entry == null ? null : entry.msg;
}

下面我们看下doWriteInternal(in, msg)的方法源码。

private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        ByteBuf buf = (ByteBuf)msg;
        if (!buf.isReadable()) {
            in.remove();
            return 0;
        }

        int localFlushedAmount = this.doWriteBytes(buf);
        if (localFlushedAmount > 0) {
            in.progress((long)localFlushedAmount);
            if (!buf.isReadable()) {
                in.remove();
            }

            return 1;
        }
    } else {
        if (!(msg instanceof FileRegion)) {
            throw new Error();
        }

        FileRegion region = (FileRegion)msg;
        if (region.transferred() >= region.count()) {
            in.remove();
            return 0;
        }

        long localFlushedAmount = this.doWriteFileRegion(region);
        if (localFlushedAmount > 0L) {
            in.progress(localFlushedAmount);
            if (region.transferred() >= region.count()) {
                in.remove();
            }

            return 1;
        }
    }

    return 2147483647;
}

使用 jdk 的自旋锁,循环16次,尝试往 jdk 底层的ByteBuffer中写数据,调用函数doWriteBytes(buf);他具体的实现是客户端 channel 的封装类NioSocketChannel实现的源码如下:

protected int doWriteBytes(ByteBuf buf) throws Exception {
    int expectedWrittenBytes = buf.readableBytes();
    // 将字节数据, 写入到 java 原生的 channel中
    return buf.readBytes(this.javaChannel(), expectedWrittenBytes);
}

这个readBytes()依然是抽象方法,因为前面我们曾经把从 ByteBuf 转化成了 Dirct 类型的,所以它的实现类是PooledDirctByteBuf 继续跟进如下:

public int readBytes(GatheringByteChannel out, int length) throws IOException {
    this.checkReadableBytes(length);
    // 关键的就是 getBytes()  跟进去
    int readBytes = this.getBytes(this.readerIndex, out, length, true);
    this.readerIndex += readBytes;
    return readBytes;
}


private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException {
    this.checkIndex(index, length);
    if (length == 0) {
        return 0;
    } else {
        ByteBuffer tmpBuf;
        if (internal) {
            tmpBuf = this.internalNioBuffer();
        } else {
            tmpBuf = ((ByteBuffer)this.memory).duplicate();
        }

        index = this.idx(index);
        // 将netty 的 ByteBuf 塞进 jdk的 ByteBuffer tmpBuf;
        tmpBuf.clear().position(index).limit(index + length);
        // 调用jdk的write()方法
        return out.write(tmpBuf);
    }
}

被使用过的节点会被remove()掉, 源码如下。

private void removeEntry(ChannelOutboundBuffer.Entry e) {
    if (--this.flushed == 0) { // 如果是最后一个节点,把所有的指针全部设为null
        this.flushedEntry = null;
        if (e == this.tailEntry) {
            this.tailEntry = null;
            this.unflushedEntry = null;
        }
    } else { // 如果不是最后一个节点, 把当前节点,移动到最后的节点
        this.flushedEntry = e.next;
    }

}

小结

1、调用write方法并没有将数据写到 Socket 缓冲区中,而是写到了一个单向链表的数据结构中,flush才是真正的写出。

2、writeAndFlush等价于先将数据写到netty的缓冲区,再将netty缓冲区中的数据写到Socket缓冲区中,写的过程与并发编程类似,用自旋锁保证写成功。

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

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

相关文章

实时聊天组合功能,你了解吗?

你有兴趣安装实时聊天组合功能吗&#xff1f;如果您选择了SaleSmartly&#xff08;ss客服&#xff09;&#xff0c;您的实时聊天插件可以不仅仅只是聊天通道&#xff0c;还可以有各种各样的功能&#xff0c;你不需要包含每一个功能&#xff0c;正所谓「宁缺勿滥」&#xff0c;功…

再获认可!腾讯连续三年被Gartner列为CWPP供应商之一

随着云的快速发展&#xff0c;企业的工作负载已经从服务器发展到虚拟机、容器、serverless等&#xff0c;部署的模式也日益复杂&#xff0c;包括公有云、混合云和多云等。在此背景下&#xff0c;传统的主机安全防护已无法满足需求&#xff0c;CWPP&#xff08;云工作负载保护平…

C#,码海拾贝(23)——求解“复系数线性方程组“的“全选主元高斯消去法“之C#源代码,《C#数值计算算法编程》源代码升级改进版

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 求解线性方程组的类 LEquations /// 原作 周长发 /// 改编 深度混淆 /// </summary> public static partial class LEquations { /// <summary&g…

day20 - 绘制物体的运动轨迹

在我们平常做目标检测或者目标追踪时&#xff0c;经常要画出目标的轨迹图。绘制轨迹图的一种方法就是利用光流估计来进行绘制。 本期我们主要来介绍视频中光流估计的使用和效果&#xff0c;利用光流估计来绘制运动轨迹。 完成本期内容&#xff0c;你可以&#xff1a; 掌握视…

网站部署与上线(1)虚拟机

文章目录 .1 虚拟机简介2 虚拟机的安装 本章将搭建实例的生产环境&#xff0c;将所有的代码搭建在一台Linux服务器中&#xff0c;并且测试其能否正常运行。 使用远程服务器进行连接&#xff1b; 基本的Linux命令&#xff1b; 使用Nginx搭建Node.js服务器&#xff1b; 在服务器端…

一、预约挂号详情

文章目录 一、预约挂号详情1、需求分析 2、api接口2.1 添加service接口2.2 添加service接口实现2.2.1 在ScheduleServiceImpl类实现接口2.2.2 在获取科室信息 2.3 添加controller方法 3、前端3.1封装api请求3.2 页面展示 二、预约确认1、api接口1.1 添加service接口1.2 添加con…

通过python采集整站lazada商品列表数据,支持多站点

要采集整站lazada商品列表数据&#xff0c;需要先了解lazada网站的结构和数据源。Lazada是东南亚最大的电商平台之一&#xff0c;提供各种商品和服务。Lazada的数据源主要分为两种&#xff1a;HTML和API。 方法1&#xff1a;采集HTML数据 步骤1&#xff1a;确定采集目标 首先…

一、CNNs网络架构-基础网络架构(LeNet、AlexNet、ZFNet)

目录 1.LeNet 2.AlexNet 2.1 激活函数&#xff1a;ReLU 2.2 随机失活&#xff1a;Droupout 2.3 数据扩充&#xff1a;Data augmentation 2.4 局部响应归一化&#xff1a;LRN 2.5 多GPU训练 2.6 论文 3.ZFNet 3.1 网络架构 3.2 反卷积 3.3 卷积可视化 3.4 ZFNet改…

Java的Arrays类的sort()方法(41)

目录 sort&#xff08;&#xff09;方法 1.sort&#xff08;&#xff09;方法的格式 2.使用sort&#xff08;&#xff09;方法时要导入的类 3.作用 4.作用的对象 5.注意 6.代码及结果 &#xff08;1&#xff09;代码 &#xff08;2&#xff09;结果 sort&#xff08;&…

【Netty】字节缓冲区 ByteBuf (六)(上)

文章目录 前言一、ByteBuf类二、ByteBuffer 实现原理2.1 ByteBuffer 写入模式2.2 ByteBuffer 读取模式2.3 ByteBuffer 写入模式切换为读取模式2.4 clear() 与 compact() 方法2.5 ByteBuffer 使用案例 总结 前言 回顾Netty系列文章&#xff1a; Netty 概述&#xff08;一&…

亏损?盈利?禾赛科技Q1财报背后的激光雷达赛道「现实」

随着禾赛科技在去年登陆美股&#xff0c;作为全球为数不多已经开始前装量产交付的激光雷达上市公司&#xff0c;财务数据的变化&#xff0c;也在一定程度上反映了行业的真实状况。 根据禾赛科技最新发布的今年一季度财报显示&#xff0c;公司季度净营收为4.3亿元&#xff08;人…

day13 - 对指纹图片进行噪声消除

在指纹识别的过程中&#xff0c;指纹图片通常都是现场采集的&#xff0c;受环境的影响会有产生很多的噪声点&#xff0c;如果直接使用&#xff0c;会对指纹的识别产生很大的影响&#xff0c;而指纹识别的应用场景又都是一些比较严肃不容有错的场合&#xff0c;所以去除噪声又不…

python+vue空巢老人网上药店购药系统9h2k5

本空巢老人购药系统主要包括三大功能模块&#xff0c;即用户功能模块、家属功能模块和管理员功能模块。 &#xff08;1&#xff09;管理员模块&#xff1a;系统中的核心用户是管理员&#xff0c;管理员登录后&#xff0c;通过管理员功能来管理后台系统。主要功能有&#xff1a;…

【实验】SegViT: Semantic Segmentation with Plain Vision Transformers

想要借鉴SegViT官方模型源码部署到本地自己代码文件中 1. 环境配置 官网要求安装mmcv-full1.4.4和mmsegmentation0.24.0 在这之前记得把mmcv和mmsegmentation原来版本卸载 pip uninstall mmcv pip uninstall mmcv-full pip uninstall mmsegmentation安装mmcv 其中&#xff…

旋翼无人机常用仿真工具

四旋翼常用仿真工具 rviz&#xff1a; 简单的质点&#xff08;也可以加上动力学姿态&#xff09;&#xff0c;用urdf模型在rviz中显示无人机和飞行轨迹、地图等。配合ROS代码使用&#xff0c;轻量化适合多机。典型的比如浙大ego-planner的仿真&#xff1a; https://github.c…

Java面试知识点(全)-分布式算法- ZAB算法

Java面试知识点(全) 导航&#xff1a; https://nanxiang.blog.csdn.net/article/details/130640392 注&#xff1a;随时更新 研究zookeeper时&#xff0c;必须要了解zk的选举和集群间个副本间的数据一致性。 什么是 ZAB 协议&#xff1f; ZAB 协议介绍 ZAB 协议全称&#xf…

树和二叉树

树 逻辑表示方法 树形表示法 文氏图表示法 凹入表示法 括号表示法 性质 树的结点数等于所有结点的度加一 度为m的树中第i层最多有m的(i-1)次方个结点 高度为h的m次树最多的节点数&#xff08;等比数列公式求和&am…

【数据结构】什么是堆,如何使用无序数组生成一个堆?

文章目录 一、堆的概念及其介绍二、如何使用无序序列构建一个堆&#xff1f;三、C语言实现堆的基本操作结构体创建与销毁获取堆顶数据与个数及堆的判空堆的插入与删除 源代码分享 一、堆的概念及其介绍 堆(Heap)是计算机科学中一类特殊的数据结构的统称&#xff0c;堆通常是一…

公网远程连接Redis数据库【内网穿透】

文章目录 1. Linux(centos8)安装redis数据库2. 配置redis数据库3. 内网穿透3.1 安装cpolar内网穿透3.2 创建隧道映射本地端口 4. 配置固定TCP端口地址4.1 保留一个固定tcp地址4.2 配置固定TCP地址4.3 使用固定的tcp地址连接 转发自cpolar内网穿透的文章&#xff1a;公网远程连接…

docker构建镜像上传到DockerHub

docker构建镜像上传到DockerHub DockerHub注册账号 DockerHub网址: https://hub.docker.com/ 注册 登录 安装docker docker宿主机环境 centos7 参考网址: https://yeasy.gitbook.io/docker_practice/install/centos 测试 docker 是否安装好 docker -v登录docker 登录 dock…