type
status
date
urlname
summary
tags
category
icon
password
catalog
sort

前言

前面几个小节,我们梳理了几个核心组建,深入的分析了这几个组建的工作原理和一些实现细节,但是细看局部总是难以获得一个全局的视角,很难把它们真正的串起来。在梳理完这几个组件之后,我最想知道的就是启动的时候和读写操作的时候,这几个组件是怎么配合工作。虽然我也看过一切流程图,和一些简单的总结,但是自己没有深入过源码总感觉缺了点什么。我相信只有真正分析调试过源码才能真正理解,各个组件之间是怎么协作的,各个组件之间的设计要点是什么了。带着这些疑惑,我深入了源码也收获颇丰。这个部分,我们将对Netty的启动过程进行深入的分析,看看Netty是如何进行注册、绑定和监听。

NIO服务器的基本设计思路

我们知道Netty也是一套高性能网络框架,Netty底层有对各种IO模型的支持,包括BIO、NIO和后来的AIO(只是Netty对于AIO的尝试是一个悲伤的故事),最终Netty选择基于NIO模型来实现。在深入Netty的启动流程之前,我们先眼熟几个NIO中比较重要的类/组件:
  • Selector:多路复用器,是NIO的一个核心组件,在BIO中是没有的,主要用于监听NIO Channel的各种事件,并检查一个或者多个通道是否处于可读/写状态。在实现单条线程管理多条链路时候,传统的BIO管理多条链路是通过多线程上下文切换来实现的,而NIO有了Selector之后,一个Selector只需要用一个线程就可以轮询处理多个链路通道。
  • ServerSocketChannel:与普通BIO中的ServerSocket一样,主要用来监听新加入的TCP连接通道,而且其启动方式与ServerSocket的启动方式也非常类似,只需要在开启端口监听之前,把ServerSocketChannel注册到Selector上,并设置OP_ACCEPT事件即可
  • SocketChannel:与普通BIO中的Socket一样,是连接到TCP网络Socket上的Channel。它的创建方式有两种:一种方式与ServerSocketChannel的创建方式类似,打开一个SocketChannel,这种方式主要用于客户端主动连接器另一种方式是当有客户端连接器ServerSocketChannel上时,会创建一个SocketChannel。
NIO服务设计设计主要分为三步:第一步,服务器启动与端口监听;第二步,ServerSocketChannel 处理新接入链路;第三步,SocketChannel 读/写数据。
notion image

Server端服务启动源码分析

Netty服务的过程和上面的NIO服务器的启动的核心部分并没有多大的区别,同样是要创建Selector,不过Netty会使用额外的线程去创建,也需要打开ServerSocketChannel,只不过采用了NIOServerSocketChannel 来进行包装实现,同样也需要把ServerSocketChannel注册到Selector上,这一步Netty也是使用NioEventLoop来实现,并返回ChannelPromise来异步通知是否注册成功。Netty的启动分三步走,register(注册)bind(绑定)active(激活)。在后续的源码分析中,我们也将整个启动流程拆分成这三个阶段来帮助源码的分析和理解,以及看看启动是如何把我们前面的几个模块串接起来的。在开始分析启动流程之前,我们先再次过一下我们熟悉的服务端启动代码和Server启动类ServerBootstrap

鸟瞰全局与ServerBootstrap

