Netty源码之Channel初始化和注册过程

上文深入研究netty框架之Channel和Unsafe主要介绍了Channel和Unsafe功能,本文主要结合源码分析Netty Channel的nio实现。通过从客户端的connect开始,并围绕客户端NioSocketChannel,详细分析Channel的初始化和注册过程。其中涉及到Channel的源码将会做深入分析,涉及Netty线程模型的部分源码不会做深入探究,这部分内容会在后续文章中体现。

客户端connect

创建Netty客户端时,需首先创建引导类Bootstrap的实例,并配置EventLoopGroup实例、Channel类型、TCP参数、初始化handler实例等等,最后调用Bootstrap的connect方法开始连接服务端,连接过程中将会执行很多初始化操作,如创建Channel实例,Channel的初始化和注册等等。下面先看看常见客户端实现的代码:

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.TCP_NODELAY, true)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     p.addLast(new EchoClientHandler());
                 }
             });
            ChannelFuture f = b.connect(HOST, PORT).sync();
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }

由这段代码即可知,Bootstrap采用了典型的Builder模式构造对象,首先创建一个空实例,然后调用方法设置Bootstrap的必要属性。需注意,创建Bootstrap空实例后,必须设置以下属性:

  • 必须调用group方法设置EventLoopGroup实例group,用于后期注册Channel到EventLoop;
  • 调用channel方法设置Channel类型,该方法会初始化ChannelFactory实例,用于后期创建Channel实例;
  • 调用handler设置ChannelInitializer实例,用于添加自定义Handler。

下面从connect方法开始分析:

    public ChannelFuture connect(SocketAddress remoteAddress) {
        if (remoteAddress == null) {
            throw new NullPointerException("remoteAddress");
        }
        validate();
        return doResolveAndConnect(remoteAddress, config.localAddress());
    }

读者可查阅validate源码实现,validate会校验Bootstrap中设置的几个必要属性,若validate校验通过,调用doResolveAndConnect进行解析并创建连接,下面看实现:

private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.isDone()) {
        if (!regFuture.isSuccess()) {
            return regFuture;
        }
        return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
    } else {
        final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                //future.cause()会读取result对象,该对象被声明为volatile,这里读出来缓存在局部变量中,
                //主要是为了防止重复读取volatile对象导致性能损失
                Throwable cause = future.cause();
                if (cause != null) {
                    promise.setFailure(cause);
                } else {
                    promise.registered();
                    doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
                }
            }
        });
        return promise;
    }
}

doResolveAndConnect主要分为以下几步:

  1. doResolveAndConnect首先调用initAndRegister完成Channel的初始化和注册;
  2. 若ChannelFuture的isDone返回true,且isSuccess返回true,调用doResolveAndConnect0执行实际的地址解析和连接操作;isSuccess返回false则直接返回regFuture;
  3. 若ChannelFuture的isDone返回false,表明注册过程未完成,创建promise,并在regFuture上添加监听器ChannelFutureListener,待完成后回调。若注册过程中无异常抛出,说明initAndRegister执行成功,在回调中调用doResolveAndConnect0。

注意点:

当ChannelFuture的isDone返回false时,表示Channel的初始化与注册过程还未完成,不能直接调用doResolveAndConnect0执行地址解析和连接操作,需要等待Channel初始化和注册完成;这时进入else代码块,else中创建了PendingRegistrationPromise的实例,同时在regFuture中添加了监听器用于完成后回调。那么为什么需要创建一个PendingRegistrationPromise的实例呢?作用是什么?鉴于篇幅,关于该问题读者可移步Netty源码之ChannelPromise回调通知问题,其中有详细的解答。

无论执行if还是else,都会创建一个新的Promise实例用于建立连接后通知客户端,if中使用channel.newPromise()创建DefaultChannelPromise的实例,由于这里调用DefaultChannelPromise的构造方法没有传入EventExecutor实例,因此默认会使用I/O线程作为executor。

Chanenl初始化与注册

