公司自研rpc框架,底层网络通信框架为netty;作为it小白,有必要学习rpc框架及对应的系统底层网络通信框架。前一篇文章初步了解nio内容,下面开始逐步学习netty源码内容。
内容参考https://www.jianshu.com/p/c5068caab217 中的部分内容。
使用版本为:


Netty 服务端创建的时序图,如下(摘自《Netty权威指南(第二版)》)

举例:服务端启动代码:

开启一个服务端,端口绑定在8888,使用nio模式,下面讲下每一个步骤的处理细节
EventLoopGroup :
是一个死循环,不停地检测IO事件,处理IO事件,执行任务,后面详细描述;初始化用于Acceptor的主"线程池"以及用于I/O工作的从"线程池";
ServerBootstrap :
初始化ServerBootstrap实例, 此实例是netty服务端应用开发的入口是服务端的一个启动辅助类,通过给他设置一系列参数来绑定端口启动服务;
.channel(NioServerSocketChannel.class) :
指定通道channel的类型,由于是服务端,故而是NioServerSocketChannel;表示服务端启动的是nio相关的channel,channel在netty里面是一大核心概念,可以理解为一条channel就是一个连接或者一个服务端bind动作;
b.childHandler(new NettyServerFilter()) 表表示一条新的连接进来之后,该怎么处理,NettyServerFilter代码如图中所示:

ChannelFuturef =b.bind(port).sync() 这里就是真正的启动过程了,绑定6789端口,等待服务器启动完毕,才会进入下行代码。
详细描述:
ServerBootstrap的各个参数的配置不再描述;直接跳入到bind()方法:

逐步step:

通过端口号创建一个 InetSocketAddress,然后继续step:

其中validate()方法用于验证服务启动需要的必要参数是否合格

如果参数合格,则调用dobind()

去掉细枝末节,让我们专注于核心方法,其实就两大核心一个是 initAndRegister(),以及doBind0();
initAndRegister() 联系到nio里面轮询器的注册,可能是把某个东西初始化好了之后注册到selector上面去,最后bind,像是在本地绑定端口号,带着这些猜测,我们深入下去
initAndRegister()

核心代码如图中红框部分;
1. new 一个channel,
2. init这个channel,即调用init(channel)初始化通道信息
3. 将这个channel register到某个对象。
后面逐一分析:
1. new 一个channel
final Channel channel = channelFactory().newChannel();
调用channelFactory生成通道channel实例,通过serverbootstrap的channel方法来指定通道类型,其实是调用基类AbstractBoostrap的channel方法,其内部其实是实例化了一个产生指定channel类型的channelFactory。
所以,initAndRegister中的channelFactory.newChannel()方法就是生成了一个NioServerSocketChannel的实例。
channel的定义,netty官方对channel的描述如下
A nexus to a network socket or a component which is capable of I/O operations such as read, write, connect, and bind
这里的channel,由于是在服务启动的时候创建,我们可以和普通Socket编程中的ServerSocket对应上,表示服务端绑定的时候经过的一条流水线。这条channel是通过一个 channelFactory new出来的,channelFactory 的接口很简单:

就一个方法,我们查看channelFactory被赋值的地方

在这里被赋值,我们层层回溯,查看该函数被调用的地方,发现最终是在这个函数channel()中,ChannelFactory被new出

demo程序调用channel(channelClass)方法的时候,传入参数

将channelClass作为BootstrapChannelFactory的构造函数创建一个BootstrapChannelFactory。
最终,在initAndRegister() 方法的开始阶段final Channel channel = channelFactory().newChannel();初始化创建channel是调用的BootstrapChannelFactory.newChannel()方法,即AbstractBootstrap类中的内部类BootstrapChannelFactory中的newChannel();

看到clazz.newInstance() 是通过反射的方式来创建一个对象,而这个clazz就是我们在ServerBootstrap中传入的NioServerSocketChannel.class
创建channel相当于调用默认构造函数new出一个 NioServerSocketChannel对象
下面分析 NioServerSocketChannel的默认构造函数:
无参构造方法中有两个关键点:
1、使用默认的多路复用器辅助类 DEFAULT_SELECTOR_PROVIDER


2、通过newSocket创建ServerSocketChannel

通过SelectorProvider.openServerSocketChannel()创建一条server端channel(后面会在AbstractNioChannel.java的构造函数中保存为成员变量),然后进入到以下方法,即:将newSocket生成的ServerSocketChannel对象继续传递给本类中的NioServerSocketChannel(ServerSocketChannel channel)构造方法

