一万字的Netty的知识点总结

Netty的知识点



前言

为了让大家对Netty有个整体认知,本文首先会对Netty的整个运作过程捋一遍,细节的东西后面再说,直接上图:
在这里插入图片描述

结合图示进行全过程讲解:

  1. ServerBootStrap作为Netty的服务端入口,会对BossGroup和WorkGroup进行相关初始化操作,在BossGroup中,主要是对客户端的新连接请求进行处理(即OP_ACCEPT事件,但其实OP_ACCEPT事件的具体处理也会涉及到读写事件,因为数据不是读就是写),在WorkGroup中,则负责处理IO读写、编解码、业务逻辑等(即OP_READ事件、OP_WRITE事件)。服务端启动的时候会绑定一个端口,作为后续客户端连接入口,绑定端口的时候会在BossGroup(由NioEventLoopGroup类创建的对象)的其中一个NioEventLoop的Selector(多路复用器)上注册一条NioServerSocketChannel通道,后面的连接处理就是在通道中进行的。

  2. BootStrap则作为Netty的客户端入口,会对ClientGroup进行相关初始化操作,在ClientGroup中,第一就是创建与服务端的连接(即OP_CONNECT事件),第二就是进行IO读写、编解码、业务逻辑等操作(即OP_READ事件、OP_WRITE事件)。

  3. 服务端和客户端启动之后,当服务端收到客户端发来的连接请求,由于属于OP_ACCEPT事件,在BossGroup中处理。BossGroup(由NioEventLoopGroup类创建的对象)管理着若干个NioEventLoop,每个NioEventLoop持有一个线程(就好比线程池中的一组线程并发处理若干个连接请求),每个NioEventLoop上会创建一个Selector,一个Selector上可以注册多个通道(所以叫多路复用器),且它会以不断轮询的方式同时监听每个通道上是否有IO事件发生,每个通道里都会有个ChannelPipeline管道,管道里全是Handler,包括管道头Head和管道尾Tail,以及进行IO读写、编解码,业务处理的若干个Handler,Handler也可以自定义,把需要的Handler注册进管道就可以执行了。当请求到达Head时,代表“请求数据”已准备好,OP_ACCEPT事件已就绪,Selector监听到事件已就绪,就会让持有的线程对事件进行处理,处理过程是在Handler中进行。首先会创建一个NioSocketChannel实例,然后交给ServerBootStrapAcceptor这个Handler,它是Netty底层代码注册的,Acceptor具体操作就是向WorkGroup中的某个Selector注册刚才创建好的NioSocketChannel,自此客户端连接请求处理结束。

  4. 客户端发出连接请求的同时会自己创建一条NioSocketChannel通道与服务端NioSocketChannel进行互通,连接完之后就是WorkGroup的事了,不需要BossGroup管了,一个客户端连接对应一条服务端NioSocketChannel。比如现在客户端要进行一个远程方法的调用,将方法参数传给服务端后,服务端处理完将结果返回给客户端。首先请求从客户端通道传输到WorkGroup中的对应通道,然后Head会申请一块堆外内存来缓冲请求内容,缓冲完之后,代表数据已准备好,OP_READ事件已就绪,selector监听到就绪事件之后,让持有的线程对事件进行处理,这里我定义了Decode解码,Compute方法调用处理和Encode编码三个Handler进行操作,其中Inbound入站Handler包括Decode和Compute(从Head到Tail就是入站),Outbound出站Handler包括Encode(从Tail到Head就是出站),每一个Handler被注册到Pipeline中的时候都会创建一个与之对应的ChannelHandlerContext,它包含着Handler的上下文信息,主要负责管理和其他在同一管道里的Handler之间的交互,它有一个前指针和后指针,可以与其他ChannelHandlerContext关联,这样Handler处理就变得更加灵活,比如这次请求需要三个Handler,而下次请求只涉及到Decode和Encode,那下次就可以执行完Decode然后指针直接指向Encode,next指针具体指向谁是依靠ChannelHandlerContext中的数据类型与其他Handler类型进行匹配得出的。在处理完读事件之后,接着处理Handler中涉及到的写事件,将处理结果写到ByteBuf中,回到Head,执行flush操作将ByteBuf内容写到SocketBuffer中,然后再到网卡buffer,通过互联网把结果传回给客户端,客户端拿到结果之后同样要进行解码,反序列化等操作,那么回过头发现客户端在发送调用请求之前在Pipeline中也进行了Encode处理的。(Head的主要作用:从SocketBuffer读请求内容到ByteBuf,从ByteBuf写返回结果到SocketBuffer)

  5. 假设又有另外一个客户端连接了服务端,且和之前那个NioSocketChannel注册到了同一个Selector上,当线程正在处理另一个通道上的事件的时候,这时该客户端也发起了一个处理请求,请求到达服务端通道之后会被Head读到堆外内存中缓冲着,此时OP_READ事件已就绪,Selector监听到了就绪事件,但由于线程正在处理另外一个通道上的事件,所以就要等当前通道的事件处理完,下一轮循环监听再处理了(这也是堆外内存的作用体现之一,数据可以先在缓冲区放着)。当两个通道被注册在不同的Selector上的时候就互不影响了,因为是在不同的线程中并行处理的。另外补充两点,第一个TaskQueue任务队列中的任务都是非IO任务,从性能上来考虑,千万不要将一个需要长时间来运行的任务放入到任务队列中,因为事件任务在一个线程中是串行执行的,这样会阻塞其他任务。解决方案是使用一个专门的EventExecutor来执行它(ChannelPipeline提供了带有EventExecutorGroup参数的addXXX()方法,该方法可以将传入的ChannelHandler绑定到你传入的EventExecutor之中),这样它就会在另一条线程中执行,与其他任务隔离。第二个Channel注册到Selector后返回的是一个SelectionKey,这个SelectionKey有以下几个重要属性:

    • interest set,通道感兴趣的事件集,就是会把该通道可能执行的事件类型都告诉Selector
    • ready set,感兴趣的事件集中的“就绪事件集”
    • 保存着的Channel
    • 保存着的Selector

