1. 程式人生 > >Netty核心概念(4)之Bootstrap

Netty核心概念(4)之Bootstrap

nec center ddl msg throws sync oot 工廠類 value

1.前言

第三節介紹了Netty的一些基本概念,此節介紹Netty的第一個概念Bootstrap——啟動類。Netty中服務端和客戶端的啟動類是不一樣的,這個不要搞錯了,類都在bootstrap包下。之後的所有章節都是基於目前最新版本的Netty 4.1.24.Final版本。

2.結構詳解

技術分享圖片

bootstrap中主要就兩個內容:bootstrap和config。channelFactory被移動到其他地方了,這個接口已經廢棄。亂入了一個FailedChannel,這裏暫且不管。

config給人帶來歧義,讓人誤以為是我們去設置這個config,bootstrap讀取這個設置進行初始化。實際上相反,這個類的作用是暴露我們對bootstrap的設置,僅僅起到展示配置的作用,並不是對其設置。設置相關內容還是需要直接操作bootstrap。所以config也不進行介紹,只剩下AbstractBootstrap、Bootstrap和ServerBootstrap了,本章主要介紹這三個內容。

2.1 AbstractBootstrap

 這個是所有啟動類的抽象父類,包含了一系列的基礎內容。

技術分享圖片

 這是該類的基礎字段:

  group:線程池

  channelFactory: 創建channel的工廠類

  localAddress:本地地址

  options: socket及其它的一些屬性設置

  attrs:附加屬性設置

  handler:channel處理類

 其中options和attrs在例子中暫時沒有表現出來,使用起來也不復雜,看具體方法即可。下面介紹一些需要關註的方法。

    public B channel(Class<? extends C> channelClass) {
        if (channelClass == null) {
            throw new NullPointerException("channelClass");
        }
        return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
    }

 這個方法可以看出,放入Channel.class之後,其實際上是通過一個工廠類生成的channel對象,ReflectiveChannelFactory的做法也十分的簡單,就是通過反射的方式newInstance()了該類的一個對象。另外bootstrap還直接提供了一個channelFactory方法,從字段也可以看出我們需要的是一個channelFactory。

 localAddress(..)該方法就是設置bind的方法監聽的本地地址端口了,和bind()配合使用,bind(..)就是直接使用傳入的地址,而不會管之前設置的localAddress。

