NettyのFuturePromise、HandlerPipeline、ByteBuf

本篇介绍Netty的剩下三个组件Future&Promise、Handler&Pipeline、ByteBuf

1、Future&Promise

        Future和Promise都是Netty实现异步的组件。

       
         1.1、JDK中的future

        在JDK中也有一个同名的Future,通常是配合多线程的Callable以及线程池的submit()方法使用,通过Future的get()方法在主线程处阻塞等待异步处理的结果:

@Slf4j
public class TestJDKFuture {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(2);

        Future<Integer> future = service.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.debug("执行call方法");
                Thread.sleep(1000);
                return 30;
            }
        });

        log.debug("main 线程运行");
        //会阻塞
        log.debug("获取到的结果是:{}",future.get());
        log.debug("main 线程执行其他逻辑");
    }
}

        在get()方法拿到线程池中任务的返回结果后,主线程结束阻塞才执行后续的逻辑:

        
         1.2、Netty中的future

        Netty中的Future,相比较JDK中的Future做出了增强,增强点在:

  • getNow()方法立刻得到异步处理结果,还未产生结果时返回 null。
  • addLinstener()添加回调,异步接收结果,主线程不会阻塞。
@Slf4j
public class TestNettyFuture {
    public static void main(String[] args) {
        EventLoopGroup group = new NioEventLoopGroup();

        EventLoop eventLoop = group.next();

        Future<Integer> future = eventLoop.submit(() -> {
            log.debug("执行call方法");
            Thread.sleep(1000);
            return 30;
        });

        //由执行call方法的线程异步获取结果,不会阻塞主线程
        future.addListener(future1 -> log.debug("获取到的结果:{}", future1.getNow()));
        log.debug("main 线程执行");
    }
}

       
        1.3、Netty中的promise

        在上面的案例中,无论是JDK和Netty的Future,都是被动地接受多线程任务异步返回的结果。

        而promise相当于一个容器,可以主动地设置多线程任务异步返回成功或失败的结果:

@Slf4j
public class TestPromise {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        EventLoop eventLoop = new NioEventLoopGroup().next();

        DefaultPromise<Object> promise = new DefaultPromise<>(eventLoop);

        new Thread(() -> {
            try {
                log.debug("开始计算");
//                int i = 1 / 0;
                //设置成功结果
                promise.setSuccess(30);
            } catch (Exception e) {
                e.printStackTrace();
                //设置失败结果
                promise.setFailure(e);
            }
        }, "test").start();

        //会一直阻塞直到获取到最终结果
        log.debug("获取到的结果:{}",promise.get());
        log.debug("main 线程执行其他代码");
    }
}

         没有发生异常,返回成功的结果:

        将代码中的int i = 1 / 0 放开,发生异常,返回失败的结果:

2、Handler & Pipeline

        Handler是Netty中的一个接口,用于处理特定类型的事件或数据,主要分为:

  • ChannelInboundHandler:处理入站事件和数据
  • ChannelOutboundHandler:处理出站数据和事件。