IO事件类型:

  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_CONNECT

因此Selector每次循环监听的其实就是SelectionKey中的就绪事件集,看是否存在已就绪的事件,存在就进行处理。SelectionKey相当于是Selector和Channel之间的桥梁。

回到顶部


一、NIO 基本概念

1. 阻塞(Block)与非阻塞(Non-Block)

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候。

阻塞: 往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。

非阻塞: 当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。

阻塞 IO :

在这里插入图片描述

非阻塞 IO :

在这里插入图片描述

2. 同步(Synchronous)与异步(Asynchronous)

同步和异步都是基于应用程序和操作系统处理 IO 事件所采用的方式。比如

同步: 是应用程序要直接参与 IO 读写的操作。

异步: 所有的 IO 读写交给操作系统去处理,应用程序只需要等待通知。

同步方式在处理 IO 事件的时候,必须阻塞在某个方法上面等待我们的 IO 事件完成(阻塞 IO 事件或者通过轮询 IO事件的方式),对于异步来说,所有的 IO 读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的 IO 操作,当操作完成 IO 后,会给我们的应用程序一个通知。

所以异步相比较于同步带来的直接好处就是在我们处理IO数据的时候,异步的方式我们可以把这部分等待所消耗的资源用于处理其他事务,提升我们服务自身的性能。

同步 IO :
在这里插入图片描述

异步 IO :

在这里插入图片描述

回到顶部


二、Java BIO与NIO对比

1. BIO(传统IO)

BIO是一个同步并阻塞的IO模式,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如File抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。

2. NIO(Non-blocking/New I/O)

NIO 是一种同步非阻塞的 I/O 模型,于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , SelectorBuffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

3. BIO与NIO的对比

IO模型 BIO NIO
通信 面向流 面向缓冲
处理 阻塞IO 非阻塞IO
触发 选择器