因为是服务端新生成的channel,第一个参数指定为null,表示没有父channel,
第二个参数指定为ServerSocketChannel,第三个参数指定ServerSocketChannel关心的事件类型为SelectionKey.OP_ACCEPT。
第一行代码就跑到父类里面去了;
第二行new出来一个 NioServerSocketChannelConfig,其顶层接口为 ChannelConfig,.
super(null, channel, SelectionKey.OP_ACCEPT);
追踪到 NioServerSocketChannel 的父类 : AbstractNioMessageChannel.java

继续追踪到父类:AbstractNioChannel.java

将前面 provider.openServerSocketChannel(); 创建出来的 ServerSocketChannel保存到成员变量ch
然后调用ch.configureBlocking(false);设置该channel为非阻塞模式
这里的 readInterestOp 即前面层层传入的 SelectionKey.OP_ACCEPT,接下来重点分析 super(parent);(这里的parent其实是null,由前面写死传入);
在AbstractNioChannel中做了下面几件事:
1、继续调用父类AbstractChannel(Channel parent)构造方法;
2、通过this.ch = ch 保存ServerSocketChannel, 因为NioServerSocketChannel是Netty封装的对象,而ServerSocketChannel是有前面默认selector_provider生成的,是java nio的, 其实“this.ch = ch”可以被认为是绑定java nio服务端通道至netty对象中;
3、设置ServerSocketChannel关心的事件类型;
4、设置ServerSocketChannel为非阻塞的(熟悉Java NIO的都知道如果不设置为false,启动多路复用器会报异
父类为AbstractChannel.java; 构造函数入切图:

此构造方法中,主要做了三件事:
1、给channel生成一个新的id
2、通过newUnsafe初始化channel的unsafe属性
3、pipeline =new DefaultChannelPipeline(this) 初始化channel的pipeline属性
此处:new出来三大组件,赋值到成员变量,分别为
1 this.parent = parent; 此时传入的是null
2 unsafe = newUnsafe();
在AbstractChannel类中,newUnsafe()是一个抽象方法

AbstractNioMessageChannel类中有newUnsafe()的实现

此方法返回一个NioMessageUnsafe实例对象,而NioMessageUnsafe是AbstractNioMessageChannel的内部类
NioMessageUnsafe 只覆盖了 父类AbstractNioUnsafe中的read方法,通过NioMessageUnsafe 及其父类的代码便可以知道, 其实unsafe对象是真正的负责底层channel的连接/读/写等操作的,unsafe就好比一个底层channel操作的代理对象。
OP_ACCEPT都已经注册上了,当接收到新用户连接时就会触发unsafe.read()方法。read()会不断调用doReadMessages(),将产生的readBuf逐一发送给Pipeline.fireChannelRead()去处理。
unsafe内容后续学习;
3 pipeline =new DefaultChannelPipeline(this);

初始化了HeadContext及TailContext对象。
head及tail初始化完成后,它们会相互连接。
通过上面的代码可以得出,pipeline就是一个双向链表。
回到NioServerSocketChannel的构造方法 NioServerSocketChannel(ServerSocketChannel channel)
config =newNioServerSocketChannelConfig(this, javaChannel().socket());
ioServerSocketChannelConfig是NioServerSocketChannel的内部类

而NioServerSocketChannelConfig 又是继承自DefaultServerSocketChannelConfig,通过代码分析,此config对象就是就会对底层ServerSocket一些配置设置行为的封装。
至此NioServerSocketChannel对象应该创建完成。
总结:
用户调用方法 Bootstrap.bind(port) 第一步就是通过反射的方式new一个NioServerSocketChannel对象,并且在new的过程中创建了一系列的核心组件,进一步研究:
1、NioServerSocketChannel对象内部绑定了Java NIO创建的ServerSocketChannel对象;
2、Netty中,每个channel都有一个unsafe对象,此对象封装了Java NIO底层channel的操作细节;
3、Netty中,每个channel都有一个pipeline对象,此对象就是一个双向链表;
NioServerSocketChannel的类继承结构图:

2.init这个channel
init(channel);

// 设置引导类配置的option
finalMap, Object> options = options0();
option: 设置通道的选项参数, 对于服务端而言就是ServerSocketChannel, 客户端而言就是SocketChannel;
// 设置引导类配置的attr,attr: 设置通道的属性;
attrs = attrs();
1. 上图这里先调用options0()以及attrs0(),然后将得到的options和attrs注入到channelConfig或者channel中

2. 上图代码第一行, 获取当前通道的pipeline,然后为 NioServerSocketChanne l绑定的 pipeline 添加 Handler;

3. 将用于服务端注册的 Handler ServerBootstrapAcceptor 添加到 ChannelPipeline 中。ServerBootstrapAcceptor 为一个接入器,专门接受新请求,把新的请求扔给某个事件循环器。对应到新进来连接对应的channel。
p.addLast()向serverChannel的流水线处理器中加入了一个 ServerBootstrapAcceptor,从名字上就可以看出来,这是一个接入器,专门接受新请求,把新的请求扔给某个事件循环器

3.将这个channel register到某个对象
ChannelFuture regFuture = group().register(channel);
调用到MultithreadEventLoopGroup的register;

然后调用到SingleThreadEventLoop中的register方法:


关键是:channel().unsafe().register(this, promise);
register跳转到AbstractUnsafe.java中的register


先将EventLoop事件循环器绑定到该NioServerSocketChannel上,然后调用 register0()

register0()方法定义了注册到EventLoop的整体框架,整个流程如下:
(1).注册的具体细节由doRegister()方法完成,子类中实现。
(2).注册后将处理业务逻辑的用户Handler添加到ChannelPipeline。fireChannelRegistered方法中实现。
invokeHandlerAddedIfNeeded()

(3).异步结果设置为成功,触发Channel的Registered事件。
(4).对于服务端接受的客户端连接,如果首次注册,触发Channel的Active事件,如果已设置autoRead,则调用read()开始读取数据。

上面的条件isActive() 方法, NioServerSocketChannel接受的Channel此时已被激活

在NioServerSocketChannel中重写此方法:

最终调用到jdk中,bound初始定义为false;oldImpl 标识为false



最终isBound返回false;
所以 isActive() 返回false;
如果isActive返回true(例如使用旧socket导致oldImpl为true)此时跳入到下列方法中:

跳转到AbstractChannelHandlerContext类中的 下列方法:

在 Outbound 事件(例如 Connect 事件)的传输过程中时, 我们也有类似的操作:
首先调用 findContextInbound, 从 Pipeline 的双向链表中中找到第一个属性 inbound 为真的 Context, 然后返回
调用这个 Context 的 invokeChannelActive

这个方法和 Outbound 的对应方法(例如 invokeConnect) 如出一辙. 同 Outbound 一样, 如果用户没有重写 channelActive 方法, 那么会调用 ChannelInboundHandlerAdapter 的 channelActive 方法,channelHandler相关内容后续学习。



isAutoRead方法默认返回true,于是进入到以下方法。


最终AbstractChannelHandlerContext中的read方法

findContextOutbound() 顾名思义, 它的作用是以当前 Context 为起点, 向 Pipeline 中的 Context 双向链表的前端寻找第一个 outbound属性为真的 Context(即关联着 ChannelOutboundHandler 的 Context), 然后返回.

详细分析后面学习。
总结,netty启动一个服务所经过的流程
1. 设置启动类参数,最重要的就是设置channel
2. 创建server对应的channel,创建各大组件,包括ChannelConfig,ChannelId,ChannelPipeline,ChannelHandler,Unsafe等
3. 初始化server对应的channel,设置一些attr,option,以及设置子channel的attr,option,给server的channel添加新channel接入器,并出发addHandler,register等事件:attr: 设置通道的属性;
4. 调用到jdk底层做端口绑定,并触发active事件,active触发的时候,真正做服务费端口绑定.
5. 在进行服务端开发时,必须通过ServerBootstrap引导类的channel方法来指定channel类型, channel方法的调用其实就是实例化了一个用于生成此channel类型对象的工厂对象。 并且在bind调用后,会调用此工厂对象来生成一个新channel。
Pipeline初始化过程分析
inbound: 本质上就是执行I/O线程将从外部read到的数据 传递给 业务线程的一个过程。
outbound: 本质上就是业务线程 将数据 传递给I/O线程, 直至发送给外部的一个过程。
过程如图:

channel中的pipeline其实就是DefaultChannelPipeline的实例

class DefaultChannelPipelineimplements ChannelPipeline
DefaultChannelPipelineimplements 实现了ChannelPipeline接口;
DefaultChannelPipelineimplements构造函数如图所示;初始化一个pipeline,此时定义tail head;

首先绑定channel对象,然后初始化头、尾上下文,然后头尾相互连接,形成双向链表。
head是HeadContext的实例,tail是TailContext的实例,HeadContext与TailContext都是DefaultChannelPipeline的内部类,它们的类继承结构图如下:
HeadContext类继承结构图

TailContext类继承结构图

从类继承图我们可以看出:
1、HeadContext与TailContext都是通道的handler(中文一般叫做处理器)
2、HeadContext可以用于outbound过程的handler
3、TailContext只可以用于inbound过程的handler
4、HeadContext 与 TailContext 同时也是一个处理器上下文对象
网友评论