Netty源码分析一之Netty重要相关组件介绍及服务端启动过程源码剖析

903 阅读14分钟

前言:netty是一个基于jdk nio(nio三大组件selector,channel,buffer)的封装的高性能的基于事件驱动网络编程框架,它高性能的主要原因之一是基于它的线程模型(reactor),netty支持多协议,例如tcp,udp,http,ws等,应用领域:netty主要用于服务端编程,例如物联网智能硬件通讯,游戏服务器,基于rpc框架的实现(dubbo,rocketMQ等),netty上手简单,但是源码较为复杂,主要是netty的api比较庞大。netty还有一个重要的特点就是异步,Netty有独特的设计IO,它的IO操作都立刻返回,他不会保证请求的IO的是完成的,不论这些IO的成功或失败,取消,Netty的ChannelFuture都会返回,异步不会保证请求的IO是立即完成状态,因为他是立刻返回的去执行其他任务。

由BIO到NIO再到reactor
Socket通讯流程:
image.png

阻塞和非阻塞: image.png

同步和非同步: image.png

BIO(同步并阻塞)模型图:
image.png 由此可见,client线程和服务端线程是1:1的,当客户端的连接数目不断增加,服务端的线程开销也会增大,严重影响服务端性能,而且有些线程还一直阻塞着。 生活中的例子: image.png

NIO(同步非阻塞): image.png
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到 多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理
生活中的例子: image.png

总结:NIO和 BIO的比较

  1. BIO 以流的方式处理数据,而 NIO 以缓冲区的方式处理数据,缓冲区 I/O 的效率比流 I/O 高很多
  2. BIO 是阻塞的,NIO则是非阻塞的
  3. BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据 总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的 事件(比如:连接请求, 数据到达等),因此使用单个线程就可以监听多个客户端通道

原生 NIO 存在的问题:

  1. NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  2. 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
  3. 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
  4. JDK NIO 的 Bug:臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到JDK 1.7版本该问题仍旧存在,没有被根本解决

netty的主要组件:
netty的线程模型是主从 Reactor 多线程模式,并且在此基础之上做了一些改进,关于reactor模式的演进可以查阅相关的资料进行对比 image.png

  1. Channel 是java NIO的一个基本组件,可以被打开或者关闭,连接或者断开连接。简单来说其实就是我们平常网络编程中经常使用的socket套接字对象。 image.png 核心API一览:
  • EventLoop eventLoop()
    返回该通道注册的事件轮询器。
  • Channel parent()
    返回该通道的父通道,如果是ServerSocketChannel实例则返回null,SocketChannel实例则返回对应的ServerSocketChannel。
  • ChannelConfig config()
    返回该通道的配置参数。
  • boolean isOpen()
    端口是否处于open,通道默认一创建isOpen方法就会返回true,close方法被调用后该方法返回false。
  • boolean isRegistered()
    是否已注册到EventLoop。
  • public boolean isActive()
    通道是否处于激活。NioSocketChannel的实现是java.nio.channels.SocketChannel实例的isOpen()与isConnected()都返回true。NioServerSocketChannel的实现是ServerSocketChannel.socket().isBound(),如果绑定到端口中,意味着处于激活状态。
    ChannelFuture closeFuture()
    Future 模式的应用,调用该方法的目的并不是关闭通道,而是预先创建一个凭证(Future),等通道关闭时,会通知该 Future,用户可以通过该 Future 注册事件。
  • ChannelFuture bind(SocketAddress localAddress)
    Netty 服务端绑定到本地端口,开始监听客户端的连接请求。该过程会触发事件链(ChannelPipeline)。该部分将在后续讲解服务端启动流程时再详细分析。
  • ChannelFuture connect(SocketAddress remoteAddress)
    Netty客户端连接到服务端,该过程同样会触发一系列事件(ChannelPipeline)。该部分将在后续讲解客户端启动流程时再详细分析。
  • ChannelFuture disconnect()
    断开连接,但不会释放资源,该通道还可以再通过connect重新与服务器建立连接。
  • ChannelFuture close()
    关闭通道,回收资源,该通道的生命周期完全结束。
  • ChannelFuture deregister()
    取消注册。
  • Channel read()
    通道读,该方法并不是直接从读写缓存区读取文件,而是向NIO Selecor注册读事件(目前主要基于NIO)。当通道收到对端的数后,事件选择器会处理读事件,从而触发ChannelInboundHandler#channelRead 事件,然后继续触发ChannelInboundHandler#channelReadComplete(ChannelHandlerContext)事件。
  • ChannelFuture write(Object msg)
    向通道写字节流,会触发响应的写事件链,该方法只是会将字节流写入到通道缓存区,并不会调用flush方法写入通道中。
  • Channel flush()
    刷写所有挂起的消息(刷写到流中)。
  • ChannelFuture writeAndFlush(Object msg)
    相当于调用write与flush方法。