4. NIO 的 Server 通信的简单模型

在这里插入图片描述

5. BIO 的 Server 通信的简单模型

在这里插入图片描述

6. NIO的特点

  1. 一个线程可以处理多个通道,减少线程创建数量;
  2. 读写非阻塞,节约资源:没有可读/可写数据时,不会发生阻塞导致线程资源的浪费

回到顶部


三、Reactor 模型

Reactor对应的叫法:1.反应器模式 2.分发者模式 3.通知者模式

  1. 基于I/O复用模型: 多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
  2. 基于线程池复用线程资源: 不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。

Reactor模式中核心组成:

  • Reactor: Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
  • Handlers: 处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。

1. 单线程的 Reactor 模型

在这里插入图片描述

  1. Select是前面I/O复用模型介绍额标准网络编程API,可以实现应用程序通过一个阻塞对象监听多路连接请求
  2. Reactor对象通过Select监控客户端请求事件,受到事件后通过Dispatch进行分发
  3. 如果是建立连接请求事件,则由Acceptor通过Accept处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理。
  4. 如果不是简介事件,则Reactor会分发调用连接对应的Handler来响应
  5. Handler会完成Read-》业务处理-》Send的完整业务流程。

优缺点:

  • 优点:模型简单,没有多线程、进程通信、竞争问题,全部都在一个线程中完成。
  • 缺点:性能问题,只有一个线程,无法发挥多核CPU性能。Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。
  • 缺点:可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接受和处理外部消息,造成节点故障。

2. 多线程的 Reactor 模型

在这里插入图片描述
handler只负责响应事件,不做具体的业务处理,会分发给后面的worker线程池的某个线程处理。

优缺点:

  • 优点:可以充分利用多核CPU性能
  • 缺点:多线程数据共享和访问比较复杂,reactor处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现瓶颈。

3. 多线程主从 Reactor 模型

在这里插入图片描述

  1. Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件
  2. 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor
  3. subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理
  4. 当有新事件发生时,subreactor 就会调用对应的 handler 处理
  5. handler 通过 read 读取数据,分发给后面的 worker 线程处理
  6. worker 线程池分配独立的 worker 线程进行业务处理,并返回结果
  7. handler 收到响应的结果后,再通过 send 将结果返回给 client
  8. Reactor 主线程可以对应多个 Reactor 子线程,即 MainRecator 可以关联多个 SubReactor

回到顶部


四、Netty 基础概念

在这里插入图片描述

  1. Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写
  2. BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
  3. NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop
  4. NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯
  5. NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop
  6. 每个 BossNioEventLoop 循环执行的步骤有 3 步
    轮询 accept 事件
    处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 worker NIOEventLoop 上的 Selector
    处理任务队列的任务,即 runAllTasks
  7. 每个 Worker NIOEventLoop 循环执行的步骤
    轮询 read,write 事件
    处理 I/O 事件,即 read,write 事件,在对应 NioScocketChannel 处理
    处理任务队列的任务,即 runAllTasks
  8. 每个 Worker NIOEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器。

1. Netty 简介

Netty 是一个 NIO 客户端服务器框架,可快速轻松地开发网络应用程序,例如协议服务器和客户端。它极大地简化和简化了网络编程,例如 TCP 和 UDP 套接字服务器。

“快速简便”并不意味着最终的应用程序将遭受可维护性或性能问题的困扰。Netty 经过精心设计,结合了许多协议(例如FTP,SMTP,HTTP 以及各种基于二进制和文本的旧式协议)的实施经验。结果,Netty 成功地找到了一种无需妥协即可轻松实现开发,性能,稳定性和灵活性的方法。

在这里插入图片描述

2. Netty 执行流程

在这里插入图片描述

3. Netty 核心组件

Channel

​Channel是 Java NIO 的一个基本构造。可以看作是传入或传出数据的载体。因此,它可以被打开或关闭,连接或者断开连接。

EventLoop 与 EventLoopGroup

