[年薪60W的技巧]工作了5年,你真的理解Netty以及为什么要用吗?(深度干货)

[年薪60W的技巧]工作了5年,你真的理解Netty以及为什么要用吗?(深度干货)

来看下面这个图,当客户端发起一次Http请求时,服务端的处理流程时怎么样的?

image-20211109084258499

简单来说可以分为以下几个步骤:

  1. 基于TCP协议建立网络通信。
  2. 开始向服务端端传输数据。
  3. 服务端接受到数据进行解析,开始处理本次请求逻辑。
  4. 服务端处理完成后返回结果给客户端。

在这个过程中,会涉及到网络IO通信,在传统的BIO模式下,客户端向服务端发起一个数据读取请求,客户端在收到服务端返回数据之前,一直处于阻塞状态,直到服务端返回数据后完成本次会话。这个过程就叫同步阻塞IO,在BIO模型中如果想实现异步操作,就只能使用多线程模型,也就是一个请求对应一个线程,这样就能够避免服务端的链接被一个客户端占用导致连接数无法提高。

同步阻塞IO主要体现在两个阻塞点

  • 服务端接收客户端连接时的阻塞。
  • 客户端和服务端的IO通信时,数据未就绪的情况下的阻塞。

image-20210811170350557

在这种传统BIO模式下,会造成一个非常严重的问题,如下图所示,如果同一时刻有N个客户端发起请求,按照BIO模型的特点,服务端在同一时刻只能处理一个请求。将导致客户端请求需要排队处理,带来的影响是,用户在等待一次请求处理返回的时间非常长。意味着服务端没有并发处理能力,这显然不合适。

image-20211109084710182

那么,服务端应该如何优化呢?

非阻塞IO

从前面的分析发现,服务端在处理一次请求时,会处于阻塞状态无法处理后续请求,那是否能够让被阻塞的地方优化成不阻塞呢?于是就有了非阻塞IO(NIO)

非阻塞IO,就是客户端向服务端发起请求时,如果服务端的数据未就绪的情况下, 客户端请求不会被阻塞,而是直接返回。但是有可能服务端的数据还未准备好的时候,客户端收到的返回是一个空的, 那客户端怎么拿到最终的数据呢?

如图所示,客户端只能通过轮询的方式来获得请求结果。NIO相比BIO来说,少了阻塞的过程在性能和连接数上都会有明显提高。

image-20210708165359843

NIO仍然有一个弊端,就是轮询过程中会有很多空轮询,而这个轮询会存在大量的系统调用(发起内核指令从网卡缓冲区中加载数据,用户空间到内核空间的切换),随着连接数量的增加,会导致性能问题。

多路复用机制

I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作

什么是fd:在linux中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个fd(文件描述符)。而对于一个socket的读写也会有相应的文件描述符,成为socketfd。

常见的IO多路复用方式有【select、poll、epoll】,都是Linux API提供的IO复用方式,那么接下来重点讲一下select、和epoll这两个模型

  • select:进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点

    • 由于他能够同时监听多个文件描述符,假如说有1000个,这个时候如果其中一个fd 处于就绪状态了,那么当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大。
    • 同时,select在单个进程中能打开的fd是有限制的,默认是1024,对于那些需要支持单机上万的TCP连接来说确实有点少
  • epoll:linux还提供了epoll的系统调用,epoll是基于事件驱动方式来代替顺序扫描,因此性能相对来说更高,主要原理是,当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那么当前进程只需要去从指定的fd上读取数据即可,另外,epoll所能支持的fd上线是操作系统的最大文件句柄,这个数字要远远大于1024

【由于epoll能够通过事件告知应用进程哪个fd是可读的,所以我们也称这种IO为异步非阻塞IO,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行】

I/O多路复用的好处是可以通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线程,降低了系统的资源开销,它的整体实现思想如图2-3所示。

客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻塞,服务端会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可,也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行相关的处理即可。

image-20210708203509498

异步IO

异步IO和多路复用机制,最大的区别在于:当数据就绪后,客户端不需要发送内核指令从内核空间读取数据,而是系统会异步把这个数据直接拷贝到用户空间,应用程序只需要直接使用该数据即可。

image-20210811172034569

图2-4 异步IO

在Java中,我们可以使用NIO的api来完成多路复用机制,实现伪异步IO。在网络通信演进模型分析这篇文章中演示了Java API实现多路复用机制的代码,发现代码不仅仅繁琐,而且使用起来很麻烦。

