【Netty源码系列】服务端启动流程(三)绑定端口并启动

通过前面两篇文章的铺垫,终于到了Netty服务端启动的核心流程,但涉及的方法十分多,希望咱们看源码之前,一定要有一个关注点,看源码的过程中就重点留意所关注的东西,其他与核心流程的逻辑关系不大,甚至有很多看不懂的方法,可以先跳过,只关注核心的东西就行。如果对于每一行代码都要执着的理解,这将会是一场灾难!很有可能会因为源码的苦涩而半途而废!所以,看源码的时候,一定要有关注点,看源码的过程中不能迷失...

好了,我们看一下Netty服务端启动的代码

    // 绑定端口并启动
    ChannelFuture f = b.bind(PORT).sync();

通过这么简单的一行,就能够将Netty服务端运行起来,但其背后却做了很多东西!(再次佩服Netty框架的设计者,使开发者可以优雅简单的运行一个网络应用程序)

好了,废话不多说,看源码!

    public ChannelFuture bind(int inetPort) {
        // InetSocketAddress类:socket编程基础,封装ip和端口为套接字
        return bind(new InetSocketAddress(inetPort));
    }

    public ChannelFuture bind(SocketAddress localAddress) {
        validate();
        return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress"));
    }

    private ChannelFuture doBind(final SocketAddress localAddress) {
        // 创建Channel,注册到对应的eventLoop。异步返回Future对象
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        // 因为regFuture是异步返回的,如果成功注册到eventLoop,则直接调用doBind0方法绑定端口
        if (regFuture.isDone()) {
            ChannelPromise promise = channel.newPromise();
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else { // 如果regFuture对象还没返回,则添加监听器,直到有返回内容后,成功注册则调用doBind0方法绑定端口
            final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
            regFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    Throwable cause = future.cause();
                    if (cause != null) {
                        promise.setFailure(cause);
                    } else {
                        promise.registered();
                        doBind0(regFuture, channel, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }

其中,doBind方法是启动ServerBootstrap的核心,咱们先来看一下initAndRegister方法

    // 创建Channel,注册到对应的eventLoop
    final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
            // 创建channel对象
            channel = channelFactory.newChannel();
            // 初始化channel
            init(channel);
        } catch (Throwable t) {
            // 省略异常处理代码
        }
        // 将channel注册到eventLoop
        ChannelFuture regFuture = config().group().register(channel);
        // 省略异常处理代码
        return regFuture;
    }

去掉异常处理的代码,可以看到主要调用的方法是newChannel()init(channel)register(channel)方法,简单来说就是创建Channel-》初始化Channel-》注册Channel
接下来继续重点关注上面三个步骤的源码

创建Channel

【Netty源码系列】服务端启动流程(二)创建并初始化ServerBootstrap对象文章中提到,Channel的创建是被封装到 ReflectiveChannelFactory 类中,所以创建Channel是调用ReflectiveChannelFactory类的newChannel方法

    @Override
    public T newChannel() {
        try {
            return constructor.newInstance();
        } catch (Throwable t) {
            throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t);
        }
    }

constructor对象是在定义Channel类型的时候定义的,比如下面例子中的channel(NioServerSocketChannel.class),所以constructor.newInstance()实际上就是调用NioServerSocketChannel的无参构造器

        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup) // 绑定线程池组
                .channel(NioServerSocketChannel.class) // 服务端channel类型
                .option(ChannelOption.SO_BACKLOG, 100) // TCP配置
                .handler(new LoggingHandler(LogLevel.INFO)) // 服务端Handler
                .childHandler(new ChannelInitializer<SocketChannel>() { // 客户端Handler
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline p = ch.pipeline();
                        //p.addLast(new LoggingHandler(LogLevel.INFO));
                        p.addLast(serverHandler);
                    }
                });
    public NioServerSocketChannel() {
        this(newSocket(DEFAULT_SELECTOR_PROVIDER));
    }

    private static ServerSocketChannel newSocket(SelectorProvider provider) {
        try {
            // 调用JDK原生的方法,创建ServerSocketChannel对象
            return provider.openServerSocketChannel();
        } catch (IOException e) {
            throw new ChannelException("Failed to open a server socket.", e);
        }
    }

    public NioServerSocketChannel(ServerSocketChannel channel) {
        // 调用父类的构造器,封装ServerSocketChannel对象为NioServerSocketChannel,并初始化相关的属性,如:unSafe、pipeline等
        super(null, channel, SelectionKey.OP_ACCEPT);
        // NioServerSocketChannel相关配置
        config = new NioServerSocketChannelConfig(this, javaChannel().socket());
    }

上面就是创建NioServerSocketChannel的流程,实际上底层调用的是JDK原生代码,通过原生代码创建出ServerSocketChannel对象,然后在Netty内部封装为NioServerSocketChannel,并初始化相关的属性。

ps.请各位注意,NioServerSocketChannel构造器中,调用父级的流程请各位自行动手debug看下,也有不少重点需要注意,比如设置channel为非阻塞,channel的id,unsafe、pipeline是如何初始化等等