先看initAndRegister源码:

    final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
            channel = channelFactory.newChannel();
            init(channel);
        } catch (Throwable t) {
            if (channel != null) {
                channel.unsafe().closeForcibly();
            }
            return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
        }

        ChannelFuture regFuture = config().group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }

        /** 如果程序到这里,说明promise没有失败,可能发生以下情况之一 
         * 1) 如果尝试将Channel注册到EventLoop,且此时注册已经完成;inEventLoop返回true,channel已经成功注
         *    册,可以安全调用bind() or connect()
         * 2) 如果尝试注册到另一个线程,即inEventLoop返回false,则此时register请求已成功添加到事件循环的任务队
         *    列中,现在同样可以尝试bind()或connect(),因为bind()或connect()会被调度在执行register 
         *    Task之后执行, 因为register(),bind()和connect()都被绑定到同一个I/O线程。
         */
        return regFuture;
    }
  • 首先使用工厂类ChannelFactory的newChannel通过反射创建Channel实例;
  • 调用init方法执行初始化操作;
  • 若在创建实例和初始化期间抛出异常,创建DefaultChannelPromise实例,写入异常并返回;
  • 调用EventLoopGroup的register方法,完成注册操作
  • regFuture返回失败,则需要关闭channel。

注意点:

  1. 调用newChannel和init抛出异常时,需要判断channel是否为空,因为当调用newChannel崩溃时,channel可能为null,主要原因可能是打开文件描述符fd过多,如抛出异常SocketException("too many open files");
  2. 调用newChannel和init抛出异常时,由于Channel还未注册到特定的EventLoop,因此创建DefaultChannelPromise实例时,需要强制使用GlobalEventExecutor作为promise的执行器。GlobalEventExecutor是一种单线程单例执行器,它会自动启动,并且当任务队列中没有任务时挂起1秒钟后停止,该执行者无法调度大量任务,更多细节这里不再展开。

下面分步介绍initAndRegister:

创建Channel实例

主要通过反射的手段,使用ChannelFactory的newChannel调用指定Channel的默认构造方法创建Channel实例,由于客户端Bootstrap实例调用channel方法时传入的Channel类型是NioSocketChannel,因此这里将调用NioSocketChannel的默认构造方法,先看看构造方法调用链路:

图中仅表述各构造方法的调用顺序,执行顺序正好相反,最先从AbstractChannel构造方法开始执行。构造方法调用顺序说明:

  • 首先,默认构造方法中传入默认的SelectorProvider实例调用带provider参数的构造方法;
  • NioSocketChannel(SelectorProvider provider)会调用newSocket来打开一个新的Java NIO SocketChannel,底层依赖java NIO的实现,然后使用SocketChannel实例作为参数调用带SocketChannel参数的构造方法;
  • NioSocketChannel(SocketChannel socket)中使用null作为当前Channe的parent,调用带parent和SocketChannel参数的构造方法;
  • NioSocketChannel(Channel parent, SocketChannel socket)构造方法首先会调用父类构造方法AbstractNioByteChannel(Channel parent, SelectableChannel ch)->AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp)->AbstractChannel(Channel parent)。

最终调用的最上层构造器是AbstractChannel,因此创建Channel对象由此开始:

  • AbstractChannel(Channel parent)首先初始化Channel的parent,id属性;然后创建unsafe实例,并初始化Channel的unsafe属性,这里newUnsafe的创建的是NioSocketChannelUnsafe的实例,主要用于后续执行I/O相关的操作;最后创建每个Channel关联的pipeline实例;
    protected AbstractChannel(Channel parent) {
        this.parent = parent;
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }
  • 然后进入AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp)构造方法。AbstractNioChannel中持有SelectableChannel的实例,主要用于将Netty中NioSocketChannel相关操作转发给Java NIO中SocketChannel相关的操作,因此这里将NioSocketChannel构造方法中创建的SocketChannel实例赋值给ch。SelectableChannel是java实现NIO的默认Channel类型,它一个可通过选择器复用的Channel,由于SocketChannel同时继承自SelectableChannel,因此SocketChannel的实例即是SelectableChannel的实例。此外,当前构造方法中还会设置当前Channel感兴趣的事件类型readInterestOp, 同时配置channel为非阻塞模式。Java NIO实现中,必须配置Channel为非阻塞模式。
   protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
        super(parent);
        this.ch = ch;
        this.readInterestOp = readInterestOp;
        try {
            ch.configureBlocking(false);
        } catch (IOException e) {
            try {
                ch.close();
            } catch (IOException e2) {
                if (logger.isWarnEnabled()) {
                    logger.warn(
                            "Failed to close a partially initialized socket.", e2);
                }
            }
            throw new ChannelException("Failed to enter non-blocking mode.", e);
        }
    }
  • 最后进入AbstractNioByteChannel(Channel parent, SelectableChannel ch)的构造方法中,传入的readInterestOp为SelectionKey.OP_READ,表示选择器需要关注Channel的可读事件。最后,进入NioSocketChannel构造方法中。
  • super返回后,最后在NioSocketChannel(Channel parent, SocketChannel socket)中初始化Channel的config实例,到此Channel创建完成。
