美文网首页
Netty源码学习(2)--服务器启动

Netty源码学习(2)--服务器启动

作者: 未名枯草 | 来源:发表于2018-01-08 20:34 被阅读95次

    公司自研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 同时也是一个处理器上下文对象

    相关文章

      网友评论

          本文标题:Netty源码学习(2)--服务器启动

          本文链接:https://www.haomeiwen.com/subject/beivnxtx.html