你好,我是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 哈。
来看下示例代码:
- // 这样是没问题的,不用异步添加 ServerBootstrapAcceptor
- ServerBootstrap sb = new ServerBootstrap();
- sb.channel(...).group(...).childHandler(...).handler(ourHandler);
-
- //这样的就需要异步添加 ServerBootstrapAcceptor
- ServerBootstrap sb = new ServerBootstrap();
- sb.channel(...).group(...).childHandler(...).handler(
- new ChannelInitializer
() { - @Override
- protected void initChannel(Channel ch) throws Exception {
- ChannelPipeline pipeline = ch.pipeline();
- pipeline.addLast(ourHandler);
- }
- }
- );
因为在利用 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,从一点点到亿点点,我们下篇见~