什么是入站和出站?

        入站指的是数据从外部(例如客户端)进入到服务器(或其他网络组件)内部的过程。在Netty中,入站数据通常包括接收自网络的字节流、连接事件、解码后的消息等。(服务器接受客户端的消息数据,消息进入服务器

        出站指的是数据从内部(例如服务器)发送到外部(例如客户端)的过程。在Netty中,出站数据通常包括编码后的消息、写入网络的字节流等。(服务器向客户端发送数据消息,消息从服务器发出)

        而Pipeline类似于流水线,由多个Handler组成,Handler可以理解成流水线上的各道工序。是Netty中的一个数据通道,负责管理和组织多个Handler的执行顺序。当一个Channel被创建时,Netty会自动创建一个与之关联的Pipeline。

        相关案例:

@Slf4j
public class PipelineDemo1 {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup(),new NioEventLoopGroup(2))
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    /**
                     * head -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> tail
                     * @param ch
                     * @throws Exception
                     */
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("1");
                                super.channelRead(ctx,msg);
                            }
                        });
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("2");
                                super.channelRead(ctx,msg);
                            }
                        });
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("3");
                                super.channelRead(ctx,msg);
                                //向byteBuff写入字节
                                ch.writeAndFlush(ctx.alloc().buffer().writeBytes("abc".getBytes(StandardCharsets.UTF_8)));
                            }
                        });
                        ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.debug("4");
                                super.write(ctx, msg, promise);
                            }
                        });
                        ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.debug("5");
                                super.write(ctx, msg, promise);
                            }
                        });
                        ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.debug("6");
                                super.write(ctx, msg, promise);
                            }
                        });

                    }
                })
                .bind(8080);
    }
}

        它的底层是一个双向链表:

        我们看一下它的执行顺序:

        会发现InBound事件是从前往后执行的,而outBound事件是从后往前执行的。

        关于outBound事件的触发时机,为什么需要手动调用这段代码?

ch.writeAndFlush(ctx.alloc().buffer().writeBytes("abc".getBytes(StandardCharsets.UTF_8)));

        原因在于,入站事件是指从客户端到服务器的数据流动,这些事件是由Netty内部自动触发的,例如接收到数据、连接建立等。出站事件是由应用程序主动触发的操作,这些事件通常是由应用程序主动触发的操作,例如写入数据到Channel、刷新数据到网络。

        案例中的这段代码,是利用了NioSocketChannel的.writeAndFlush方法,而ChannelHandlerContext也具有.writeAndFlush方法,两者之间有什么区别?

        将案例中的代码进行替换:

 ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("abc".getBytes(StandardCharsets.UTF_8)));

        结果是出站事件一个都没有触发,为什么?

        原因在于它们的触发方式和传播机制:

  • ctx是上下文的实例,会从当前节点向前去找所有的outBound事件并且触发。
  • ch是整个连接通道的实例,会从尾部节点向前去找所有的outBound事件并且触发。

        当前节点是在inBound3位置,它的前面没有任何outBound事件:

        如果此时有一个outBound事件在inBound3之前,使用ctx去进行写入并刷新也是可以触发的:

        将事件4放在事件3的前面:

        在执行完所有inBound事件后回头执行了outBound事件:


        在上面的案例中提到了,ctx是上下文的实例。调用链最初的ctx,可以从客户端接受消息,并且逐层传递给后一个调用链上的handle进行处理。那么它是如何通知后一个调用链进行处理的?

        关键在于super.channelRead(ctx,msg); 它的内部调用了上下文实例的.fireChannelRead方法。这个方法大致的含义是唤醒下一个handle并且将ctx进行传递。

3、ByteBuf

         ByteBuf可以理解成对于NIO中缓冲区ByteBuffer的增强,最大的区别在于:

  • ByteBuffer在创建时指定了最大容量后不可以扩容,而ByteBuf具有扩容机制。
  • ByteBuffer在写入/读取数据前需要进行读写模式切换,指针在读写模式下代表的含义也是不一样的,而ByteBuf内部维护了独立的读,写指针,互不干扰。

        3.1、ByteBuf的初始容量

        我们可以通过下面的api去创建一个ByteBuf:

ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();

        DEFAULT 是ByteBufAllocator接口中的一个成员变量:

        它的来源又是ByteBufUtil中的成员变量DEFAULT_ALLOCATOR

        在静态代码块中,会在类加载时给DEFAULT_ALLOCATOR 赋值,其数据来源会根据是否开启池化进行分派:(这里暂且知道有池化这回事,后续会进行说明)

        假设目前是开启了池化,最终DEFAULT_ALLOCATOR 的值是新创建的PooledByteBufAllocator实例。

        PooledByteBufAllocator 的类关系图:

        最后调用的buffer();方法还是PooledByteBufAllocator 的父类AbstractByteBufAllocator中的

        有三个重载的方法,它们的共同点都是通过directByDefault 的布尔值判断返回何种类型的Buffer(直接内存或堆内存)

        如果我们没有指定大小,走的是第一个无参的方法,初始容量是256,最大容量是integer的最大值:

        3.2、ByteBuf的扩容机制

        ByteBuf的扩容,主要体现在buffer.writeBytes()方法上,在上面的案例中,它的实际类型是