2.EventLoop
这也是netty是基于事件驱动的本质,简单的理解就是个死循环(直到停止),EventLoop是一个Reactor模型的事件处理器,一个EventLoop对应一个线程,其内部会维护一个selector和taskQueue,负责处理客户端请求和内部任务,内部任务如ServerSocketChannel注册、ServerSocket绑定和延时任务处理等操作

2.1、一个EventLoopGroup包含一个或者多个EventLoop;
2.2、一个EventLoop在它的生命周期内只和一个Thread绑定;
2.3、所有有EventLoop处理的I/O事件都将在它专有的Thread上被处理;
2.4、一个Channel在它的生命周期内只注册于一个EventLoop;
2.5、一个EventLoop可能会被分配给一个货多个Channel;
其实我们可以简单的把EventLoop及其相关的实现NioEventLoop、NioEventLoopGroup等理解为netty针对我们网络编程时创建的多线程进行了封装和优化,构建了自己的线程模型。 关于eventLoop做了哪些事情以及它的责任强烈建议参考这篇文章zhuanlan.zhihu.com/p/95301195

3.ChannelHandler和ChannelPipeline
3.1 channelPipeline实际上就是一个channelHandler链容器,它的真实实现(DefaultChannelPipeline)就是一个双向链表,核心api就是addLast(往链表的尾部添加一个channelHandlerContext)和addFirst(往链表的头部添加一个channelHandler)。ChannelPipeline本身不做任何事情,它相当于容器,一个IO时间Handler要么是一个ChannelInboundHandler处理,要么是一个ChannelOutboundHandler处理,转发与它最近的Handler通过事件传播方法定义ChannelHandlerContext来实现(重点image.png 用一张图来形象的表示一下channelPipeline的大致构成 image.png 上图的每个结点都是一个channelHandler,从头部节点向右入是一个inBound(解码)事件传播,从尾部节点向左是一个outBound(编码)节点,需要小心的是我们在往pipeline添加handler的时候需要注意我们业务handler的执行顺序,关于channelPipline先到此为止。
3.2 ChannelHandler
处理I/O事件或截取I/O操作,并将其转发到中的下一个处理程序。先上一张channelHandler UML类图: image.png 我们需要关心它的两个子接口ChannelInboundHandler(入站)和ChannelOutBoundHandler(出站)
「inbound 事件(通常由I/O 线程触发,例如 TCP 链路建立事件、链路关闭事件、读事件、异常通知时间等,对应上图的左半部分)」

  • ChannelHandlerContext fireChannelRegistered(); Channel 注册事件
  • ChannelHandlerContext fireChannelActive(); Tcp 链路建立成功,Channel 激活事件
  • ChannelHandlerContext fireChannelRead(Object msg); 读事件
  • ChannelHandlerContext fireChannelReadComplete(); 读操作完成通知事件
  • ChannelHandlerContext fireExceptionCaught(Throwable cause); 异常通知事件
  • ChannelHandlerContext fireUserEventTriggered(Object event);用户自定义事件
  • ChannelHandlerContext fireChannelWritabilityChanged(); Channel 的可写状态变化通知事件
  • ChannelHandlerContext fireChannelInactive();Tcp 连接关闭,链路不可用通知事件 pipeline 中以 fireXXX 命名的方法都是从 IO 线程流向用户业务 handler 的 iobound 事件,他们的实现因功能而异

「outbound 事件(通常由用户主动发起的网络 I/O 操作,例如用户发起的连接操作、绑定操作、消息发送等操作)」

  • ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise); 绑定本地地址事件
  • ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise);连接服务端事件
  • ChannelFuture write(Object msg, ChannelPromise promise);发送事件
  • ChannelHandlerContext flush();刷新事件
  • ChannelHandlerContext read();读事件
  • ChannelFuture disconnect(ChannelPromise promise);断开连接事件
  • ChannelFuture close(ChannelPromise promise); 关闭当前 Channel 事件 由用户线程或者代码发起的 IO 操作被称为 outbound 事件,事实上 inbound 和 outbound 是 netty 自身根据时间在 pipeline 中的流向抽象出来的术语,再其他 nio 框架中并没有这个概念。\