所以Netty出现了,Netty的I/O模型是基于非阻塞IO实现的,底层依赖的是JDK NIO框架的多路复用器Selector来实现。

一个多路复用器Selector可以同时轮询多个Channel,采用epoll模式后,只需要一个线程负责Selector的轮询,就可以接入成千上万个客户端连接。

Reactor模型

http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf

了解了NIO多路复用后,就有必要再和大家说一下Reactor多路复用高性能I/O设计模式,Reactor本质上就是基于NIO多路复用机制提出的一个高性能IO设计模式,它的核心思想是把响应IO事件和业务处理进行分离,通过一个或者多个线程来处理IO事件,然后将就绪得到事件分发到业务处理handlers线程去异步非阻塞处理,如图2-5所示。

Reactor模型有三个重要的组件:

  • Reactor :将I/O事件发派给对应的Handler
  • Acceptor :处理客户端连接请求
  • Handlers :执行非阻塞读/写

image-20210708212057895

图2-5 Reactor模型

这是最基本的单Reactor单线程模型(整体的I/O操作是由同一个线程完成的)

其中Reactor线程,负责多路分离套接字,有新连接到来触发connect 事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。

Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于racotor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,有Reactor分发)

Reactor 模式本质上指的是使用 I/O 多路复用(I/O multiplexing) + 非阻塞 I/O(non-blocking I/O) 的模式。

多线程单Reactor模型

单线程Reactor这种实现方式有存在着缺点,从实例代码中可以看出,handler的执行是串行的,如果其中一个handler处理线程阻塞将导致其他的业务处理阻塞。由于handler和reactor在同一个线程中的执行,这也将导致新的无法接收新的请求,我们做一个小实验:

  • 在上述Reactor代码的DispatchHandler的run方法中,增加一个Thread.sleep()。
  • 打开多个客户端窗口连接到Reactor Server端,其中一个窗口发送一个信息后被阻塞,另外一个窗口再发信息时由于前面的请求阻塞导致后续请求无法被处理。

为了解决这种问题,有人提出使用多线程的方式来处理业务,也就是在业务处理的地方加入线程池异步处理,将reactor和handler在不同的线程来执行,如图4-7所示。

image-20210709154534593

图2-6

多线程多Reactor模型

在多线程单Reactor模型中,我们发现所有的I/O操作是由一个Reactor来完成,而Reactor运行在单个线程中,它需要处理包括Accept()/read()/write/connect操作,对于小容量的场景,影响不大。但是对于高负载、大并发或大数据量的应用场景时,容易成为瓶颈,主要原因如下:

  • 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的读取和发送;
  • 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;

所以,我们还可以更进一步优化,引入多Reactor多线程模式,如图2-7所示,Main Reactor负责接收客户端的连接请求,然后把接收到的请求传递给SubReactor(其中subReactor可以有多个),具体的业务IO处理由SubReactor完成。

Multiple Reactors 模式通常也可以等同于 Master-Workers 模式,比如 Nginx 和 Memcached 等就是采用这种多线程模型,虽然不同的项目实现细节略有区别,但总体来说模式是一致的。

image-20210709162516832

图2-7

  • Acceptor,请求接收者,在实践时其职责类似服务器,并不真正负责连接请求的建立,而只将其请求委托 Main Reactor 线程池来实现,起到一个转发的作用。
  • Main Reactor,主 Reactor 线程组,主要负责连接事件,并将IO读写请求转发到 SubReactor 线程池
  • Sub Reactor,Main Reactor 通常监听客户端连接后会将通道的读写转发到 Sub Reactor 线程池中一个线程(负载均衡),负责数据的读写。在 NIO 中 通常注册通道的读(OP_READ)、写事件(OP_WRITE)。

高性能通信框架之Netty

在Java中,网络编程框架有很多,比如Java NIO、Mina、Netty、Grizzy等。但是在大家接触到的所有中间件中,绝大部分都是采用Netty。

原因是Netty是目前最流行的一款高性能Java网络编程框架,它被广泛引用在中间件、直播、社交、游戏等领域。谈及到开源中间件,大家熟知的Dubbo、RocketMQ、Elasticsearch、Hbase、RocketMQ等都是采用Netty实现。