最後介紹一下最重要的bind(address)方法,這個就是針對服務端的監聽端口環節,看看這步究竟做了些什麽事情:

    public ChannelFuture bind(SocketAddress localAddress) {
        validate();
        if (localAddress == null) {
            throw new NullPointerException("localAddress");
        }
        return doBind(localAddress);
    }

    private ChannelFuture doBind(final SocketAddress localAddress) {
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        if (regFuture.isDone()) {
            // At this point we know that the registration was complete and successful.
            ChannelPromise promise = channel.newPromise();
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
            // Registration future is almost always fulfilled already, but just in case it‘s not.
            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) {
                        // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                        // IllegalStateException once we try to access the EventLoop of the Channel.
                        promise.setFailure(cause);
                    } else {
                        // Registration was successful, so set the correct executor to use.
                        // See https://github.com/netty/netty/issues/2586
                        promise.registered();

                        doBind0(regFuture, channel, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }

  1、先檢驗基礎配置是否完成,主要關心兩個內容:線程池和channelFactory。

  2、通過channelFactory生成一個channel對象,並初始化(init(Channel),方法該方法是個抽象方法,子類完成初始化動作),失敗關閉相關資源,之後將channel註冊到線程池中(即線程池提供了註冊channel的方法),失敗一樣清除相關資源。

  3、如果前面成功,開始綁定端口地址,綁定失敗自動關閉,這個就是由channel自身完成,channel.bind(address, promise)。

 上面整個bind方法就完成了,主要註意的有以下幾個內容:

    1.啟動類可以自己決定channel初始化的一些操作;

    2.channel必須註冊到線程池中,即線程池提供channel接入的入口;

    3.具體的監聽端口方法由channel自身實現;

 其它的方法就無關緊要了,抽象父類需要關註的內容就這麽點。

2.2 ServerBootstrap

 該類是服務端的啟動類,根據第三節所說的,為了區別處理服務端本身的監聽端口channel和客戶端的channel都使用了連接線程池。實際上不只是線程池,其繼承抽象父類的都是服務端的相關內容,客戶端的內容用另一組字段設置了,所以我們可以看到我們設置的handler是childHandler,服務端本身是不需要設置handler的。具體客戶端相關設置如下:

技術分享圖片

 大部分都是針對child設置的內容,不需要一一解釋,應該都能明白。

 該類需要關註的方法只有一個,就是抽象父類交給子類實現的init方法,如何初始化channel?

    void init(Channel channel) throws Exception {
        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()) {
                @SuppressWarnings("unchecked")
                AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
                channel.attr(key).set(e.getValue());
            }
        }

        ChannelPipeline p = channel.pipeline();

        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        final Entry<ChannelOption<?>, Object>[] currentChildOptions;
        final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
        synchronized (childOptions) {
            currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
        }
        synchronized (childAttrs) {
            currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
        }

        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(final Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler();
                if (handler != null) {
                    pipeline.addLast(handler);
                }

                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

  1、首先就是設置channel的socket參數了,這個就是我們之前設置的option參數了。

  2、就是設置屬性了,這個屬性就是與channel綁定的,也是之前設置的attr參數。

  3、最後是關鍵的一步,獲取了這個channel的pipeline,pipeline的概念channel那章再介紹,這裏只需要了解對channel的管理都是通過pipeline完成的。之前我說過服務端不需要設置handler,並不意味著完全可以不要,這裏就為服務端的channel設置了handler。我們可以想一下,如果不處理,如何對客戶端的channel進行設置屬性等內容呢。這裏就在channel初始化的時候設置了ServerBootstrapAcceptor。

    public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;

            child.pipeline().addLast(childHandler);

            setChannelOptions(child, childOptions, logger);

            for (Entry<AttributeKey<?>, Object> e: childAttrs) {
                child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
            }

            try {
                childGroup.register(child).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            forceClose(child, future.cause());
                        }
                    }
                });
            } catch (Throwable t) {
                forceClose(child, t);
            }
     }

 上述就是ServerBootstrapAcceptor的read方法,其做了如下操作:

  1、設置handler

  2、設置socket屬性

  3、設置attr

  4、將channel註冊到childGroup,客戶端的線程池中。

 這裏可以推測一下相關邏輯了,因為read方法不是數據讀取的時候才觸發嗎?根據JAVA NIO例子,這步應該出現在accept事件階段,所以可以推測是該階段調用了handler的read方法,傳遞了accept的客戶端channel,在此對該channel進行處理,至於對不對,要後續解讀代碼進行驗證(這個沒錯,簡單說下,以NIO為例,主要邏輯在NioEventLoop調用的unsafe.read(),對於Server端而言是NioMessageUnsafe的read()方法,其調用了NioServerSocketChannel的doReadMessages方法,在buf中放入了accept的NioSocketChannel對象,再回到read()方法,通過pipeline調用fireChannelRead(buf[i]),這個會最終傳導到handler,該handler接收到的不是byte而是一個客戶端的channel對象,所以擴展連接方法的時候這段邏輯要非常註意)。但是通過上述說明,也就能明白服務端為什麽不用設置handler了,因為能做的事情也就這些,除非你還有其他的事情要做,也許是統計多久有一個連接進來?這個沒實驗過,不過根據原理,應該可行,除非做了其他限制。

2.3 Bootstrap

 最後一個我們客戶端的設置。客戶端與服務端最大的不同在於,其是connect而不是bind,而且其需要知道遠程服務端的地址。所以客戶端的字段多了如下內容:

技術分享圖片

 遠程地址,和翻譯遠程地址的resolver。由於客戶端關註的是connect方法,而不是bind方法,我們重新對connect方法進行說明:

    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 {
            // Registration future is almost always fulfilled already, but just in case it‘s not.
            final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
            regFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    // Directly obtain the cause and do a null check so we only need one volatile read in case of a
                    // failure.
                    Throwable cause = future.cause();
                    if (cause != null) {
                        // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                        // IllegalStateException once we try to access the EventLoop of the Channel.
                        promise.setFailure(cause);
                    } else {
                        // Registration was successful, so set the correct executor to use.
                        // See https://github.com/netty/netty/issues/2586
                        promise.registered();
                        doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }

  1、第一步和bind操作一樣,創建一個channel,初始化這個channel(操作差不多,註冊handler,設置socket參數,設置attr)註冊到線程池中,失敗就清除資源。

  2、根據線程池獲取resolver,對不同的連接,翻譯不同的地址。

  3、進行連接,調用channel提供的connect方法。

 大體內容和server端差不多,這裏不需要再增加額外的內容了,只需要使用抽象父類的字段就可以完成了。

3.後記

 本節主要介紹了bootstrap包中的一些內容,即Netty啟動的一些操作。主要內容如下:

  1.客戶端和服務端執行的邏輯不同,一個是connect一個是bind,但是最終都是通過channel來完成該操作的,即channel決定了連接的方式。

  2.服務端不需要設置服務端的handler,其內置了一個ServerBoostrapAcceptor,主要設置了客戶端的channel屬性,這段邏輯最終也是由服務端的channel的read相關方法控制的,即服務端的channel,read方法接收的是一個channel而不是一個byte。

  3.所有的channel都需要註冊到EventLoop中。

 以上幾點內容都是與channel,EventLoop相關聯了,所以特此提出,方便後續概念的相互驗證,其具體做了哪些工作。

Netty核心概念(4)之Bootstrap