原来 Netty 的核心启动逻辑是这样的!

开发 前端
bind 的很多方法都是异步执行的,所以有些流程是主线程执行,有些是 eventloop 执行的,这个需要注意下,不然会感觉有点乱。

你好,我是yes。

上篇我们已经了解了 Netty 的启动流程,还剩一个 bind 方法没有细讲,这篇我们就着重的说下 bind 方法,这个方法也是触发 Netty 真正启动的方法。

先打个预防针,源码也不是那么简单的,有时候看着有点绕,如果你想面试的时候胸有成竹,还是得有点耐心的,如果中间没看懂没事,最后我有总结,看完总结之后应该会清晰的。

对了,如果有条件的话,建议在电脑上看这篇文章,会更加舒适。

好了,开局先来一张图,bind 的核心操作就如下图所示,下面长篇的源码分析也是为了说清楚这个流程,所以什么类名,方法名都不重要,重要的是知晓整体流程:

注意,上图的 Channel 指的是 ServerChannel。

bind 的很多方法都是异步执行的,所以有些流程是主线程执行,有些是 eventloop 执行的,这个需要注意下,不然会感觉有点乱。

先看看这张图大体有个印象,然后我们开始盘 bind 方法~

bind 流程

从 bind 方法进入,实际上就会调用父类 AbstractBootstrap#doBind。

我们来简单的看下 doBind 这个方法,要点我都用文字标明了:

可以看到,这个方法主要做了下面这四件事:

  • 创建 channel
  • 初始化 channel
  • 绑定 channel 至 group 内的某个 eventLoop 上
  • 绑定端口

下面我会逐一的分析。

创建 channel

先来看下 initAndRegister 方法。

从下面的源码可以看到,这个方法主要就是创建一个 Channel 对象,然后初始化 Channel,最后将它注册到 eventLoop 上。

channelFactory.newChannel() 是利用反射得到一个 Channel 对象。还记得我们构建 ServerBootstrap 时候设置的 channel 类型吗:.channel(NioServerSocketChannel.class)

通过传入的这个 class 就可以得到构造器,然后调用 newInstance 即可得到一个对象。

这样就创建了一个 NioServerSocketChannel 对象。

通过继承链,我们可以发现 NioServerSocketChannel 会调用父类 AbstractChannel 的构造器,而在这里就会创建三个重要的东西:

  • id,channel 的标识
  • unsafe,用来操作底层数据读写
  • pipeline,handler的编排

所以,从这里可以得知,创建一个 Channel 配套就会新建一个 pipeline,即每个 Channel 都有自己的 pipeline。

初始化 channel

创建完 Channel 的操作之后,紧接着就初始化 Channel ,即 init() 方法。

可以看到,初始化首先就是把之前在 ServerBootstrap 时配置的 option 和 attr 塞到已经创建的 ServerSocketChannel 中,这个很好理解。

然后往 ServerSocketChannel 的 pipeline 中塞入一个 ChannelInitializer。

那 ChannelInitializer 是个什么玩意?它其实是一个抽象类,且是一个入站的 handler。

不过它是一个特殊的 ChannelHandler ,从 ChannelInitializer 类的注释就知道:

它的使命就是简化注册完毕后的初始化操作,可以理解为是一个中转的 handler,一个工具类。它在完成初始化动作之后会从 pipeline 中被移除。

所以,利用 ChannelInitializer 将初始化逻辑封装起来,当 channel 注册到 eventLoop 中之后会触发事件,然后就会调用 ChannelInitializer#initChannel 来执行初始化,仅此而已。

我们可以看到,上面 initChannel 逻辑是先添加之前配置给 ServerSocketChannel 的 handler,在我们的 helloworld 示例中就是添加 LoggingHandler。

然后再异步添加一个 ServerBootstrapAcceptor 这个 handler,从名字来看,这个 handler 主要是接收处理新连接。

小贴士:此时的 initChannel 逻辑还未执行,是要等到后面事件触发了才会执行,且执行的线程是 eventLoop 线程。

好了,看到这肯定有人会有疑问,为什么 initChannel 里面要异步添加 ServerBootstrapAcceptor?

为什么要异步添加 ServerBootstrapAcceptor?不直接添加?

其实源码注释说明的很清楚(上面为了清晰结构,把注释都删了)