class io.netty.buffer.PooledUnsafeDirectByteBuf

        又调用了另一个writeBytes方法,传递了参数数组以及参数数组的长度。

 

        再次进入会发现内部调用了三个方法:

        研究扩容机制,我们重点关注ensureWritable方法

        3.2.1、ensureWritable(length)

        ensureWritable(length)方法的内部又调用了两个方法(方法的参数minWritableBytes

是将要写入的byte数组的长度):

  • checkPositiveOrZero(minWritableBytes, "minWritableBytes"):这个方法的目的是为了检查将要写入的byte数组长度的合法性:

  • ensureWritable0(int minWritableBytes):该方法是扩容逻辑的体现:

        这段代码的含义是,判断当前byte数组的长度如果小于或等于可写入大小,则无需扩容直接返回:

if (minWritableBytes <= writableBytes()) {
     return;
}

        writableBytes() 方法会用当前bytebuf的容量-写指针的索引得到可写入大小        

        假设没有设置ByteBuf的容量,那么它的默认值此时是256,还没有进行写入,写指针的索引就在0位置。writableBytes()方法返回的是256 - 0 = 256。并且假设需要写入的byte数组的大小是300,那么300 > 256(300比当前的容量大,但是小于最大容量),就不满足该if代码块,会继续向下执行,如果没有进入该if块就说明需要扩容了。


        在这里有必要顺带着说明一下ByteBuf的数据结构:

  • Capacity(容量):是缓冲区中可以存储的最大字节数。
  • Reader Index(读索引):表示当前读操作的位置。
  • Writer Index(写索引):表示当前写操作的位置。
  • Max  Capacity(最大容量):integer的最大值或自己设置。

        可扩容字节 = 最大容量 - 容量。


        然后会将当前写指针的位置(0)赋值给一个局部变量。