在实际开发中,今天来听课的同学,99%的人都不会涉及到使用Netty做网络编程开发,但是为什么还要花精力给大家讲呢?原因有几个

  • 在很多大厂面试的时候,会涉及到相关的知识点
    • Netty高性能表现在哪些方面
    • Netty中有哪些重要组件
    • Netty的内存池、对象池的设计
  • 很多中间件都是用netty来做网络通信,那么我们在分析这些中间件的源码时,降低网络通信的理解难度
  • 提升Java知识体系,尽可能的实现对技术体系理解的全面性。

为什么选择Netty

Netty其实就是一个高性能NIO框架,所以它是基于NIO基础上的封装,本质上是提供高性能网络IO通信的功能。由于前面的课程中我们已经详细的对网络通信做了分析,因此在学习Netty时,学习起来应该是更轻松的。

Netty提供了上述三种Reactor模型的支持,我们可以通过Netty封装好的API来快速完成不同Reactor模型的开发,这也是为什么大家都选择Netty的原因之一,除此之外,Netty相比于NIO原生API,它有以下特点:

  • 提供了高效的I/O模型、线程模型和时间处理机制
  • 提供了非常简单易用的API,相比NIO来说,针对基础的Channel、Selector、Sockets、Buffers等api提供了更高层次的封装,屏蔽了NIO的复杂性
  • 对数据协议和序列化提供了很好的支持
  • 稳定性,Netty修复了JDK NIO较多的问题,比如select空转导致的cpu消耗100%、TCP断线重连、keep-alive检测等问题。
  • 可扩展性在同类型的框架中都是做的非常好的,比如一个是可定制化的线程模型,用户可以在启动参数中选择Reactor模型、 可扩展的事件驱动模型,将业务和框架的关注点分离。
  • 性能层面的优化,作为网络通信框架,需要处理大量的网络请求,必然就面临网络对象需要创建和销毁的问题,这种对JVM的GC来说不是很友好,为了降低JVM垃圾回收的压力,引入了两种优化机制
    • 对象池复用,
    • 零拷贝技术

Netty的生态介绍

首先,我们需要去了解Netty到底提供了哪些功能,如图2-1所示,表示Netty生态中提供的功能说明。后续内容中会逐步的分析这些功能。

image-20210811151520387

图2-1 Netty功能生态

Netty的基本使用

需要说明一下,我们讲解的Netty版本是4.x版本,之前有一段时间netty发布了一个5.x版本,但是被官方舍弃了,原因是:使用ForkJoinPool增加了复杂性,并且没有显示出明显的性能优势。同时保持所有的分支同步是相当多的工作,没有必要。

添加jar包依赖

使用4.1.66版本

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
</dependency>

创建Netty Server服务

大部分场景中,我们使用的主从多线程Reactor模型,Boss线程是住Reactor,Worker是从Reactor。他们分别使用不同的NioEventLoopGroup

主Reactor负责处理Accept,然后把Channel注册到从Reactor,从Reactor主要负责Channel生命周期内的所有I/O事件。

public class NettyBasicServerExample {

    public void bind(int port){
        // 我们要创建两个EventLoopGroup,
        // 一个是boss专门用来接收连接,可以理解为处理accept事件,
        // 另一个是worker,可以关注除了accept之外的其它事件,处理子任务。
        //上面注意,boss线程一般设置一个线程,设置多个也只会用到一个,而且多个目前没有应用场景,
        // worker线程通常要根据服务器调优,如果不写默认就是cpu的两倍。
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        EventLoopGroup workerGroup=new NioEventLoopGroup();
        try {
            //服务端要启动,需要创建ServerBootStrap,
            // 在这里面netty把nio的模板式的代码都给封装好了
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup) //配置boss和worker线程
                //配置Server的通道,相当于NIO中的ServerSocketChannel
                .channel(NioServerSocketChannel.class)
                //childHandler表示给worker那些线程配置了一个处理器,
                // 配置初始化channel,也就是给worker线程配置对应的handler,当收到客户端的请求时,分配给指定的handler处理
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new NormalMessageHandler()); //添加handler,也就是具体的IO事件处理器
                    }
                });
            //由于默认情况下是NIO异步非阻塞,所以绑定端口后,通过sync()方法阻塞直到连接建立
            //绑定端口并同步等待客户端连接(sync方法会阻塞,直到整个启动过程完成)
            ChannelFuture channelFuture=bootstrap.bind(port).sync();
            System.out.println("Netty Server Started,Listening on :"+port);
            //等待服务端监听端口关闭
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放线程资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new NettyBasicServerExample().bind(8080);
    }
}

