Netty源码分析-05 Netty服务器启动过程

服务器的启动过程大量使用了EventLoop和Future/Promise,在阅读源码之前,建议首先要对Netty的这两种机制进行了解。由于Netty更多是在服务器端使用,因此以服务器的启动过程为例进行学习。

5.1 阶段:配置config

配置阶段的工作很简单,主要就是初始化启动类,设置相关参数。
Bootstrap启动类主要功能是初始化启动器,为启动器设置相关属性。我们先来看一下Bootstrap的类结构,启动类有一个AbstractBootstrap基类,有两个实现类Bootstrap和ServerBootstrap,分别用于客户端和服务器的启动。

AbstractBootstrap

属性

EventLoopGroup group; //线程组,对于ServerBootstrap来说,group为ServerSocketChannel服务
ChannelFactory<? extends C> channelFactory; //用于获取channel的工厂类
SocketAddress localAddress;//绑定的地址
Map<ChannelOption<?>, Object> options;//channel可设置的选项,包含java-channel和netty-channel
Map<AttributeKey<?>, Object> attrs;//channel属性,便于保存用户自定义数据
ChannelHandler handler;//Channel处理器

方法

group() //设置线程组
channelFactory()及channel() 设置channel工厂和channel类型
localAddress() 设置地址
option() 添加channel选项
attr() 添加属性
handler() 设置channelHander

上面这些方法主要用于设置启动器的相关参数,除此之外,还有一些启动时调用的方法

register() 内部调用initAndRegister() 用来初始化channel并注册到线程组
bind() 首先会调用initAndRegister(),之后绑定IP地址,使用Promise保证先initAndRegister()在bind()
initAndRegister(),主要是创建netty的channel,设置options和attrs,注册到线程组

ServerBootstrap

ServerBootstrap在AbstractBootstrap的基础上添加了如下属性,用来设置子Channel,也就是客户端连接后创建的Channel的属性。另外,还实现了抽象类中定义的init()方法。

 Map<ChannelOption<?>, Object> childOptions  子channel的配置
 Map<AttributeKey<?>, Object> childAttrs  子channel的属性
EventLoopGroup childGroup;    处理子channel的事件循环组
ChannelHandler childHandler;  处理子channel事件的handler

5.2 阶段:初始化init

初始化init阶段的主要功能是:创建并初始化服务器的Netty-Channel;分为两个步骤:创建和初始化。

创建NettyChannel

  • 使用SelectorProvider打开java通道
  • 为Channel分配全局唯一的ChannelID
  • 创建NioMessageUnsafe,用于netty底层的读写操作
  • 创建ChannelPipeline,默认的是DefaultChannelPipeline

下面是初始init阶段的主要代码:

Channel channel = null;
        try {
            channel = channelFactory.newChannel();// 创建NettyChannel
            init(channel);//初始化NettyChannel
        } catch (Throwable t) {
            if (channel != null) {
                // channel can be null if newChannel crashed (eg SocketException("too many open files"))
                channel.unsafe().closeForcibly();
            }
            // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
            return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
        }

channelFactory用于获取Channel实例,启动时,channelFactory在调用channel(NioServerSocketChannel.class)设置channel类型时创建,由于我们使用的是设置class的方法,会使用ReflectiveChannelFactory作为工厂类,其会直接调用class的newInstance获取Channel实例。Netty中,服务器端的Channel为NioServerSocketChannel,客户端为NioSocketChannel。

Channel的创建过程如下:

  1. 打开java通道:NioServerSocketChannel创建时,首先使用SelectorProvider的openServerSocketChannel打开服务器套接字通道。SelectorProvider是Java的NIO提供的抽象类,是选择器和可选择通道的服务提供者。具体的实现类有SelectorProviderImpl,EPollSelectorProvide,PollSelectorProvider。选择器的主要工作是根据操作系统类型和版本选择合适的Provider:如果LInux内核版本>=2.6则,具体的SelectorProvider为EPollSelectorProvider,否则为默认的PollSelectorProvider。至此,底层的Java ServerSocketChannel创建完毕。
public NioServerSocketChannel() {
        this(newSocket(DEFAULT_SELECTOR_PROVIDER));
    }
 private static ServerSocketChannel newSocket(SelectorProvider provider) {
        return provider.openServerSocketChannel();
    }
  1. Java ServerSocketChannel创建完毕后,会进入netty-Channel的构造方法,首先初始化ChannelId,ChannelId是一个全局唯一的值;
  2. 之后,创建NioMessageUnsafe实例,该类为Channel提供了用于完成网络通讯相关的底层操作,如connect(),read(),register(),bind(),close()等;
  3. 为Channel创建DefaultChannelPipeline,初始化双向链表;
  4. 讲java-channel设置为非阻塞,将关注的操作设置为SelectionKey.OP_ACCEPT(服务器)
 protected DefaultChannelPipeline(Channel channel) {
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        succeededFuture = new SucceededChannelFuture(channel, null);
        voidPromise =  new VoidChannelPromise(channel, true);

        // 初始化双向链表
        tail = new TailContext(this); // 创建head 
        head = new HeadContext(this); // 创建tail

        head.next = tail;
        tail.prev = head;
    }

初始化NettyChannel

创建NettyChannel后,下一步需要进行初始化,由于服务器端和客户端的Channel不一样,因此init方法被分别实现到了ServerBootstrap和Bootstrap中,我们主要分析服务器的init。服务器的init分为几个步骤:

  • 将启动器设置的选项和属性设置到NettyChannel上面
  • 向Pipeline添加初始化Handler,供注册后使用

具体实现在ServerBootstrap类的init方法中,程序比较简单。每个NettyChannel对象保护一个ChannelConfig类保存相关配置,还有Map<AttributeKey<?>, Object> attrs用来保存自定义属性。至于初始化Handler,我们先记住,在bind中会说明其作用。