final int writerIndex = writerIndex();

        这段代码的含义是检查边界(默认情况下都是要检查的,都会进入该if条件块)

        如果当前需要写入的byte数组的大小比最大容量还要大,就会抛出异常。

     if (checkBounds) {
            if (minWritableBytes > maxCapacity - writerIndex) {
                throw new IndexOutOfBoundsException(String.format(
                        "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                        writerIndex, minWritableBytes, maxCapacity, this));
            }
        }

         这段代码是将写指针的索引+byte数组的长度赋值给一个新的变量(此时的值是300)

int minNewCapacity = writerIndex + minWritableBytes;

        然后会在这段代码中执行扩容的逻辑,调用calculateNewCapacity方法,传入byte数组的长度和最大容量:

int newCapacity = alloc().calculateNewCapacity(minNewCapacity, maxCapacity);

        选择第一个实现:

        同样去进行条件检查:

checkPositiveOrZero(minNewCapacity, "minNewCapacity");
  if (minNewCapacity > maxCapacity) {
       throw new IllegalArgumentException(String.format(
            "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
             minNewCapacity, maxCapacity));
  }

        然后会将一块4MiB大小的区域赋值给变量threshold,如果当前数组容量和它相等,就直接返回。

        final int threshold = CALCULATE_THRESHOLD; // 4 MiB page

        if (minNewCapacity == threshold) {
            return threshold;
        }

        否则进入该if块:

  • 首先用当前数组容量(300)/ threshold * threshold (当前数组容量/threshold的整数倍)赋值给newCapacity。
  • 如果newCapacity + threshold > 最大容量,就将newCapacity设置成最大容量。
  • 否则newCapacity加上一个threshold。
      if (minNewCapacity > threshold) {
            int newCapacity = minNewCapacity / threshold * threshold;
            if (newCapacity > maxCapacity - threshold) {
                newCapacity = maxCapacity;
            } else {
                newCapacity += threshold;
            }
            return newCapacity;
        }

        没有进入上面的if块就说明当前数组的容量要小于threshold,就会执行下面的while循环:

  • 从64开始翻倍,直到newCapacity大于等于当前数组的容量
       // Not over threshold. Double up to 4 MiB, starting from 64.
        int newCapacity = 64;
        while (newCapacity < minNewCapacity) {
            newCapacity <<= 1;
        }

        最后会返回扩容后的数组容量和最大容量的小值。

 return Math.min(newCapacity, maxCapacity);

        总结:对于小的容量需求,使用指数增长方式;对于大的容量需求,采用线性增长方式。

        案例中的300容量,最终会走while (newCapacity < minNewCapacity)的逻辑,也就是最终扩容的结果是从最初的256->512。

        3.3、ByteBuf堆内存和直接内存

        ByteBuf和ByteBuffer一样,都支持创建基于堆的ByteBuf和基于直接内存的ByteBuf 。

  • 基于堆的ByteBuf受JVM控制,由JVM在合适的时机进行垃圾回收。正是因为此,使用堆内存会增加垃圾回收器的负担,尤其是在大量分配和释放内存的场景中,可能会导致 GC 开销较高。
  • 基于直接内存的ByteBuf使用 JVM 堆外内存,需要手动在合适的时机进行回收,Netty 使用引用计数(Reference Counting)机制来管理直接内存的生命周期。

        学过JVM的都知道,垃圾回收有两种机制,可达性分析引用计数。在JVM中实际上默认也是使用可达性分析 的,这样做的目的是为了解决循环引用的问题。为什么在Netty中使用了引用计数 ?

Netty 选择引用计数的原因

  1. 引用计数法在回收垃圾时是实时的,当引用计数归零时,会立刻释放内存,无需等待下一次垃圾回收的触发。
  2. Netty中的内存结构,通常不涉及复杂的对象图和循环引用,引用计数的缺点在这种情况下影响较小。
  3. Netty 结合了内存池化机制,进一步减少了频繁分配和释放内存的开销。

        那什么是内存池化机制,基于直接内存的ByteBuf又为什么通常配合内存池化机制 一起使用?

        池化是一种基于复用的思想,类似于线程池,数据库连接池,我们通常会使用线程池去管理线程,而不是手动地去创建线程,有新任务被分配时,线程池会使用空余的线程去进行处理,处理完毕后则归还线程,避免大量地去创建线程。数据库连接池也是一样的道理。

        如果ByteBuf没有利用内存池化机制 ,则每次都要创建新的实例,对于内存的影响较大。特别是基于直接内存的ByteBuf,直接内存创建和销毁的代价昂贵 ,频繁地创建销毁很影响效率。并且在高并发时,池化功能更节约内存,减少内存溢出的可能。


        在上文中提到,如果使用直接内存的ByteBuf,需要手动地去释放内存。那么释放内存的时机是?

        我们知道,pipeline是一种链式调用的结构,ByteBuf可能在多个handle之间传递。那么我们就应该在最后一个ByteBuf被使用,并且不再向下传递的handle中进行release操作。具体也可细分为以下几种情况:

  • 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release

  • 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release

  • 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release

  • 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release

  • 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)

  • 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release。

        这是尾节点的类,实现了入站处理器的接口:

        其中的onUnhandledInboundMessage方法就是release的逻辑:

        检查msg的类型是否属于ReferenceCounted及其子类。(ByteBuf实现了ReferenceCounted接口)

 

        3.4、Slice&CompositeByteBuf

        Slice和CompositeByteBuf也是ByteBuf中的两个方法,也是零拷贝的体现,下面通过案例的形式简单的介绍一下:

        3.4.1、Slice

        通过ByteBuf的Slice方法,将一个完整的byte数组切分成为了两个不同的部分,但是内存和完整的byte数组是同一个。

        正因为如此,如果提前将完整数组手动release,则切片无法正常工作。

/**
 * 切片
 */
public class SliceDemo1 {
    public static void main(String[] args) {
        ByteBuf origin = ByteBufAllocator.DEFAULT.buffer(10);
        origin.writeBytes(new byte[]{1, 2, 3, 4});
        MyByteBufUtil.log(origin);

        ByteBuf b1 = origin.slice(0, 2);
        ByteBuf b2 = origin.slice(2, 2);
        MyByteBufUtil.log(b1);
        MyByteBufUtil.log(b2);

        //slice 并没有切分内存
        System.out.println("**********************");
        b1.setByte(0,'a');
        MyByteBufUtil.log(origin);
        MyByteBufUtil.log(b1);


    }
}

        可以看到对任何一个切片内容的修改,都会体现在完整的Byte上:

        3.4.2、CompositeByteBuf

        CompositeByteBuf是Slice的反向操作,将多个ByteBuf组合在一起,同样也不会开辟一份新的内存。

public class CompositDemo {
    public static void main(String[] args) {
        ByteBuf b1 = ByteBufAllocator.DEFAULT.buffer();
        b1.writeBytes(new byte[]{'1','2'});


        ByteBuf b2 = ByteBufAllocator.DEFAULT.buffer();
        b2.writeBytes(new byte[]{'3','4'});


        //将多个ByteBuf组合在一起,同样也不会开辟一份新的内存
        //内部维护了一个 Component 数组,每个 Component 管理一个 ByteBuf
        //也就是说目前只存在b1和b2两块内存
        CompositeByteBuf compositeBuffer = ByteBufAllocator.DEFAULT.compositeBuffer();
        compositeBuffer.addComponents(true,b1,b2);
        MyByteBufUtil.log(compositeBuffer);
    }
}

4、双向通信综合案例

        最后我们通过一个客户端,服务器互相发送消息的综合案例来总结一下上面的知识点:

        服务器端,我们可以重点回顾一下这几个问题:

  1.  ch.writeAndFlush 和 ctx.writeAndFlush 有什么区别?
  2. ChannelOutboundHandlerAdapter出站事件的触发时机以及顺序?
  3. ByteBuf手动release的时机?
public class TestServer2 {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup(),new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new StringDecoder(Charset.defaultCharset()));
                        pipeline.addLast(new StringEncoder(Charset.defaultCharset()));
                        pipeline.addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("从客户端读取到的消息:{}",msg);
                                //调用ch,触发出站事件 无视顺序
                                ByteBuf buffer = ctx.alloc().buffer();
                                ch.writeAndFlush(buffer.writeBytes("abc".getBytes(StandardCharsets.UTF_8)))
                                        .addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
                                //byteBuf没有继续向下传递,所以需要release?
                                buffer.release();
                            }
                        });
                        pipeline.addLast(new ChannelOutboundHandlerAdapter(){
                            @Override
                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                                log.debug("向客户端发送消息");
                                super.write(ctx, msg, promise);
                                log.debug("向客户端发送消息完成");
                            }
                        });
                    }
                })
                .bind(8080);
    }
}

        客户端,我们可以重点回顾一下这几个问题:

  1. 建立连接是由主线程完成的吗?
  2. 建立连接后为什么要调用.sync方法,以及有无其他方式达成同样的效果?
  3. 关闭连接是否由主线程完成?如何才能正确地处理关闭连接后的逻辑?