ChannelHandler 支持的注解:

Sharable:多个 ChannelPipeline 共有同一个 ChannelHandler
Skip:被 skip 注解的方法不会被调用,直接被忽略\

ChannelHandlerAdapter来由

对于大多数 ChannelHandler 会选择性的拦截和处理某个或者某些事件,其他事件会忽略,由下一个 Handler 进行拦截和处理,这就导致用户的 ChannelHandler 必须实现所有接口,这样就形成了代码冗余,可维护性差。为了解决这个问题,Netty 提供了 ChannelHandlerAdapter 基类,他的所有接口实现都是事件透传,如果用户 ChannelHandler 关心某个事件,只需要覆盖 ChannelHandlerAdapter 对应的方法即可,这样类的代码就会非常简洁和清晰。ChannelHandlerAdapter 类继承图如下: image.png

ChannelHandlerContext详解:
简称ctx,该类可以理解为一个通道处理器的上下文,里面包含了channelPipeline,channelHandler,channel,EventExecutor等对象,方便程序员使用。 image.png 如上图,双向链表的每一个节点都是一个channelHandlerContext,而每一个结点的handler又是我们业务处理的handler(进站或出站),我们可以在某个业务处理的handler(例如进站的某个业务处理的handler)的channelRead方法通过ctx get一个channel做writer操作,还可以在这个业务的handler 通过ctx get一个EventExecutor对象,执行一个异步任务。Ctx相当于一个组合模式,组合了以上对象方便我们在实际业务处理的handler里面用ctx获取以上对象来方便我们需要的做对应的业务处理。

4.ByteBuf
netty也提供了自己独特的字节数组方便存取数据。于jdk不同的是jdk也提供了自己的ByteBuffer,这里强烈建议使用netty的byteBuf,因为jdk的byteBuf使用起来非常麻烦。首先他们两个都是基于数组实现的,jdk的buffer只有一个索引,每次进行读写切换非常不方便,需要调用flip进行切换读写模式,而netty的buf维护了两个索引,一个readerIndex和writerIndex
image.png 我们假如由0位置开始写入3个字节,那么readerIndex还是0,而writerIndex则由0变成3 image.png

源码解读部分:
2.1EventLoopGroup事件循环组启动过程分析:
我们重点看下eventLoop里面包含了哪几个重要的属性,以及它的启动流程的一些细节。 EventLoopGroup 是一组 EventLoop 的抽象,Netty为了更好的利用多核CPU资源, 一般会有多个EventLoop同时工作<每个EventLoop维护着一个Selector和taskQueue

image.png