在addLast时,由于还未注册,因此会加入到Pipeline的一个等待链表中,待注册后执行。

if (!registered) {
    newCtx.setAddPending();
    callHandlerCallbackLater(newCtx, true);
    return this;
}

总结一下,这个阶段的代码我们可以看出,channel内部包含几个重要对象:

ChannelID 全局唯一ID
ChannelConfig  保存配置
ChannelPipeline  通道的流水线
Unsafe  Netty底层封装的网络I/O操作

5.3 阶段:注册register

这个阶段的主要工作是将创建并初始化后的NettyChannel注册到selector上面。具体过程:

  • 将打开NettyChannel注册到线程池组的selector上;
  • 触发Pipeline上面ChannelHandler的channelRegistered,
// AbstractBootstrap类 initAndRegister()
 ChannelFuture regFuture = config().group().register(channel);

上面的程序会使用传入的线程池组的register(channel);注册NettyChannel,具体方法定义在SingleThreadEventLoop中,其会使用NettyChannel的unsafe的register方法,该方法首先会判断当前线程是否是指定线程池正在运行的线程,如果不是提交到要注册的线程池中执行。执行时调用下面的程序。

// AbstractUnsafe,删去了部分校验代码
private void register0(ChannelPromise promise) {
            try {
                boolean firstRegistration = neverRegistered;// 是否为首次注册
                doRegister(); //  1. 注册
                neverRegistered = false;
                registered = true;
                pipeline.invokeHandlerAddedIfNeeded();// 2. 将注册之前加入的handler加入进来
                safeSetSuccess(promise); // 注册成功,通知promise
                pipeline.fireChannelRegistered();// 4. Pipeline通知触发注册成功
      
                if (isActive()) { // 是否已经绑定 因为register和bind阶段是异步的
                    if (firstRegistration) { 
                        pipeline.fireChannelActive(); // 5.首次注册,通知
                    } else if (config().isAutoRead()) {// Channel会deregister后重新注册到线程组时,且配置了AutoRead
                        beginRead();
                    }
                }
            } catch (Throwable t) {
                closeForcibly();
                closeFuture.setClosed();
                safeSetFailure(promise, t);
            }
        }
  1. 注册:将NettyChannel内部的javaChannel注册到线程池的selector上面,由线程池不断执行select()查询准备就绪的文件描述符。具体实现在AbstractNioChannel中的doRegister()
  2. invokeHandlerAddedIfNeeded: 注册成功后,找到初始化阶段通过pipeline.addLast()加入的ChannelInitializer,执行其ChannelInitializer的initChannel方法,之后将其删除(在ChannelInitializer的initChannel方法中);初始化NettyChannel阶段,我们addLast了一个初始化Handler,现在来看看其作用
// init初始化阶段添加了一个ChannelInitializer
 p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler();// 获取config时设置的handler
                if (handler != null) {
                    pipeline.addLast(handler); // 将其添加到链表尾部
                }
                 // 加入一个ServerBootstrapAcceptor处理器,用于处理Accept
                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });

从上面的程序中可以看到,由于初始化时,还未将javaChannel注册到线程池的selector上,此时还无法设置Channel将Accept注册到选择器上,因此先加入了一个ChannelInitializer,等待register后向Pipeline加入ServerBootstrapAcceptor。此时,NettyChannel的Pipeline的链表结构为:

Head <-->  InitialHandler  <--> ServerBootstrapAcceptor <--> Tail

在initChannel执行的最后会将InitialHandler从Pipeline移除,此时NioServerSocketChannel的链表结构为

Head   <--> ServerBootstrapAcceptor <--> Tail
  1. fireChannelRegistered,沿着pipeline的head到tail,调用ChannelHandler的channelRegistered方法,
public final ChannelPipeline fireChannelRegistered() {
        AbstractChannelHandlerContext.invokeChannelRegistered(head);
        return this;
    }
    
private void invokeChannelRegistered() {
        if (invokeHandler()) { // 状态是否正确
            try {
                ((ChannelInboundHandler) handler()).channelRegistered(this); // 触发
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireChannelRegistered();// 状态不正确,通知下一个Handler
        }
    }
  1. fireChannelActive 由于注册阶段和绑定bind阶段都是异步的,如果此时注册完成时bind阶段已经绑定了本地端口,会沿着pipeline的head到tail,调用各个Handler的channelActive方法

5.4 阶段:绑定bind

本阶段的主要内容是:将NettyChannel内部的java的ServerSocketChannel绑定到本地的端口上面,结束后使用fireChannelActive通知Pipeline里的ChannelHandle,执行其channelActive方法。

bind的入口为AbstractBootstrap的doBind0(),内部会调用pipeline中的bind方法,逻辑为从tail出发,调用outbound的ChannelHandler的bind方法,从上面我们可以看到当前的链表如下:

Head[I/O] <-->  ServerBootstrapAcceptor[IN] <--> Tail[IN]

只有Head可以用来处理Outbound,Head的bind方法调用了channel创建过程中生成的unsafe对象NioMessageUnsafe的实例,该实例的bind方法首先java的channel bind本地地址,然后触发fireChannelActive。

public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
            boolean wasActive = isActive();
            try {
                doBind(localAddress);
            } catch (Throwable t) {
                safeSetFailure(promise, t);
                closeIfClosed();
                return;
            }

            if (!wasActive && isActive()) {
                invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.fireChannelActive();
                    }
                });
            }
            safeSetSuccess(promise);
        }

至此,Netty的服务器段已经启动,Channel和ChannelPipeline已经建立。EventLoop也在不断的select()查找准备好的I/O。

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

推荐阅读更多精彩内容