@Slf4j
public class TestClient1 {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        Channel channel = new Bootstrap()
                .group(eventLoopGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        //字符输出编码
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new StringDecoder(Charset.defaultCharset())); // 添加解码器
                        pipeline.addLast(new StringEncoder(Charset.defaultCharset()));
                        pipeline.addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.debug("从服务器端读取到的消息:{}", msg);
                                super.channelRead(ctx, msg);
                            }
                        });
                    }
                })
                .connect(new InetSocketAddress("localhost", 8080))
                .sync()//阻塞方法 连接建立成功后才会执行
                .channel();

        //新开启一个线程向服务端写入
        new Thread(()->{
            Scanner sc = new Scanner(System.in);
            while (true){
                String str = sc.nextLine();
                if (str.equals("q")){
                    //由nioEventLoopGroup线程异步关闭连接
                    channel.close();
                    break;
                }
                channel.writeAndFlush(str);
            }
        },"input").start();

        //主线程调用监听器发布通知,由nioEventLoopGroup在channel.close()后 处理连接关闭后释放资源
        channel.closeFuture().addListener(future -> eventLoopGroup.shutdownGracefully());
    }
}

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

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

相关文章

Rocky Linux 更换CN镜像地址

官方镜像列表&#xff0c;下拉查找 官方镜像列表&#xff1a;https://mirrors.rockylinux.org/mirrormanager/mirrorsCN 开头的站点。 一键更改镜像地址脚本 以下是更改从默认更改到阿里云地址 cat <<EOF>>/RackyLinux_Update_repo.sh #!/bin/bash # -*- codin…