Channel初始化

创建为Channel实例后,将调用Bootstrap的init方法初始化Channel,下面看实现:

    void init(Channel channel) throws Exception {
        ChannelPipeline p = channel.pipeline();
        p.addLast(config.handler());
        final Map<ChannelOption<?>, Object> options = options0();
        synchronized (options) {
            setChannelOptions(channel, options, logger);
        }
        final Map<AttributeKey<?>, Object> attrs = attrs0();
        synchronized (attrs) {
            for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
                channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
            }
        }
    }

由init实现可知,首先会添加ChannelInitializer实例到pipeline中,这里由于Channel还未注册完成,因此不会真正添加到pipeline,需等待Channel注册完成后通知,ChannelInitializer的添加过程可阅读文章Netty源码之ChannelPipeline和ChannelHandlerContext;然后配置Channel的可选参数,options0()方法会返回通过Bootstrap的option方法配置的参数,主要包含一些TCP相关参数,这些参数将通过Channel的Config实例配置到底层Socket上;最后,添加Channel的可选attr属性,attrs0方法会返回通过Bootstrap的attr方法配置的参数,其中key的类型为AttributeKey。由于Channel支持并发访问,因此配置Channel属性时使用synchronized保证线程安全。

Channel注册

创建Channel实例和初始化完成之后,需要调用register注册Channel。过程如下:

首先调用group返回初始化Boostrap创建的NioEventLoopGroup实例,调用它的register方法,实现在父类MultithreadEventLoopGroup中,看源码:

    public ChannelFuture register(Channel channel) {
        return next().register(channel);
    }

next()用于从MultithreadEventLoopGroup中选择一个单线程的NioEventLoop实例,并调用它的register方法,(这里不对next做深入分析,后续在Netty线程模型中介绍),进入SingleThreadEventLoop register源码:

    public ChannelFuture register(Channel channel) {
        return register(new DefaultChannelPromise(channel, this));
    }
    public ChannelFuture register(final ChannelPromise promise) {
        ObjectUtil.checkNotNull(promise, "promise");
        promise.channel().unsafe().register(this, promise);
        return promise;
    }

由源码可知,最终调用了unsafe中的register方法,默认实现参考AbstractUnsafe源码:

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    if (eventLoop == null) {
        throw new NullPointerException("eventLoop");
    }
    if (isRegistered()) {
        promise.setFailure(new IllegalStateException("registered to an event loop already"));
        return;
    }
    if (!isCompatible(eventLoop)) {
        promise.setFailure(
                new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
        return;
    }
    AbstractChannel.this.eventLoop = eventLoop;
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();//最终调用NioSocketChannel中的doClose方法关闭channel
            closeFuture.setClosed();//表明Channel已被关闭
            safeSetFailure(promise, t);
        }
    }
}
  • 首先,参数校验,检查eventLoop是否为空、当前Channel是否已经注册过、当前eventLoop是否为NioEventLoop的实例。不同I/O类型使用不同的EventLoop,如Nio使用NioEventLoop,Oio使用ThreadPerChannelEventLoop;
  • 然后将eventLoop绑定到当前Channel,以后Channel相关的操作都将由该eventLoop完成;
  • 最后在eventLoop中执行register0,如果当前线程不是eventLoop对应的线程,则将register0封装在Runnable任务中,提交到NioEventLoop任务队列中交由eventLoop对应的线程执行;调用register0需传入promise实例,用于操作成功后通知结果。
  • 若提交register0任务失败,说明eventLoop不接受当前Channel的注册任务,此时需要强制关闭当前Channel,并写入结果到promise。

下面继续看register0的实现:

