简单解析Netty模型的执行流程

首先解释一下Netty的零拷贝体现在何处?

Netty的零拷贝主要体现在三个方面:

第一种实现:DirectByteBuf   直接内存缓冲区
就如上所说,ByteBuf可以分为HeapByteBuf和DirectByteBuf,当使用DirectByteBuf可以实现零拷贝

第二种实现:CompositeByteBuf   复合缓冲区
CompositeByteBuf将多个ByteBuf封装成一个ByteBuf,对外提供封装后的ByteBuf接口

第三种实现:DefaultFileRegion  
DefaultFileRegion是Netty的文件传输类,它通过transferTo方法将文件直接发送到目标Channel,而不需要循环拷贝的方式,提升了传输性能

为什么Netty使用NIO而不是AIO?

  1. Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化
  2. Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱,把AIO也改造成reactor模型看起来是把epoll绕个弯又绕回来
  3. AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多
  4. Linux上AIO不够成熟,处理回调结果速度跟不到处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)

Netty线程模型

服务端线程模型

第一步:从用户线程起发起创建服务端操作,代码如下:【用户线程创建服务端代码示例】

NioEventLoopGroup boss=new NioEventLoopGroup(1);
        NioEventLoopGroup worker=new NioEventLoopGroup();

        try {

            //服务端启动辅助类,主要作用就是将接收连接和用户逻辑处理相关的配置进行初始化
            ServerBootstrap bootstrap=new ServerBootstrap()
                    //将主/工作EventLoopGroup进行配置
                    .group(boss,worker)
                    //设置主EventLoopGroup处理的通道类型
                    .channel(NioServerSocketChannel.class)
                    //配置工作EventLoopGroup相关的Handler
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast( new NettyServerHandler());
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new StringEncoder());
                        }
                    });
            //同步阻塞绑定端口,真正启动服务端
            ChannelFuture sync=bootstrap.bind(6666).sync();

            //同步阻塞关闭服务端
            sync.channel().closeFuture().sync();

通常情况下,服务端的创建是在用户进程启动的时候进行,因此一般由Main函数或者启动类负责创建,服务端的创建由业务线程负责完成。在创建服务端的时候实例化了2个EventLoopGroup,1个EventLoopGroup实际就是一个EventLoop线程组,负责管理EventLoop的申请和释放。

EventLoopGroup管理的线程数可以通过构造函数设置,如果没有设置,默认取-Dio.netty.eventLoopThreads,如果该系统参数也没有指定,则为可用的CPU内核数 × 2。

bossGroup线程组实际就是Acceptor线程池,负责处理客户端的TCP连接请求,如果系统只有一个服务端端口需要监听,则建议bossGroup线程组线程数设置为1。

workerGroup是真正负责I/O读写操作的线程组,通过ServerBootstrap的group方法进行设置,用于后续的Channel绑定。

第二步,Acceptor线程绑定监听端口,启动NIO服务端,相关代码如下:

图2-3 从bossGroup中选择一个Acceptor线程监听服务端

其中,group()返回的就是bossGroup,它的next方法用于从线程组中获取可用线程,代码如下:

图2-4 选择Acceptor线程

服务端Channel创建完成之后,将其注册到多路复用器Selector上,用于接收客户端的TCP连接,核心代码如下:

图2-5 注册ServerSocketChannel 到Selector

第三步,如果监听到客户端连接,则创建客户端SocketChannel连接,重新注册到workerGroup的IO线程上。首先看Acceptor如何处理客户端的接入:

图2-6 处理读或者连接事件

调用unsafe的read()方法,对于NioServerSocketChannel,它调用了NioMessageUnsafe的read()方法,代码如下:

图2-7 NioServerSocketChannel的read()方法

最终它会调用NioServerSocketChannel的doReadMessages方法,代码如下:

图2-8 创建客户端连接SocketChannel

其中childEventLoopGroup就是之前的workerGroup, 从中选择一个I/O线程负责网络消息的读写。

第四步,选择IO线程之后,将SocketChannel注册到多路复用器上,监听READ操作。

图2-9 监听网络读事件

第五步,处理网络的I/O读写事件,核心代码如下:

客户端线程模型

客户端的线程模型简单一些,它的工作原理如下:

图2-11 Netty客户端线程模型

第一步,由用户线程发起客户端连接,示例代码如下:

图2-12 Netty客户端创建代码示例

大家发现相比于服务端,客户端只需要创建一个EventLoopGroup,因为它不需要独立的线程去监听客户端连接,也没必要通过一个单独的客户端线程去连接服务端。Netty是异步事件驱动的NIO框架,它的连接和所有IO操作都是异步的,因此不需要创建单独的连接线程。相关代码如下:

当前的group()就是之前传入的EventLoopGroup,从中获取可用的IO线程EventLoop,然后作为参数设置到新创建的NioSocketChannel中。

第二步,发起连接操作,判断连接结果,代码如下:

判断连接结果,如果没有连接成功,则监听连接网络操作位SelectionKey.OP_CONNECT。如果连接成功,则调用pipeline().fireChannelActive()将监听位修改为READ。

第三步,由NioEventLoop的多路复用器轮询连接操作结果,代码如下:

判断连接结果,如果或连接成功,重新设置监听位为READ:

第四步,由NioEventLoop线程负责I/O读写,同服务端。

总结:客户端创建,线程模型如下:

  1. 由用户线程负责初始化客户端资源,发起连接操作;
  2. 如果连接成功,将SocketChannel注册到IO线程组的NioEventLoop线程中,监听读操作位;
  3. 如果没有立即连接成功,将SocketChannel注册到IO线程组的NioEventLoop线程中,监听连接操作位;
  4. 连接成功之后,修改监听位为READ,但是不需要切换线程。

 NioEventLoop介绍

NioEventLoop是Netty的Reactor线程,它的职责如下:

  1. 作为服务端Acceptor线程,负责处理客户端的请求接入;
  2. 作为客户端Connecor线程,负责注册监听连接操作位,用于判断异步连接结果;
  3. 作为IO线程,监听网络读操作位,负责从SocketChannel中读取报文;
  4. 作为IO线程,负责向SocketChannel写入报文发送给对方,如果发生写半包,会自动注册监听写事件,用于后续继续发送半包数据,直到数据全部发送完成;
  5. 作为定时任务线程,可以执行定时任务,例如链路空闲检测和发送心跳消息等;
  6. 作为线程执行器可以执行普通的任务线程(Runnable)。

NioEventLoop设计原理

串行化设计避免线程竞争

我们知道当系统在运行过程中,如果频繁的进行线程上下文切换,会带来额外的性能损耗。多线程并发执行某个业务流程,业务开发者还需要时刻对线程安全保持警惕,哪些数据可能会被并发修改,如何保护?这不仅降低了开发效率,也会带来额外的性能损耗。

串行执行Handler链

为了解决上述问题,Netty采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由IO线程NioEventLoop负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险,对于用户而言,甚至不需要了解Netty的线程细节,这确实是个非常好的设计理念,

参考:https://blog.csdn.net/u010739551/article/details/80519295

猜你喜欢

转载自blog.csdn.net/qq_40303781/article/details/88667799