​EventLoop 定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件,在内部,将会为每个Channel分配一个EventLoop。

​EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。

​Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件。

​一个 Channel 一旦与一个 EventLoop 相绑定,那么在 Channel 的整个生命周期内是不能改变的。一个 EventLoop 可以与多个 Channel 绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。

ServerBootstrap 与 Bootstrap

Bootstarp 和 ServerBootstrap 被称为引导类,指对应用程序进行配置,并使他运行起来的过程。Netty处理引导的方式是使你的应用程序和网络层相隔离。

​Bootstrap 是客户端的引导类,Bootstrap 在调用 bind()(连接UDP)和 connect()(连接TCP)方法时,会新创建一个 Channel,仅创建一个单独的、没有父 Channel 的 Channel 来实现所有的网络交换。

​ServerBootstrap 是服务端的引导类,ServerBootstarp 在调用 bind() 方法时会创建一个 ServerChannel 来接受来自客户端的连接,并且该 ServerChannel 管理了多个子 Channel 用于同客户端之间的通信。

ChannelHandler 与 ChannelPipeline

​ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。

ChannelFuture

​Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个ChannelFuture 对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的addListener() 方法为该异步操作添加监 NIO 网络编程框架 Netty 听器,为其注册回调:当结果出来后马上调用执行。

​Netty 的异步编程模型都是建立在 Future 与回调概念之上的。

回到顶部


五、Netty 源码阅读

源码阅读,最好可以再 Debug 的情况下进行,这样更容易帮助理解,因此在分析 Netty 前的我准备一个客户端和服务端的代码。

1. Netty - Server 代码

public class NettyServer {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        EventLoopGroup parentGroup = new NioEventLoopGroup();
        EventLoopGroup childGroup = new NioEventLoopGroup();
        try {
    
    

            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(parentGroup, childGroup)
                     .channel(NioServerSocketChannel.class)
                     .childHandler(new ChannelInitializer<SocketChannel>() {
    
    

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
    
    

                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new StringEncoder());
                            pipeline.addLast(new SomeSocketServerHandler());
                         }
                    });

            ChannelFuture future = bootstrap.bind(8888).sync();
            System.out.println("服务器已启动。。。");

            future.channel().closeFuture().sync();
        } finally {
    
    
            parentGroup.shutdownGracefully();
            childGroup.shutdownGracefully();
        }
    }
}

2. Server 端 Handler

public class DemoSocketServerHandler
                       extends ChannelInboundHandlerAdapter {
    
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
    
    
        System.out.println("Client Address ====== " + ctx.channel().remoteAddress());
        ctx.channel().writeAndFlush("from server:" + UUID.randomUUID());
        ctx.fireChannelActive();
        TimeUnit.MILLISECONDS.sleep(500);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause) throws Exception {
    
    
        cause.printStackTrace();
        ctx.close();
    }
}

3. Netty - Client 代码

public class NettyClient {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
    
    
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
    
    
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
    
    
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new DemoSocketClientHandler());
                        }
                    });

            ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
            future.channel().closeFuture().sync();
        } finally {
    
    
            if(eventLoopGroup != null) {
    
    
                eventLoopGroup.shutdownGracefully();
            }
        }
    }
}

4. Client 端 Handler

public class DemoSocketClientHandler
               extends ChannelInboundHandlerAdapter {
    
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
    
    
        System.out.println(msg);
        ctx.channel().writeAndFlush("from client: " + System.currentTimeMillis());
        TimeUnit.MILLISECONDS.sleep(5000);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx)
            throws Exception {
    
    
        ctx.channel().writeAndFlush("from client:begin talking");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause) throws Exception {
    
    
        cause.printStackTrace();
        ctx.close();
    }
}

5. NioEventLoopGroup 初始化分析

首先根据 Server 服务端代码,分析 NioEventLoopGroup 的初始化过程。而在分析 NioEventLoopGroup 之前,有必要简单的说一说 NioEventLoopGroup 与 NioEventLoop ,方便后续源码的理解。