上述代码说明如下:

  • EventLoopGroup,定义线程组,相当于我们之前在写NIO代码时定义的线程。这里定义了两个线程组分别是boss线程和worker线程,boss线程负责接收连接,worker线程负责处理IO事件。boss线程一般设置一个线程,设置多个也只会用到一个,而且多个目前没有应用场景。而worker线程通常要根据服务器调优,如果不写默认就是cpu的两倍。
  • ServerBootstrap,服务端要启动,需要创建ServerBootStrap,在这里面netty把nio的模板式的代码都给封装好了。
  • ChannelOption.SO_BACKLOG

设置Channel类型

NIO模型是Netty中最成熟也是被广泛引用的模型,因此在使用Netty的时候,我们会采用NioServerSocketChannel作为Channel类型。

bootstrap.channel(NioServerSocketChannel.class);

除了NioServerSocketChannel以外,还提供了

  • EpollServerSocketChannel,epoll模型只有在linux kernel 2.6以上才能支持,在windows和mac都是不支持的,如果设置Epoll在window环境下运行会报错。
  • OioServerSocketChannel,用于服务端阻塞地接收TCP连接
  • KQueueServerSocketChannel,kqueue模型,是Unix中比较高效的IO复用技术,常见的IO复用技术有select, poll, epoll以及kqueue等等。其中epoll为Linux独占,而kqueue则在许多UNIX系统上存在。

注册ChannelHandler

在Netty中可以通过ChannelPipeline注册多个ChannelHandler,该handler就是给到worker线程执行的处理器,当IO事件就绪时,会根据这里配置的Handler进行调用。

这里可以注册多个ChannelHandler,每个ChannelHandler各司其职,比如做编码和解码的handler,心跳机制的handler,消息处理的handler等。这样可以实现代码的最大化复用。

.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new NormalMessageHandler());
    }
});

ServerBootstrap中的childHandler方法需要注册一个ChannelHandler,这里配置了一个ChannelInitializer的实现类,通过实例化ChannelInitializer来配置初始化Channel。

当收到IO事件后,这个数据会在这多个handler中进行传播。上述代码中配置了一个NormalMessageHandler,用来接收客户端消息并输出。

绑定端口

完成Netty的基本配置后,通过bind()方法真正触发启动,而sync()方法会阻塞,直到整个启动过程完成。

ChannelFuture channelFuture=bootstrap.bind(port).sync();

NormalMessageHandler

ServerHandler继承了ChannelInboundHandlerAdapter,这是netty中的一个事件处理器,netty中的处理器分为Inbound(进站)和Outbound(出站)处理器,后面会详细介绍。

public class NormalMessageHandler extends ChannelInboundHandlerAdapter {
    //channelReadComplete方法表示消息读完了的处理,writeAndFlush方法表示写入并发送消息
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //这里的逻辑就是所有的消息读取完毕了,在统一写回到客户端。Unpooled.EMPTY_BUFFER表示空消息,addListener(ChannelFutureListener.CLOSE)表示写完后,就关闭连接
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    //exceptionCaught方法就是发生异常的处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    //channelRead方法表示读到消息以后如何处理,这里我们把消息打印出来
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in=(ByteBuf) msg;
        byte[] req=new byte[in.readableBytes()];
        in.readBytes(req); //把数据读到byte数组中
        String body=new String(req,"UTF-8");
        System.out.println("服务器端收到消息:"+body);
        //写回数据
        ByteBuf resp=Unpooled.copiedBuffer(("receive message:"+body+"").getBytes());
        ctx.write(resp);
        //ctx.write表示把消息再发送回客户端,但是仅仅是写到缓冲区,没有发送,flush才会真正写到网络上去
    }
}

通过上述代码发现,我们只需要通过极少的代码就完成了NIO服务端的开发,相比传统的NIO原生类库的服务端,代码量大大减少,开发难度也大幅度降低。

Netty和NIO的api对应

TransportChannel —-对应NIO中的channel

EventLoop—- 对应于NIO中的while循环

EventLoopGroup: 多个EventLoop,就是事件循环

