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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容