初始化channel


    /**
     * 1. 设置channel option
     * 2. 设置channel attribute
     * 3. 设置channel pipeline
     *    3.1 pipeline 添加 handler
     * @param channel
     */
    @Override
    void init(Channel channel) {
        // 1. 设置channel option
        setChannelOptions(channel, newOptionsArray(), logger);
        // 2. 设置channel attribute
        setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));
        // 获取channel的pipeline对象
        ChannelPipeline p = channel.pipeline();

        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        final Entry<ChannelOption<?>, Object>[] currentChildOptions;
        // 设置socket channel的option
        synchronized (childOptions) {
            currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);
        }
        // 设置socket channel的attribute
        final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY);
        // pipeline 添加 handler
        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(final Channel ch) {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler();
                if (handler != null) {
                    pipeline.addLast(handler);
                }
                // 为每一个ServerSocketChannel的pipeline添加ServerBootstrapAcceptor处理器,该处理器专门用于接收客户端请求的连接
                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

初始化channel相关代码我都加上注释,其实都比较简单明了,依次设置了ServerSocketChannel的option、attribute相关属性,除此之外,还设置了pipeline内的处理器。值得注意的是,每个ServerSocketChannel都会添加ServerBootstrapAcceptor处理器,这个处理器在客户端请求的时候将会发挥很大的作用,简单来说, 该处理器会将SocketChannel注册到WorkerGroup的某一个EventLoop,然后交给WorkerGroup的线程处理客户端的请求。

让我们回过头,当ServerSocketChannel初始化完成后,就会将当前的ServerSocketChannel注册到BossGroup的EventLoop,在 initAndRegister 方法中对应的代码是ChannelFuture regFuture = config().group().register(channel)。通过debug分析,实际上是调用SingleThreadEventLoop的register方法

注册Channel

    /**
     * 将channel封装成channelPromise对象
     * @param channel
     * @return
     */
    @Override
    public ChannelFuture register(Channel channel) {
        return register(new DefaultChannelPromise(channel, this));
    }

    @Override
    public ChannelFuture register(final ChannelPromise promise) {
        ObjectUtil.checkNotNull(promise, "promise");
        // 将当前channel注册到eventLoop
        promise.channel().unsafe().register(this, promise);
        return promise;
    }

通过上面源码可以看到,promise.channel().unsafe().register(this, promise)会将channel注册到EventLoop上,实际上是调用AbstractChannel&AbstractUnsafe的register方法,那继续往下看...

        @Override
        public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            // 省略一些判断逻辑
            AbstractChannel.this.eventLoop = eventLoop;
            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    eventLoop.execute(new Runnable() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                    // 省略异常逻辑
                }
            }
        }

        private void register0(ChannelPromise promise) {
            try {
                // 省略一些判断逻辑
                boolean firstRegistration = neverRegistered;
                // channel注册到eventLoop的核心方法!!!
                doRegister();
                neverRegistered = false;
                registered = true;
                pipeline.invokeHandlerAddedIfNeeded();
                safeSetSuccess(promise);
                // 调用下一个channelInboundHandler的channelRegistered()方法
                pipeline.fireChannelRegistered();
                // 如果Channel处于活动状态
                if (isActive()) {
                    if (firstRegistration) {
                        // 调用下一个channelInbooundHandler的channelActive()方法
                        pipeline.fireChannelActive();
                    } else if (config().isAutoRead()) {
                        beginRead();
                    }
                }
            } catch (Throwable t) {
                // 省略异常逻辑
            }
        }

通过一层层的debug,终于接近ServerSocketChannel注册到eventLoop的底层实现啦!继续往下看doRegister()方法。由于当前channel是NioServerSocketChannel,所以doRegister方法是在AbstractNioChannel类中实现的

    @Override
    protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
                selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
                return;
            } catch (CancelledKeyException e) {
               // 省略异常逻辑
            }
        }
    }

我们终于知道在Netty中是如何将服务端ServerSocketChannel注册到BossGroup的eventLoop上。实际上是使用JDK原生的register方法注册ServerSocketChannel到BossGroup的eventLoop的Selector上

目前为止,服务端启动流程就可以启动了吗?不不不,还差最后一步,绑定端口!

绑定端口————doBind0方法

绑定端口涉及到的类较多,我这里就不一一展示出来,只展示最后一步绑定端口的相关方法,其完整的调用逻辑可以参考下面的时序图

    // NioServerSocketChannel类
    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
    @Override
    protected void doBind(SocketAddress localAddress) throws Exception {
        if (PlatformDependent.javaVersion() >= 7) {
            javaChannel().bind(localAddress, config.getBacklog());
        } else {
            javaChannel().socket().bind(localAddress, config.getBacklog());
        }
    }

通过调用NioServerSocketChannel类doBind的方法就可以绑定端口,源码中可以发现实际上是调用JDK原生绑定端口方法。以上就是服务端启动的大致流程,希望可以通过上面的流程图和源码剖析过程中的注释和解析可以帮助各位更加了解Netty服务端的启动流程,这对使我们更加熟悉Netty框架,并学习其中一些编程思想,帮助我们日常编码过程中,可以模仿这些优秀框架的设计模式和思想写出更加优质的代码!

如果觉得文章不错的话,麻烦点个赞哈,你的鼓励就是我的动力!对于文章有哪里不清楚或者有误的地方,欢迎在评论区留言~

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

推荐阅读更多精彩内容