这里我们分析的启动类是EchoServer,这是一个io.netty.example中的一个example,因为它足够简单,也方便我们对启动类信息分析。
从上面的代码我们不难看出,我们梳理过的一些核心组件都是通过ServerBootstrap组合在一起,其中group()方法关联主从EventLoop,channel()指定bossGroup中的负责监听的channel。option()方法可以指定一些参数,handler()则是启动过程中的一些处理事件。后面的childHandler()则是具读写事件的channel已经channelPipline处理链的一些配置,最后一个重要的方法是bind()在上面所有的配置完成之后,bind()方法将拉起整个Netty服务。下面我们来深入ServerBootstrap的细节。
在ServerBootstrap中还有一个内部类对象ServerBootstrapAcceptor,这个对象在启动过程终会被加入到BossGroup的channel的Pipline中,作为Pipeline的最后一个处理器用于监听连接事件,它继承了ChannelInboundHandlerAdapter是一个InBoundhandler。这个类可以理解发挥着承上启下的功能,从他的名字Acceptor也不难看出它是用于监听OP_ACCEPT连接事件,当Accept事件发生后会触发channelRead方法,将childChannel注册到 childGroup (即我们创建 workerGroup)进行具体的读写操作。这个会在Netty就绪事件处理的部分进行详细分析。
ServerBootstrap 中还有一个需要我们注意的重要的方法init(),这个方法在后续整个启动流程的注册和启动部分会被调用。这个方法整体来说非常简单,需要注意的点是后半部分,将配置的handler加入到ServerSocketChannel 的 pipleline的这部分代码,这部分代码和我们构建ServerBootstrap时候写childHandler初始化的代码类似。这里在ServerSocketChannel中加入一个channel初始化的Handler即ChannelInitializer,在这个初始化过程中,又将我们构造ServerBootstrap时添加的LoggingHandler加入pipeline。最后包装创建的Accptor(ServerBootstrapAcceptor)对象加入到Pipeline中。但是加入的方式有一些特殊,并不是直接由当前执行线程直接加入,而是包装成一个EventLoop的task,等待后续的BossGroup的EventLoop进行执行。
看完上面的一些逻辑后,我们对于ServerBootstrap也有了一个大致的了解,接下来我们将从bind方法开始,具体看看Netty的启动是怎么样的,看看Netty是如何一步步的完成registerbindactive这几步操作的。在分析源码之前先提出两个两个问题,我们可以带着这两个问题去看源码;
NioServerSocketChannel的Handle管道DefaultChannelPipeline是如何添加Handler并触发各种事件的?
在服务启动的过程中,register、bind、active操作是通过EventLoopTask的形式执行的,那他们是如何保证有序的?
接下来我们就来从ServerBootstrapbind()方法开始分析Netty的启动流程源码。

initAndRegister(初始化与注册)部分剖析

这个部分我们要关注的主体方法initAndRegister(),这个方法会返回一个ChannelFuture(一个包着Channel的Future,因为这个channel在bind部分需要用)。在这个大方法中我们将完成通过反射创建在配置中预先设置的channel的NioServerSocketChannel实例对象,再调用SelectableChannelregister()方法注册到NioEventLoop的Selector上,并返回对应的selectionKey。完成注册之后,再调用DefaultChannelPipeline的callHandlerAddedForAllHandlers()方法,将ServerBootstrapAcceptor和一些自定义的handler加入到NioServerSocketChannel的pipleline。
这里要注意⚠️,这里的pipleline还只是NioServerSocketChannel的pipleline。
完成了这一步的操作之后,设置初始化完成和注册操作的主体就已经完成了,接下来将future设置为成功,并执行future中设置的监听器,也就是拉起我们的下一步bind操作。bind操作则是加入到eventLoop的下一个task。在完成bind方法的链接之后,我们的eventLoop线程(是的,这个时候已经不是主线程在操作了,而是eventLoop线程了)将沿着我们的NioServerSocketChannel的pipleline挨个执行InboundHandler的channelRegistered()方法。当这个方法执行完成之后,我们的initAndRegister部分就执行结束了。所以通过上面的简单介绍,我们对initAndRegister有一个大致的印象了,主要有一下几个部分。
不用着急,下面我们会深入源码一步步走一遍。
notion image

深入bind()方法

以下我们从bind方法开始这一部分的源码分析:
上面这部分我们从bind()方法入手,再经过多次调用重载方法之后,我们看到这个初始化与注册阶段的核心方法调用initAndRegister。initAndRegister是一个异步方法,返回一个ChannelFuture对象。在这个调用完成之后,拉起doBind0()方法执行端口绑定操作。以下是initAndRegister()方法的源码剖析。