简单翻译下,就是用户可能会用 ChannelInitializer 来设置 ServerSocketChannel 的 handler ,注意是ServerSocketChannel 的 handler,不是那个 childHandler 哈。

来看下示例代码:

  1. // 这样是没问题的,不用异步添加 ServerBootstrapAcceptor 
  2.  ServerBootstrap sb = new ServerBootstrap(); 
  3.     sb.channel(...).group(...).childHandler(...).handler(ourHandler); 
  4.      
  5. //这样的就需要异步添加 ServerBootstrapAcceptor 
  6. ServerBootstrap sb = new ServerBootstrap(); 
  7. sb.channel(...).group(...).childHandler(...).handler( 
  8.     new ChannelInitializer<Channel>() { 
  9.         @Override 
  10.         protected void initChannel(Channel ch) throws Exception { 
  11.             ChannelPipeline pipeline = ch.pipeline(); 
  12.             pipeline.addLast(ourHandler); 
  13.         } 
  14.     } 
  15. ); 

因为在利用 ChannelInitializer 设置 handler 的情况下,initChannel(…)方法只会在该方法(init 内添加ServerBootstrapAcceptor的方法)返回后被调用。

因此,需要确保以延迟的方式添加,使得用户定义的 handler 都放在 ServerBootstrapAcceptor 前面。

简单地说,就是让 ServerBootstrapAcceptor 成为 ServerSocketChannel 的 pipeline 中最后一个 inboundHandler,这样用户定义的 handler 逻辑才会被调用到。

因为当事件传递到 ServerBootstrapAcceptor 过就不会再继续通过 pipeline 传递了,会将接待的子 channel 直接分配给 workerGroup了,如果用户自定义的 handler 在 ServerBootstrapAcceptor 后面的话,里面的逻辑是不会被执行的,等于白加。

不理解的可以多读几遍上面的话哈,有一点点小绕。

都说到这了,那就来看看 ServerBootstrapAcceptor 的内部逻辑吧。

ServerBootstrapAcceptor 的内部逻辑

很简单,就是根据 selector 得到新连接对应的 channel(子channel),然后为其配置之前(初始化ServerBootstrap时)设置的 childhandler、childoption、childattr,紧接着从 workerGroup 中选择一个 eventLoop ,将 channel 注册到这个 eventLoop 上:

这样,新建的子 channel 之后的所有事件(读、写等 I/O 事件),都由从 workerGroup 中选定的那个 eventLoop 负责。

至此,我们讲完了 init(channel) 的操作。

channel 注册至 eventLoop

创建且初始化完 channel 之后,就需要把已经准备完毕的 channel 注册到一个 eventLoop 中。

即上面的ChannelFuture regFuture = config().group().register(channel);(从返回值可以得知这是一个异步执行流程)

这个动作就是从 bossGroup 中选一个 EventLoop ,然后将 channel 注册到选定的 EventLoop 上。

这个 next() 实际就是调用我们之前提到的 chooser 来选择一个 eventLoop,最终会将此 eventLoop 传递到 AbstractUnsafe#register 中,执行注册逻辑,核心就在这个 register0 方法。

可以看到,无论如何都是由 eventLoop 线程来执行 register0 操作(所以对主线程而言,这是异步的)。

我们来看下 register0 都做了什么事:

  • 调用底层接口,将 channel 注册到 selector 上
  • 触发之前配置的 ChannelInitializer#initChannel
  • 异步添加绑定端口的任务到 eventLoop 中
  • 触发 Registered 事件,使之在 pipeline 上传递

我们先看第一步 doRegister,看看具体是怎么注册 Channel 至 Selector 上的。

因为我们都知道 ServerSocketChannel 是 Netty 定义的类,和 JDK 没任何关系,那如何与 JDK 的类适配呢?到底是如何注册到 JDK 的 Selector 上的呢?

看到我圈起来的地方没,实际会把之前创建的 JDK 的 Channel 注册到 Selector 上,而 Netty 的 Channel 会作为一个 attachment 绑定到 JDK 的 Channel 上。

这样一来,每次 Selector 的时候,如对应的 JDK Channel 有事件发生,则 Netty 都可以从返回的 JDK Channel 的 attachment 中获取自己的 Channel,然后触发后续的逻辑,这招叫移花接木。