2.2线程组源码流程分析参考下图 image.png netty tcp服务端经典案例 image.png 设置线程数 image.png 上一个构造创建了选择器 image.png 调用super进入父类的构造 image.png newChild方法完成NioEventLoop的创建 image.png 点进去newChild方法 image.png 创建NioEventLoop image.png 进入这个NioEventLoop的构造函数,可以看到创建任务队列和选择器 image.png 自此,就完成创建NioEventLoop,可以看到children就是一个EventExecutor数组,里面放了NioEventLoop, 这里就只有1个EventLoop,因为我们bossGroup设置的是1。NioEventLoop里面还有2个重要的属性selecto和taskQueue。我们可以将NioEventLoop看成是线程池里面的一个线程,可以重复使用。 image.png 完成workGroup的创建,可以看到是cpu核心数的2倍 image.png 接下来可以看看bootstrap.bind()【重点】做了哪些事情(给serverBootstrap就不看了,主要是设置参数,留意一下通道初始化对象的创建,后续讲解)点进去 image.png image.png 跟入initAndRegister方法 image.png 这个initAndRegister方法主要是创建了一个channel和完成channel初始化,类型是NioServerSocketChannel。调用init方法(模板方法)。下面这张图应该是截错了,先跳过这张图请继续往下看 image.png 走完该方法接下里就开始注册通道 image.png image.png 一直跟进去会进入AbstractUnsafe的register的方法 image.png 这个eventLoop是bossGroup对象,因为我们设置bossGroup对象的线程数为1
接下来执行execute方法 image.png 这里做了三件时间1:先是判断当前线程是不是eventLoop,2:添加任务队列,3:启动线程 image.png image.png 点进去doStartThread看看启动线程做了什么 image.png 进入this.run() //该方法很重要,就是个死循环,对应开始的线程模型图的这部分 image.png image.png 这个run方法执行三个动作 image.png image.png 第一次进来先走step2,因为第一次判断stitch里面的selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())的结果是0。原因是step1的select方法没有轮询到连接事件,也即为nio的selector事件。
接下来点进这个step2的processSelectorKeys干了些什么 image.png image.png 进入processSelectedKeysOptimized方法 image.png 可以看到第一次进来selectedKeys.size==0,所以又退出去了,退出去之后就进入finally块 执行了step3的runAllTasks方法 image.png 进入runAllTasks,重点看safeExecute方法 image.png 进入safeExecute方法,这个方法实际上就是执行register0 image.png image.png 可以看到回到了register0 image.png 这个register0方法里面有个doRegister()方法【这个方法是注册通道】 image.png 进入doRegister方法,这里有个非常关键的步骤就是selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); image.png 我们看看javaChannel是什么,点进去看就知道了。它就是一个ServerSocketChannel image.png 退出去回到上一个方法我们看到.register里面有个eventLoop().unwrappedSelector()这个方法的调用。 它实际上就是返回了一个selector。目前我已经有了通道和选择器,然后执行javaChannel().register的注册方法注册accpet事件。这个方法往里面走就是nio的事件注册,自此bossGroup就不断循环执行 step1,step2,step3。
总结下一下bossGroup大致工作流程(建议看的时候脑子里有netty的线程模型图):
image.png 主要看NioEventLoop这个类里面的run方法的for(;;){}
step1: selector【检查是否有selector事件】 select方法实际上就是执行nio的selector.select();也就是检查当前的选择器是否有事件(因为这个eventLoop是boosGroup,所以这里检查的是否有accpet事件),然后还做了一个选择器重建(selectRebuildSelector方法),这个是解决了nio空轮询的bug image.png image.png step2:accpet【建立与此频道的套接字的连接】 image.png image.png image.png 进入到这个方法processSelectedKey方法后直接看最后面的unsafe.read();里面做了是什么事情 image.png image.png 可以看到do...while里面有个doReadMessage这个方法,还传入了一个readBuf的Object类型的集合,这个集合先不说用来干嘛的。我们先进入doReadMessage看看里面做了什么 image.png image.png 可以看到最后就是调用jdk的serverSocketChannel的accpet函数。自此我们也就知道了step2做了什么事情了。我们再看看还有一个很重要的一步,在SocketChannel ch = SocketUtils.accept(javaChannel());这一行下面的try里面有个buf.add方法, 这个readBuf的list是做什么的呢?看下图 image.png 一直跟进去fireChannelRead就会发现它就是一个入站的操作,这部分我后续分开来讲。

step3: register【将服务端的通道注册到workGroup上的选择器】
一直跟进去看 image.png image.png 再跟进safeExecute(task) image.png 看到有一个run方法,点进去run方法,实际上就进入了AbstractChannel类的这个run方法 image.png 然后进入register0方法,就会看到有个doRegister image.png 进入doRegister方法可以看到javaChannel.register方法,这个方法就是nio的将通道注册到选择器上 image.png 自此,就完成了step3。然后我们可以发现eventLoop那个for(;;){}会不断的重复做3三件事情。以上就完成了eventLoop启动的源码讲解。关于workGroup的处理基本和workGroup是一致的,只不过是监听的事件不同而已。后续有时间再讲解下入站和出站的源码解读。自己在学习源码的过程也有些过程不是很清晰,自己的查阅了很多博客,观看netty讲解的视频总结的出来的解答,如有不对或不足的地方请指正,谢谢!