initAndRegister()方法剖析

这个部分我们深入initAndRegister源码,这部分的源码逻辑也像方法名 initAndRegister,完成了serverBootstrap的init操作,随后就是register 方法,也就是随后将channel注册到selector上。(这个register 方法也包了好几层,最后是在AbstractUnsafe中实现了这个register方法,没错又是unsafe方法。)在注册完成后,调用invokeHandlerAddedIfNeeded将handler加入到channel的pipeline中,然后调用safeSetSuccess(promise)设promise操作成功,调用promise中设置的listener方法。在将handler加入到pipeline之后,调用pipeline.fireChannelRegistered()顺着pipeline调用inboundHandler的channelRegistered(),最后调用isActive()校验下是否channel是否已经开启并且已经绑定端口,正常情况下到这里都是没有isActive的结果都是false的。接下来,我们深入看看那invokeHandlerAddedIfNeededsafeSetSuccess(promise)pipeline.fireChannelRegistered()深入剖析下这些方法底层的实现细节。

invokeHandlerAddedIfNeeded() 方法剖析

在initAndRegister这部分分析的开始我们提到了两个问题,其中第一个问题是“NioServerSocketChannel的Handle管道DefaultChannelPipeline是如何添加Handler并触发各种事件的?”,而invokeHandlerAddedIfNeeded()就是将ServerBootstrap中初始化的handler加入到Channel的Pipeline中。接下来我们一起来看看吧。
上面这半段逻辑是invokeHandlerAddedIfNeeded()方法的前半段。这个部分我们看到在方法内部调用了callHandlerAddedForAllHandlers方法,这个方法是拉起channel的pipeline中handlerAdded的主要方法,这个方法里面我们重点关注了一个类PendingHandlerCallback,这个类本质上是一个链表结构的类,在第一次调用pipeline.addLast()的时候会包装一个handler进去作为链表的头,并将add的handler添加到链表的最后。并且每次调用pipeline.addLast()如果此时还没完成注册,就会在这个链表的最后添加handler。回到我们的callHandlerAddedForAllHandlers方法,在拿到了PendingHandlerCallback之后,我们顺着链表依次执行callHandlerAdded0方法即ChannelHandler调用自己的handlerAdded()方法。
理解这一段需要结合源码反复品。
那么我的问题来,我们前面提到了PendingHandlerCallback是一个链表结构,我们会依次沿着这个链表初始化ChannelHandler,那么第一次执行到这里的handler(),具体是哪个对象呢?换一个角度问这个问题,这个链表结构中,第一个被加入的handler是什么呢,第一次调用pipeline.addLast()被加的handler是哪个实例呢?其实这个实例是ChannelInitializer,channel初始化的handler。
所以通过上面代码的分析,我们不难得出,PendingHandlerCallback这个handlerAdd方法调用链上的第一个处理器就是我们在ServerBootstrap的init()方法中的ChannelInitializer内部类,弄清楚了这个问题,接下来我们顺着handler().handlerAdded(this);方法继续往下走。
ChannelInitializer的初始化方法中,在初始化过程中,我们创建LoggingHandler实例也被加入到pipeline中,我们前面分析的pipeline.addLast()方法,会判断register来决定是立刻执行HandlerAdded()方法,还是包装成PendingHandlerCallback加入到链表中。在这个场景中,register为true,会直接执行HandlerAdded(),即AbstractChannelHandlerContext#callHandlerAdded()方法。
而此时的handler()则是LoggingHandler的实例。LoggingHandler并没有实现handlerAdded()方法,因此实际执行的是io.netty.channel.ChannelHandlerAdapter#handlerAdded()
随着最后下面这段逻辑将ChannelInitializer从pipeline中移出去。invokeHandlerAddedIfNeeded()的逻辑也就结束了。当然最后还会通过 eventLoop的task的方式将ServerBootstrapAcceptor加入到pipeline中。

safeSetSuccess 与 fireChannelRegistered