private void register0(ChannelPromise promise) {
    try {
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        doRegister();
        neverRegistered = false;
        registered = true;
        pipeline.invokeHandlerAddedIfNeeded();
        safeSetSuccess(promise);
        pipeline.fireChannelRegistered();
        if (isActive()) {
            if (firstRegistration) {
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                beginRead();
            }
        }
    } catch (Throwable t) {
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}

下面分析register0的整个过程:

  • 首先调用setUncancellable将当前promise设置为不可取消,或者promise已经成功完成而不能被取消,两种情况都将返回true,否则,promise已经被取消,退出register0;setUncancellable返回true则调用ensureOpen确保Channel仍然打开,如果Channel已经关闭,退出register0;
  • 如果promise未取消且Channel仍然打开,执行doRegister将Channel注册到EventLoop的多路复用器Selector上,该方法在AbstractNioChannel中实现;
  • Channel注册到多路复用器后,调用pipeline.invokeHandlerAddedIfNeeded(),保证在Channel注册之后立刻回调handlerAdded,以保证handler被添加到pipeline上。
  • 调用调用pipeline的fireChannelRegistered触发Channel成功注册事件;
  • 如果Channel处于活动状态,且Channel是第一次注册,调用pipeline.fireChannelActive()触发Channel处于活动状态事件,同一个Channel只有在第一次注册的情况下才可能触发该事件;这样是为了防止多次触发Channel Active事件,如Channel注册后,调用deregister,再次调用register;
  • 如果Channel处于活动状态,且设置了autoRead,则发起read操作;
  • 整个过程中若抛出异常,需强制关闭Channel,防止fd泄漏。

注意点:

  • 将当前Channel注册到多路复用器之前,必须确保Channel仍然打开,因为当register0进入任务队列中等待I/O线程执行时,Channel可能被关闭了;
  • 调用safeSetSuccess通知promise之前需首先调用pipeline.invokeHandlerAddedIfNeeded()确保handler已经被添加到pipeline上,这是非常有必要的。假设先调用safeSetSuccess,即通知promise通道成功注册到多路复用器上,promise将调用notifyListeners通知所有监听者,若客户端已经在ChannelFutureListener中通过pipeline触发事件,用户自定义handler可能就会丢失该事件。回顾一下,context将事件提交给handler处理时,都会先调用invokeHandler()检查handler的状态,对于需要按顺序处理事件的context,只有handler的状态为ADD_COMPLETE,invokeHandler()才会将事件交给对应的handler处理,否则,只负责传递事件。而只有当Channel注册完成之后,handler的状态才会被设置为ADD_COMPLETE。因此需要首先调用invokeHandlerAddedIfNeeded确保ChannelInitializer的实例被添加到pipeline中,然后将自定义的handler添加到pipeline,这样可保证handler不会丢失事件。
  • 每个Channel只会触发一次Channel活动事件,因此这里通过firstRegistration标志位来确保只有Channel第一次注册时才调用fireChannelActive触发事件,防止Channel注册后,调用deregister之后再次调用register,从而多次触发该事件。

下面看看doRegister将Channel注册到多路复用器的实现:

protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
            return;
        } catch (CancelledKeyException e) {
            if (!selected) {
                eventLoop().selectNow();
                selected = true;
            } else {
                throw e;
            }
        }
    }
}

javaChannel用于返回调用AbstractNioChannel构造方法初始化的SelectableChannel 实例,eventLoop().unwrappedSelector()将返回构造NioEventLoop实例时绑定的Selector对象,这样通过SelectableChannel的register方法将当前Channel注册到EventLoop的多路选择器上。

AbstractNioChannel注册Selector的事件类型为0,表示对任何事件都不感兴趣,仅仅是为了完成注册操作。调用SelectableChannel 的register方法时,将AbstractNioChannel的子类实例(this)作为附加对象传入,这样使得后续Channel接收到网络事件通知时可以从SelectionKey中重新获取之前的附加对象进行处理,即这里可通过SelectionKey可以从多路复用器中获取Channel对象。

如果当前注册返回的SelectionKey已经被取消,则抛出CancelledKeyException 异常;如果是第一次处理该异常,调用多路复用器的selectNow方法将已经取消的SelectionKey从多路复用器删除;操作成功后,selected设为true,说明失效的SelectionKey已经删除;继续进行下一次注册,如果仍然抛出CancelledKeyException异常,说明无法删除已经注册的SelectionKey,按照JDK官方说明,不应该出现这种意外。如果发生这种问题,直接抛出异常到上层处理,说明NIO相关类库可能存在不可恢复的BUG。

到此Channel的初始化和注册操作完成。

欢迎指出本文有误的地方,转载请注明原文出处https://my.oschina.net/7001/blog/1421893

转载于:https://my.oschina.net/7001/blog/1421893

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值