然后再来看第二步 pipeline.invokeHandlerAddedIfNeeded()。

此时才会调用 ChannelInitializer#initChannel 来添加 handler,且异步提交了一个添加 ServerBootstrapAcceptor 的任务,随后将 ChannelInitializer 从 pipeline 中移除。

紧接着就是触发 Registered 事件,这个事件会随着 pipeline 传播至所有 handler,只要是入站的 handler,且实现了下面这个方法就会被调用,所以如果你想在注册完毕之后做一些逻辑处理,那么就可以创建一个 handler 且实现 channelRegistered 方法。

好了,至此我们终于把 Channel 的创建、初始化、注册给说完了,后面就是绑定的端口的操作了。

绑定端口

绑定端口主要有两个事情需要关注下,一个是调用底层 JDK 绑定端口的实现,二是绑定完之后触发 active 事件,表明 channel 一切都已就绪。

底层 JDK Channel 的 bind 方法,我就不说了,这个触发 active 事件比较关键,最终会触发 AbstractNioChannel#doBeginRead,注册 accept 事件,这样 ServerSocketChannel 感兴趣事件也注册完毕,可以干活了!

好了,现在我们再来看下这张图,来对整体的过程捋一捋,至于上面的那些代码理不清没事,你只要看懂下面这种图,知晓大体的流程就够了:

现在看这张图是不是有不一样的感觉?其实Netty 服务端的启动流程也就这么回事儿,我再用语言组织一下:

  • 创建 ServerSocketChannel(配套指定一个ID、底层操作 unsafe对象、pipeline)
  • 将配置的option、attr设置在创建的 ServerSocketChannel 上
  • 添加一个 ChannelInitializer 至 ServerSocketChannel 的 pipeline 中
  • 从 bossGroup 中选择一个 eventLoop,将 ServerSocketChannel 与之绑定,且之后生命周期内的操作都由这个 eventLoop 执行
  • 触发第三步添加的 ChannelInitializer 的 initChannel 方法,将用户配置的 handler 添加到 ServerSocketChannel 的 pipeline 中,且由于需要保证框架内置的 ServerBootstrapAcceptor 这个 handler 处于 inboundhandler 的最后一个,因此是异步添加。
  • 触发 registered 事件,使之在 pipeline 上传播
  • 调用 JDK 底层方法,绑定端口
  • 触发 active 事件,注册 ServerSocketChannel 感兴趣的事件(OP_ACCEPT),用于接收新连接
  • over

最后

好了,我相信通过最后一张图和最后一段话,你应该大致了解了整个 Netty 的启动流程。

如果不明白的话再看看图,最后是在电脑上看哈,看着代码应该不难理解的,重点就是分清上面的 Channel 指的是 Server 端的 Channel,然后主线程和 eventLoop 线程的执行逻辑,有条件的建议自己打断点试试。

下篇我们将谈谈 Netty 的 Reactor 模型,这玩意面试被问的几率好像挺大,如果你还不清楚,没事,下篇我们慢慢盘!

 

我是 yes,从一点点到亿点点,我们下篇见~

 

责任编辑:武晓燕 来源: yes的练级攻略
相关推荐

2022-05-09 08:37:43

IO模型Java

2020-06-08 17:35:27

Redis集群互联网

2022-12-14 07:32:40

InnoDBMySQL引擎

2009-03-11 14:42:57

面试求职案例

2023-05-08 07:52:29

JSXReactHooks

2021-11-10 09:45:06

Lambda表达式语言

2020-05-26 08:52:36

Java JVM多态

2017-01-16 13:34:21

2023-05-22 15:58:11

2022-05-05 08:55:12

工业物联网IIoT

2024-02-06 09:30:25

Figma矩形矩形物理属性

2020-11-24 06:20:02

Linux日志文件系统

2017-01-05 15:07:33

2018-04-02 15:13:21

网络

2023-02-15 08:17:38

2016-10-12 08:54:24

2021-08-17 07:00:00

双重检查锁Nacos

2020-09-24 11:38:52

Java开发代码

2022-05-31 08:04:23

TSdeclareTypeScript

2019-12-18 14:41:07

Redis数据结构
点赞
收藏

51CTO技术栈公众号