完成了对方法invokeHandlerAddedIfNeeded()的深入剖析之后,我们继续回到register0()方法中。在完成了pipeline.invokeHandlerAddedIfNeeded();的调用,Netty将调用safeSetSuccess(promise);将异步结果 promise 设置为成功,并且调用后续的fireChannelRegistered()方法,触发pipeline中的handler的channelRegistered()方法。最后还会判断当前是否已经激活,如果激活直接执行channelActive方法。
首先我们先深入safeSetSuccess(promise);方法, 这个方法里面其实很简单CAS的方式设置futurePromise成功,并notify设置好的Listener监听器。下面是safeSetSuccess(promise);的源码剖析。
在这里完成Listener的回调,其实就是我们前面分析的拉起后续端口bind操作。也就是下面这段代码。如果注册的过程中没有出现异常,doBind0(regFuture, channel, localAddress, promise);即将bind任务加入到eventLoop的task队列中。
在核心注册方法register0()完成safeSetSuccess(promise);的调用之后,会执行pipeline.fireChannelRegistered()方法,这个方法的执行的操作和这个方法的名字一样,依次执行Channel的pipeline中的InboundHandler的ChannelRegistered() 方法。
通过上面这部分代码,我们就实现了顺着pipeline的channelHandler的调用链,依次执行合适的handler方法即这里的MASK_CHANNEL_REGISTERED所指代的channelRegistered()方法。到此fireChannelRegistered的代码执行逻辑原理我们也就分析完了。我们回到register0方法,简单看下主方法体的最后一段。如果此时的channel已经激活并且是第一次注册,需要调用pipeline.fireChannelActive(),顺着handler调用链依次执行channelActive(ctx)方法,其原理和pipeline.fireChannelRegistered()方法一致。
到这里register0的分析完成,我们initAndRegister注册和初始化部分的源码也算是剖析完成了。按照顺序我们应该继续分析启动流程的下一个部分bind端口绑定部分,但是我想再中间插入一个小菜,也是我在看源码中感觉设计精妙的点,这个点就是eventloop。之前我对eventloop的理解只是简单处理连接handler逻辑的线程池结构的组件。但是在启动过程中,我又发现Netty又充分利用eventloop的task队列进行Server的拉起。在开始bind绑定部分之前,我们先一起看看eventloop在这承上启下过程中发挥的作用。

EventLoop启动与Task任务队列

EventLoop在前面的我们有详细的剖析,但是没有实际的结合执行代码来分析。这个部分我们将结合启动过程中,eventloop通过task队列将启动过程中各个部分解耦拆分的过程,来深入理解eventloop在整个启动过程中所发挥的作用。同时深入我们对eventLoop这个模块的理解。我们回到代码本身来从头看看eventLoop是什么时候参与到Netty服务的启动的。

eventLoop的启动

我们在整个服务中多次提到了executor.inEventLoop()校验,即判断当前线程是否是eventLoop线程。按照我们代码的执行顺序,第一是出现在调用register0方法时候,由main线程触发。
因为这里是main线程触发,所以这里会通过调用eventLoop.execute(runnable)方法启动已经完成初始化的eventLoop。以下是详细的源码剖析。
通过这部分代码的执行,我们启动了一个初始化完成的eventloop,我们不难发现,eventLoop的启动并不是在addTask之前来调用特定的方法实现。而是整合到addTask的过程中,先将task加入队列中,然后判断当前线程是不是eventLoop线程,如果不是则启动eventLoop,否则跳过启动。最后再由后续的eventLoop的pollTask来执行task。这一点设计很巧妙。

task队列任务的执行