ChatTTS增强版V3【已开源】,长文本修复,中英混读,导入音色,批量SRT、TXT

ChatTTS增强版V3来啦&#xff01;本次更新增加支持导入SRT、导入音色等功能。结合上次大家反馈的问题&#xff0c;修复了长文本、中英混读等问题。 项目已开源(https://github.com/CCmahua/ChatTTS-Enhanced) 项目介绍 V3 ChatTTS增强版V3&#xff0c;长文本修复&#xff0c…

【职场人】职场进化记:我的“不惹人厌邀功精”之路

刚步入职场的我&#xff0c;就像一张白纸&#xff0c;什么都不懂&#xff0c;只知道埋头苦干。但渐渐地&#xff0c;我发现那些经常“冒泡”的同事似乎总能得到更多的关注和机会。我不禁想&#xff1a;“我是否也要成为那样一个‘邀功精’呢&#xff1f;” 不过&#xff0c;我…

Go自定义数据的序列化流程

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

Apple - Launch Services Programming Guide

本文翻译整理自&#xff1a;Launch Services Programming Guide https://developer.apple.com/library/archive/documentation/Carbon/Conceptual/LaunchServicesConcepts/LSCIntro/LSCIntro.html#//apple_ref/doc/uid/TP30000999-CH201-TP1 文章目录 一、导言谁应该阅读此文档…

Oracle基本语法(SQLPlus)

目录&#xff1a; 前言&#xff1a; 准备工作&#xff1a; 登录&#xff1a; 1.打开SQL Plus命令行工具 第一种方式&#xff1a; 第二种方式&#xff1a; 2.以不同用户登录 SYSTEM&#xff08;普通管理员&#xff09;&#xff1a; SYS(超级管理员)&#xff1a; 不显示…

二叉搜索树及其Java实现

二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称BST&#xff09;是一种特殊的二叉树数据结构&#xff0c;它满足以下特性&#xff1a; 有序性&#xff1a;对于树中的任意一个节点&#xff0c;其左子树中所有节点的值都小于该节点的值&#xff0c;而其右子树中所有节…

Web Worker 学习及使用

了解什么是 Web Worker 提供了可以在后台线程中运行 js 的方法。可以不占用主线程&#xff0c;不干扰用户界面&#xff0c;可以用来执行复杂、耗时的任务。 在worker中运行的是另一个全局上下文&#xff0c;不能直接获取 Window 全局对象。不同的 worker 可以分为专用和共享&…

FreeCAD中事务机制实现原理分析

1.基本实现思路 实现一个文件的撤销重做最简单的思想就是&#xff0c;在每个撤销重做节点处保存一份文件的内容&#xff0c;撤销重做时&#xff0c;分别替换对应节点处的文件内容即可。这种做法开销太大&#xff0c;每个节点处都需要保存一份完整的文档内容&#xff0c;每次撤…

fastapi+vue3+primeflex前后端分离开发项目第一个程序