NioEventLoop 的继承体系

在这里插入图片描述
从 NioEventLoop 的继承体系中可以看到,NioEventLoop 本身就是一个 Executor,并且还是一个 单线程的 Executor。Executor 必然拥有一个 execute(Runnable command) 的实现方法,而 NioEventLoop 的 execute() 实现方法在其父类 SingleThreadEventExecutor 中,找到具体代码:

    public void execute(Runnable task) {
    
    
        if (task == null) {
    
    
            throw new NullPointerException("task");
        }

        boolean inEventLoop = inEventLoop();
        addTask(task);
        if (!inEventLoop) {
    
    
            startThread();
            if (isShutdown()) {
    
    
                boolean reject = false;
                try {
    
    
                    if (removeTask(task)) {
    
    
                        reject = true;
                    }
                } catch (UnsupportedOperationException e) {
    
    
                    // The task queue does not support removal so the best thing we can do is to just move on and
                    // hope we will be able to pick-up the task before its completely terminated.
                    // In worst case we will log on termination.
                }
                if (reject) {
    
    
                    reject();
                }
            }
        }
        if (!addTaskWakesUp && wakesUpForTask(task)) {
    
    
            wakeup(inEventLoop);
        }
    }

这里不细说,但是贴出这段代码主要为了引出 startThread(); 这句代码,在跟这句代码会发现,它最终调用了 NioEventLoop 的一个成员 Executor 执行了当前成员的 execute() 方法。对应的成员 io.netty.util.concurrent.SingleThreadEventExecutor#executor

在这里插入图片描述
而 executor 成员的初始化也是在当前代码执行时创建的匿名 Executor ,也就是执行到即新建并且执行当前 匿名 executor() 方法。

总结

  1. NioEventLoop 本身就是一个 Executor。
  2. NioEventLoop 内部封装这一个新的线程 Executor 成员。
  3. NioEventLoop 有两个 execute 方法,除了本身的 execute() 方法对应的还有成员属性 Executor 对应的execute() 方法。

备注: 因为这里出现了四个 Executor,为了区分,我们给其新的名称:

NioEventLoop 本身 ExecutorNioEventLoop
NioEventLoop 的成员 Executor:子 Executor
NioEventLoopGroup 本身 ExecutorNioEventLoopGroup
NioEventLoopGroup 的构造参数 Executor :总Executor

6. NioEventLoopGroup 的继承体系

在这里插入图片描述
看到继承体系可以直接知道 NioEventLoopGroup 也是一个 Executor,并且是一个线程池的 Executor,所以他也有 execute() 方法。对应的实现再其父类之中:io.netty.util.concurrent.AbstractEventExecutorGroup#execute

在这里插入图片描述
而这里还需要说到的一点是:在 NioEventLoopGroup 的构造中,再其父类 MultithreadEventExecutorGroup 的构造再次引入了一个新的 Executor,

在这里插入图片描述
之所以这里提到这个 Executor,是因为这个 Executor 是对应的 execute() 就是在 NioEventLoop 中的成员 Executor 的 execute() 执行时调用的。也就是下面对应的代码调用。io.netty.util.internal.ThreadExecutorMap#apply(java.util.concurrent.Executor, io.netty.util.concurrent.EventExecutor)

在这里插入图片描述
到这如果不明白,没关系,因为只是为了引入 NioEventLoopGroup 和 NioEventLoop 的对应的两个 Executor,和两个 Executor 对应的两个 execute() 方法。这个后面还会有详细分析。

总结

  1. NioEventLoopGroup 是一个线程池线程 Executor。
  2. NioEventLoopGroup 也封装了一个线程 Executor。
  3. NioEventLoopGroup 也有两个 execute()方法。

未完待续。。。

回到顶部


参考文章:

https://blog.csdn.net/qq_35190492/article/details/113174359

回到顶部


猜你喜欢

转载自blog.csdn.net/weixin_47410172/article/details/127733682