后面的逻辑就是我们前面剖析的NioEventLoop中的run方法剖析了,这里我们再简单的过一下,我们这次着重关注task队列任务的执行。
这段逻辑我们就不重复分析了,这段逻辑大致可以分为select轮询就绪KeyprocessSelectedKeys处理就绪keyrunAllTasks处理队列中的任务处理空轮询bug这几个部分,其中无论ioRatio的值为多少runAllTasks都会执行。这就保证了我们处于队列中的任务都会被执行。以下是runAllTasks()方法的详细剖析。
run()方法中除了runAllTasks()方法还有一个重载方法runAllTasks(long timeoutNanos)这个方法则是执行时间长度为 timeoutNanos 纳秒的runAllTask方法。实现的方法也很简单,每执行64个task检查下时间是否超时,如果没有超时继续执行,否则退出方法。如果runAllTasks(0)则是执行最小单位的runAllTask方法(即64个task)。

承上启下的task队列

以上就是eventLoop启动与task队列的源码剖析,而结合我们前面的initAndRegister部分的源码分析,我们知道我们将registeraddServerBootstrapAcceptorbind操作放入了task队列中。也就是下面这些代码片段。
在initAndRegister的部分,taskQueue中有以下的几个任务,并且这几个任务之间是有明确的先后顺序的。其中register部分是main线程加进去的,其余的都是由eventLoop线程加入到taskQueue中。
notion image
那么main线程是在什么时候退出的呢?main线程在执行完bind(PORT)方法之后,就阻塞在下面这段EchoServer中的这段f.channel().closeFuture().sync();代码上。后续的bindactive等操作都将以eventLoop的task的方式执行。

bind(端口绑定)和active操作源码剖析

上面我们分析了整个启动的流程是通过evetLoop的Task的方式执行,而registeraddAcceptorbind等操作都是作为一个个task任务加入到taskQueue中,最后在eventLoop的run方法中调用runAllTasks方法执行这些task任务。在前面的initAndRegister的解析中,我们分析到register方法返回了一个promise,我们在promise添加了一个Listener(监听器),监听器加入的是bind端口绑定的逻辑,并且这段逻辑是通过eventLoop的Task方式执行的。

bind端口绑定操作源码剖析

接下来,我们继续分析bind端口绑定操作的源码。整个流程前面pipeline.channelRegistered()是类似的,只不过channelRegistered()是inboundHandler里面的方法,而bind()是outBoundHandler里的方法。以下是bind部分的源码剖析。
其实,我们的bind操作和channelRegistered操作基本是类似的,只不过bind方法是outBoundHandler中的方法,outBoundHandler的方法不再想inBoundHandler里的方法一样从头到尾节点依次执行handler的方法,outBoundHandler执行顺序反过来,从尾节点开始向前遍历到头节点依次执行对应的handler方法
所以上面这段代码,从调用pipeline.bind(localAddress, promise);方法开始,从后向前依次调用outboundHandler的bind方法。以上的代码逻辑就是这个调用过程,方法调用最后的bind方法就是头节点即HeadContext中的额bind方法,这个方法里面的bind操作是调用unsafe方法,也就是我们的channel与端口的绑定,以及后续讲active操作添加到eventLoop的Task队列中。

active部分方法调用源码剖析

active部分并不是需要激活什么,本质上是调用pipelineChannelHandler的channelActive(ctx)方法,channelActivechannelRegistered一样都是inboundHandler里面的方法,因此调用过程和channelRegistered的过程一致。在前面的bind端口绑定部分的解析,我们看到bind方法内通过调用invokeLater方法,将pipeline.fireChannelActive();加入到eventLoop的task队列中。
因此我们从pipeline.fireChannelActive();方法开始继续我们的源码剖析。
结合上面分析的channelActive、bind和channelRegistered方法的调用,大体上都是相同的。我们再次结合复习下pipelineChannelHandler中的调用过程。
notion image
上面的图简单的表述了这个调用的代码实现过程,本质上channelActivebindchannelRegistered的调用过程本质上都是一样的, 唯一不同的点就是channelActive和channelRegistered是inboundHandler的方法,所以这两个方法的调用是从头向后依次向后执行即从headContext向tailContext执行。而bind方法是outboundHandler的方法,所以是执行顺序是反过来的,也就是从tailContext向前执行,即下面这张图的逻辑。
notion image
在完成了pipelineChannelHandler的channleActive的调用之后,Netty将开始启动过程的最后一步,对ACCEPT时间的监听。即调用readIfIsAutoRead();设置interestOpsOP_ACCEPT
完成上面代码的调用,Netty的服务也基本上启动完成即完成了服务的初始化,完成了服务基础初始化,将channelHandler加入到pipeline中,并调用handler的channelRegistered,bind,channelActive和read方法等。通过这些方法的调用,完成了channel的注册,端口的绑定,channelActive的调用和最后对Channel的Accpet事件的监听,整个过程中都依赖EventLoop的Task进行任务的调度执行。在上面这个调用过程中,我发现一个有意思的代码设计即ctx.executeMask的设计逻辑,这也是我们熟悉的二进制标识的应用。以下是这个部分的详细代码剖析。