安装axios axios是用来请求后端接口的。 https://www.axios-http.cn/docs/intro pnpm 是一个前端的包管理工具&#xff0c;当我们需要给前端项目添加新的依赖的时候&#xff0c;就可以使用pnpm install 命令进行安装。 pnpm install axios安装 primeflex primeflex是一个cs…

十大经典排序算法——插入排序与希尔排序(超详解)

一、插入排序 1.基本思想 直接插入排序是一种简单的插入排序法&#xff0c;基本思想是&#xff1a;把待排序的记录按其数值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完为止&#xff0c;得到一个新的有序序列。 2.直接插入排序 当插入第 e…

(八)ReactHooks使用规则

ReactHooks使用规则 只能在组件中或者其他自定义Hook函数中使用只能在组件的顶层调用&#xff0c;不能嵌套在if、for、其他函数中

模拟原神圣遗物系统-小森设计项目,设计圣遗物词条基类

项目分析 首先需要理解圣遗物的方方面面 比如说圣遗物主词条部分和副词条部分都有那些特点 稍等一会&#xff1a;原神&#xff0c;启动&#xff01; 在此说明了什么&#xff1f; 这是完全体 &#xff1a;主副 词条都有 如果 升级直接暴击率 那么就留点 或者是另外的元素充能 …

如何自制一个Spring Boot Starter并推送到远端公服

在现代Java开发中&#xff0c;Spring Boot无疑是一个强大且便捷的框架&#xff0c;它通过提供大量的Starter来简化依赖管理和项目配置。有时&#xff0c;我们可能需要为特定功能或团队定制Starter。本文将指导你如何创建自己的Spring Boot Starter并将其推送到远程公共服务器上…

[SAP ABAP] 运算符与操作符

1.算数运算符 算术运算符描述加法-减法*乘法/除法MOD取余 示例1 输出结果: 输出结果: 2.比较运算符 比较运算符描述示例 等于 A B A EQ B <> 不等于 A <> B A NE B >大于 A > B A GT B <小于 A < B A LT B >大于或等于 A > B A GE B <小…

33 - 连续出现的数字(高频 SQL 50 题基础版)

33 - 连续出现的数字 -- 开窗函数lead(col,n) 统计窗口内往下第n行值 -- over(partition by xxx) 按照xxx所有行进行分组 -- over(partition by xxx order by aaa) 按照xxx分组&#xff0c;按照aaa排序select distinct num as ConsecutiveNums from(select num,# 从当前记录获…

Mac M3 Pro 安装 Zookeeper-3.4.6

1、下载安装包 官方下载地址&#xff1a;https://archive.apache.org/dist/zookeeper/ 网盘下载地址&#xff1a;https://pan.baidu.com/s/1j6iy5bZkrY-GKGItenRB2w?pwdirrx 提取码: irrx 2、解压并添加环境变量 # 将安装包移动到目标目录 mv ~/Download/zookeeper-3.4.6.…

回归预测 | Matlab实现NGO-HKELM北方苍鹰算法优化混合核极限学习机多变量回归预测

回归预测 | Matlab实现NGO-HKELM北方苍鹰算法优化混合核极限学习机多变量回归预测 目录 回归预测 | Matlab实现NGO-HKELM北方苍鹰算法优化混合核极限学习机多变量回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现NGO-HKELM北方苍鹰算法优化混合核极限…

elementui表格el-table最右侧操作列展示不完全

解决方法 .el-table__fixed,.el-table__fixed-right{height:100% !important;}

C++继承与多态—多重继承的那些坑该怎么填

课程总目录 文章目录 一、虚基类和虚继承二、菱形继承的问题 一、虚基类和虚继承 虚基类&#xff1a;被虚继承的类&#xff0c;就称为虚基类 virtual作用&#xff1a; virtual修饰成员方法是虚函数可以修饰继承方式&#xff0c;是虚继承&#xff0c;被虚继承的类就称为虚基类…