ChannelHandler和ChannelPipeline—对应于NIO中的客户逻辑实现handleRead/handleWrite(interceptor pattern)

ByteBuf—- 对应于NIO 中的ByteBuffer

Bootstrap 和 ServerBootstrap —对应NIO中的Selector、ServerSocketChannel等的创建、配置、启动等

Netty的整体工作机制

Netty的整体工作机制如下,整体设计就是前面我们讲过的多线程Reactor模型,分离请求监听和请求处理,通过多线程分别执行具体的handler。

image-20210812181454154

图2-2

网络通信层

网络通信层主要的职责是执行网络的IO操作,它支持多种网络通信协议和I/O模型的链接操作。当网络数据读取到内核缓冲区后,会触发读写事件,这些事件在分发给时间调度器来进行处理。

在Netty中,网络通信的核心组件以下三个组件

  • Bootstrap, 客户端启动api,用来链接远程netty server,只绑定一个EventLoopGroup
  • ServerBootStrap,服务端监听api,用来监听指定端口,会绑定两个EventLoopGroup, bootstrap组件可以非常方便快捷的启动Netty应用程序
  • Channel,Channel是网络通信的载体,Netty自己实现的Channel是以JDK NIO channel为基础,提供了更高层次的抽象,同时也屏蔽了底层Socket的复杂性,为Channel提供了更加强大的功能。

如图2-3所示,表示的是Channel的常用实现实现类关系图,AbstractChannel是整个Channel实现的基类,派生出了AbstractNioChannel(非阻塞io)、AbstractOioChannel(阻塞io),每个子类代表了不同的I/O模型和协议类型。

image-20210812213408836

图2-3 Channel的类关系图

随着连接和数据的变化,Channel也会存在多种状态,比如连接建立、连接注册、连接读写、连接销毁。随着状态的变化,Channel也会处于不同的生命周期,每种状态会绑定一个相应的事件回调。以下是常见的时间回调方法。

  • channelRegistered, channel创建后被注册到EventLoop上
  • channelUnregistered,channel创建后未注册或者从EventLoop取消注册
  • channelActive,channel处于就绪状态,可以被读写
  • channelInactive,Channel处于非就绪状态
  • channelRead,Channel可以从源端读取数据
  • channelReadComplete,Channel读取数据完成

简单总结一下,Bootstrap和ServerBootStrap分别负责客户端和服务端的启动,Channel是网络通信的载体,它提供了与底层Socket交互的能力。

而当Channel生命周期中的事件变化,就需要触发进一步处理,这个处理是由Netty的事件调度器来完成。

事件调度器

事件调度器是通过Reactor线程模型对各类事件进行聚合处理,通过Selector主循环线程集成多种事件(I/O时间、信号时间),当这些事件被触发后,具体针对该事件的处理需要给到服务编排层中相关的Handler来处理。

事件调度器核心组件:

  • EventLoopGroup。相当于线程池

  • EventLoop。相当于线程池中的线程

EventLoopGroup本质上是一个线程池,主要负责接收I/O请求,并分配线程执行处理请求。为了更好的理解EventLoopGroup、EventLoop、Channel之间的关系,我们来看图2-4所示的流程。

image-20210812220244801

图2-4,EventLoop的工作机制

从图中可知

  • 一个EventLoopGroup可以包含多个EventLoop,EventLoop用来处理Channel生命周期内所有的I/O事件,比如accept、connect、read、write等
  • EventLoop同一时间会与一个线程绑定,每个EventLoop负责处理多个Channel
  • 每新建一个Channel,EventLoopGroup会选择一个EventLoop进行绑定,该Channel在生命周期内可以对EventLoop进行多次绑定和解绑。

图2-5表示的是EventLoopGroup的类关系图,可以看出Netty提供了EventLoopGroup的多种实现,如NioEventLoop、EpollEventLoop、NioEventLoopGroup等。

从图中可以看到,EventLoop是EventLoopGroup的子接口,我们可以把EventLoop等价于EventLoopGroup,前提是EventLoopGroup中只包含一个EventLoop。

image-20210812221329760
图2-5 EventLoopGroup类关系图