一个小细节 ctx.executeMask

在看源码过程中,我翻了一些资料和一些博客,在之前的pipelineChannelHandler的版本中,都有标识当前handler是inBoundHandler还是outBoundHandler,但是在我看的这版本源码中并有有关的变量,那它是怎么标识,当前handler是inBound还是outBoundHandler的呢?在一条调用链上,当前handler是否包含了当前的方法呢?答案就是executeMask。但是我debug源码的时候,executeMask的值只是一个普通的常数啊,随着对源码的深入,我发现executMask使用二进制的方式标识重载方法,这又是一个使用二进制位标识状态的应用。我们一起深入源码看看,它是怎么玩的吧。
以上是在寻找下个合适的Handler时,使用executeMask进行handler的过滤,二进制的方式进行判断效率很高。下面这段代码则是构建ChannelContextHandler时对executeMask进行赋值的逻辑。
从上面的源码代码里面可以看到,netty也是用了比较”笨”的方式去生成了executeMask,生成的主要性能开销集中在启动阶段并且添加了一个简单的缓存,所以整体的性能还是很可观的。使用二进制标识多个状态的示例还是很多的例如对象头中的对象状态,有赞权限系统的权限标识等。这里的executeMask也是一个优秀的实现案例。

总结

这一小节,我们深入剖析了Netty的启动源码,我们从NIO服务的基本设计思路入手,分析了组成服务的一些基本要素。随后我们从ServerBootstrap开始,在创建ServerBootstrap阶段配置一些如eventLoop、handler等核心的参数。在最后的bind方法即绑定端口的方法开始真正的服务启动操作,启动的过程主要分为 initAndRegister初始化、bind端口绑定和active操作。其中,initAndRegister主要包括服务初始化、将handler加入到pipeline中、调用pipelineChannelHander的channelRegistered。bind端口绑定即将channel绑定相应的端口,最后就是active操作,active操作包括调用pipelineChannelHandler的channelActive方法和调用read方法完成OP_ACCEPT方法的监听。这些操作都是通过eventLoop的task方式实现。
在分析源码之前我们提出了下面两个问题,我并没有直接给出答案。看到这里,你是否已经有了答案呢?
NioServerSocketChannel的Handle管道DefaultChannelPipeline是如何添加Handler并触发各种事件的?
在服务启动的过程中,register、bind、active操作是通过EventLoopTask的形式执行的,那他们是如何保证有序的?
本来这一小节准备是准备I/O就绪操作一起梳理掉,但是这一小节洋洋洒洒已经写了这么多🤣,而且源码这种需要结合代码反复的理解才能消化吃透。因此我就准备把I/O就绪处理放到下一小节,下一小节,我们将开始对连接监听读写操作进行深入剖析。
Netty — API网关DemoNetty核心组件源码剖析 — ByteBuffer
Loading...
Honesty
Honesty
人道洛阳花似锦,偏我来时不逢春
最新发布
Java IO — NIO Buffer
2024-10-21
Java IO — NIO Channel
2024-10-21
Java IO — IO/NIO模型
2024-10-21
Java异步编程方式介绍
2024-10-21
Elasticsearch — 索引(Mapping Index)
2024-10-19
Elasticsearch — 如何存储数据并保持一致性?
2024-10-19