EventLoopGroup是Netty的核心处理引擎,它和前面我们讲解的Reactor线程模型有什么关系呢?其实,我们可以简单的把EventLoopGroup当成是Netty中Reactor线程模型的具体实现,我们可以通过配置不同的EventLoopGroup使得Netty支持多种不同的Reactor模型。

  • 单线程模型,EventLoopGroup只包含一个EventLoop,Boss和Worker使用同一个EventLoopGroup。
  • 多线程模型:EventLoopGroup包含多个EventLoop,Boss和Worker使用同一个EventLoopGroup。
  • 主从多线程模型:EventLoopGroup包含多个EventLoop,Boss是主Reactor,Worker是从Reactor模型。他们分别使用不同的EventLoopGroup,主Reactor负责新的网络连接Channel的创建(也就是连接的事件),主Reactor收到客户端的连接后,交给从Reactor来处理。

服务编排层

服务编排层的职责是负责组装各类的服务,简单来说,就是I/O事件触发后,需要有一个Handler来处理,所以服务编排层可以通过一个Handler处理链来实现网络事件的动态编排和有序的传播。

它包含三个组件

  • ChannelPipeline,它采用了双向链表将多个Channelhandler链接在一起,当I/O事件触发时,ChannelPipeline会依次调用组装好的多个ChannelHandler,实现对Channel的数据处理。ChannelPipeline是线程安全的,因为每个新的Channel都会绑定一个新的ChannelPipeline。一个ChannelPipeline关联一个EventLoop,而一个EventLoop只会绑定一个线程,如图2-6所示,表示ChannelPIpeline结构图。

    image-20210812223234507
    图2-6 ChannelPipeline

    从图中可以看出,ChannelPipeline中包含入站ChannelInBoundHandler和出站ChannelOutboundHandler,前者是接收数据,后者是写出数据,其实就是InputStream和OutputStream,为了更好的理解,我们来看图2-7。

image-20210812224219710

图2-7 InBound和OutBound的关系

  • ChannelHandler, 针对IO数据的处理器,数据接收后,通过指定的Handler进行处理。

  • ChannelHandlerContext,ChannelHandlerContext用来保存ChannelHandler的上下文信息,也就是说,当事件被触发后,多个handler之间的数据,是通过ChannelHandlerContext来进行传递的。ChannelHandler和ChannelHandlerContext之间的关系,如图2-8所示。

    每个ChannelHandler都对应一个自己的ChannelHandlerContext,它保留了ChannelHandler所需要的上下文信息,多个ChannelHandler之间的数据传递,是通过ChannelHandlerContext来实现的。

image-20210812230122911

图2-8 ChannelHandler和ChannelHandlerContext关系

以上就是Netty中核心的组件的特性和工作机制的介绍,后续的内容中还会详细的分析这几个组件。可以看出,Netty的架构分层设计是非常合理的,它屏蔽了底层NIO以及框架层的实现细节,对于业务开发者来说,只需要关心业务逻辑的编排和实现即可。

组件关系及原理总结

如图2-9所示,表示Netty中关键的组件协调原理,具体的工作机制描述如下。

  • 服务单启动初始化Boss和Worker线程组,Boss线程组负责监听网络连接事件,当有新的连接建立时,Boss线程会把该连接Channel注册绑定到Worker线程
  • Worker线程组会分配一个EventLoop负责处理该Channel的读写事件,每个EventLoop相当于一个线程。通过Selector进行事件循环监听。
  • 当客户端发起I/O事件时,服务端的EventLoop讲就绪的Channel分发给Pipeline,进行数据的处理
  • 数据传输到ChannelPipeline后,从第一个ChannelInBoundHandler进行处理,按照pipeline链逐个进行传递
  • 服务端处理完成后要把数据写回到客户端,这个写回的数据会在ChannelOutboundHandler组成的链中传播,最后到达客户端。

image-20210814151504091

图2-9 Netty各个组件的工作原理

Netty中核心组件的详细介绍

在2.5节中对Netty有了一个全局认识后,我们再针对这几个组件做一个非常详细的说明,加深大家的理解。

启动器Bootstrap和ServerBootstrap作为Netty构建客户端和服务端的路口,是编写Netty网络程序的第一步。它可以让我们把Netty的核心组件像搭积木一样组装在一起。在Netty Server端构建的过程中,我们需要关注三个重要的步骤

  • 配置线程池
  • Channel初始化
  • Handler处理器构建

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注「跟着Mic学架构」公众号公众号获取更多技术干货!

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » [年薪60W的技巧]工作了5年,你真的理解Netty以及为什么要用吗?(深度干货)