netty系列之:Bootstrap,ServerBootstrap和netty中的實現

flydean發表於2022-02-21

簡介

雖然netty很強大,但是使用netty來構建程式卻是很簡單,只需要掌握特定的netty套路就可以寫出強大的netty程式。每個netty程式都需要一個Bootstrap,什麼是Bootstrap呢?Bootstrap翻譯成中文來說就是鞋拔子,在計算機世界中,Bootstrap指的是載入程式,通過Bootstrap可以輕鬆構建和啟動程式。

在netty中有兩種Bootstrap:客戶端的Bootstrap和伺服器端的ServerBootstrap。兩者有什麼不同呢?netty中這兩種Bootstrap到底是怎麼工作的呢? 一起來看看吧。

Bootstrap和ServerBootstrap的聯絡

首先看一下Bootstrap和ServerBootstrap這兩個類的繼承關係,如下圖所示:

netty系列之:Bootstrap,ServerBootstrap和netty中的實現

可以看到Bootstrap和ServerBootstrap都是繼承自AbstractBootstrap,而AbstractBootstrap則是實現了Cloneable介面。

AbstractBootstrap

有細心的同學可能會問了,上面圖中還有一個Channel,channel跟AbstractBootstrap有什麼關係呢?

我們來看下AbstractBootstrap的定義:

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable

AbstractBootstrap接受兩個泛型引數,一個是B繼承自AbstractBootstrap,一個是C繼承自Channel。

我們先來觀察一下一個簡單的Bootstrap啟動需要哪些元素:

EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new FirstServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 繫結埠並開始接收連線
            ChannelFuture f = b.bind(port).sync();
            // 等待server socket關閉
            f.channel().closeFuture().sync();

上面的程式碼是一個最基本也是最標準的netty伺服器端的啟動程式碼。可以看到和Bootstrap相關的元素有這樣幾個:

  1. EventLoopGroup,主要用來進行channel的註冊和遍歷。
  2. channel或者ChannelFactory,用來指定Bootstrap中使用的channel的型別。
  3. ChannelHandler,用來指定具體channel中訊息的處理邏輯。
  4. ChannelOptions,表示使用的channel對應的屬性資訊。
  5. SocketAddress,bootstrap啟動是繫結的ip和埠資訊。

目前看來和Bootstrap相關的就是這5個值,而AbstractBootstrap的建構函式中也就定義了這些屬性的賦值:

    AbstractBootstrap(AbstractBootstrap<B, C> bootstrap) {
        group = bootstrap.group;
        channelFactory = bootstrap.channelFactory;
        handler = bootstrap.handler;
        localAddress = bootstrap.localAddress;
        synchronized (bootstrap.options) {
            options.putAll(bootstrap.options);
        }
        attrs.putAll(bootstrap.attrs);
    }

示例程式碼中的group,channel,option等方法實際上都是向這些屬性中賦值,並沒有做太多的業務操作。

注意,AbstractBootstrap中只存在一個group屬性,所以兩個group屬性是在ServerBootstrap中新增的擴充套件屬性。

在Bootstrap中,channel其實是有兩種賦值方法,一種是直接傳入channel,另外一種方法是傳入ChannelFactory。兩者的本質都是一樣的,我們看下channel是怎麼轉換成為ChannelFactory的:

    public B channel(Class<? extends C> channelClass) {
        return channelFactory(new ReflectiveChannelFactory<C>(
                ObjectUtil.checkNotNull(channelClass, "channelClass")
        ));
    }

channelClass被封裝在一個ReflectiveChannelFactory中,最終還是設定的channelFactory屬性。

AbstractBootstrap中真正啟動服務的方法就是bind,bind方法傳入的是一個SocketAddress,返回的是ChannelFuture,很明顯,bind方法中會建立一個channel。我們來看一下bind方法的具體實現:

   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;
        }
    }

在doBind方法中,首先呼叫initAndRegister方法去初始化和註冊一個channel。

channel是通過channelFactory的newChannel方法來建立的:

channel = channelFactory.newChannel();

接著呼叫初始化channel的init方法。這個init方法在AbstractBootstrap中並沒有實現,需要在具體的實現類中實現。

有了channel之後,通過呼叫EventLoopGroup的register方法將channel註冊到 EventLoop中,並將註冊生成的ChannelFuture返回。

然後通過判斷返回的regFuture的狀態,來判斷channel是否註冊成功,如果註冊成功,最後呼叫doBind0方法,完成最後的繫結工作:

    private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {

        // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
        // the pipeline in its channelRegistered() implementation.
        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (regFuture.isSuccess()) {
                    channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
                } else {
                    promise.setFailure(regFuture.cause());
                }
            }
        });
    }

因為eventLoop本身是一個Executor,所以可以執行一個具體的命令的,在它的execute方法中,傳入了一個新的Runnable物件,在其中的run方法中執行了channel.bind方法,將channel跟SocketAddress進行繫結。

到此,Bootstrap的bind方法執行完畢。

我們再來回顧一下bind方法的基本流程:

  1. 通過ChannelFactory建立一個channel。
  2. 將channel註冊到Bootstrap中的EventLoopGroup中。
  3. 如果channel註冊成功,則呼叫EventLoopGroup的execute方法,將channel和SocketAddress進行繫結。

是不是很清晰?

講完AbstractBootstrap,接下來,我們再繼續探討一下Bootstrap和ServerBootstrap。

Bootstrap和ServerBootstrap

首先來看下Bootstrap,Bootstrap主要使用在客戶端使用,或者UDP協議中。

先來看下Bootstrap的定義:

public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> 

Bootstrap和AbstractBootstrap相比,主要多了一個屬性和一個方法。

多的一個屬性是resolver:

private static final AddressResolverGroup<?> DEFAULT_RESOLVER = DefaultAddressResolverGroup.INSTANCE;

private volatile AddressResolverGroup<SocketAddress> resolver =
            (AddressResolverGroup<SocketAddress>) DEFAULT_RESOLVER;

AddressResolverGroup裡面有一個IdentityHashMap,它的key是EventExecutor,value是AddressResolver:

    private final Map<EventExecutor, AddressResolver<T>> resolvers =
            new IdentityHashMap<EventExecutor, AddressResolver<T>>();

實際上AddressResolverGroup維護了一個EventExecutor和AddressResolver的對映關係。

AddressResolver主要用來解析遠端的SocketAddress的地址。因為遠端的SocketAddress可能並不是一個IP地址,所以需要使用AddressResolver解析一下。

這裡的EventExecutor實際上就是channel註冊的EventLoop。

另外Bootstrap作為一個客戶端的應用,它需要連線到伺服器端,所以Bootstrap類中多了一個connect到遠端SocketAddress的方法:

    public ChannelFuture connect(SocketAddress remoteAddress) {
        ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
        validate();
        return doResolveAndConnect(remoteAddress, config.localAddress());
    }

connect方法和bind方法的邏輯類似,只是多了一個resolver的resolve過程。

解析完畢之後,會呼叫doConnect方法,進行真正的連線:

    private static void doConnect(
            final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {

        // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
        // the pipeline in its channelRegistered() implementation.
        final Channel channel = connectPromise.channel();
        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (localAddress == null) {
                    channel.connect(remoteAddress, connectPromise);
                } else {
                    channel.connect(remoteAddress, localAddress, connectPromise);
                }
                connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            }
        });
    }

可以看到doConnect方法和doBind方法很類似,都是通過當前channel註冊的eventLoop來執行channel的connect或許bind方法。

再看一下ServerBootstrap的定義:

public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel>

因為是ServerBootstrap用在伺服器端,所以不選Bootstrap那樣去解析SocketAddress,所以沒有resolver屬性。

但是對應伺服器端來說,可以使用parent EventLoopGroup來接受連線,然後使用child EventLoopGroup來執行具體的命令。所以在ServerBootstrap中多了一個childGroup和對應的childHandler:

    private volatile EventLoopGroup childGroup;
    private volatile ChannelHandler childHandler;

因為ServerBootstrap有兩個group,所以ServerBootstrap包含一個含有兩個EventLoopGroup的group方法:

    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) 

還記得bind方法需要實現的init方法嗎? 我們看下ServerBootstrap中init的具體邏輯:

   void init(Channel channel) {
        setChannelOptions(channel, newOptionsArray(), logger);
        setAttributes(channel, newAttributesArray());

        ChannelPipeline p = channel.pipeline();

        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);
        final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs);

        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);
                }

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

首先是設定channel的一些屬性,然後通過channel.pipeline方法獲得channel對應的pipeline,然後向pipeline中新增channelHandler。

這些都是常規操作,我們要注意的是最後通過channel註冊到的eventLoop,將ServerBootstrapAcceptor加入到了pipeline中。

很明顯ServerBootstrapAcceptor本身應該是一個ChannelHandler,它的主要作用就是用來接受連線:

    private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter

我們來看一下它的channelRead方法:

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

            child.pipeline().addLast(childHandler);

            setChannelOptions(child, childOptions, logger);
            setAttributes(child, childAttrs);

            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);
            }
        }

因為server端接受的是客戶端channel的connect操作,所以對應的channelRead中的物件實際上是一個channel。這裡把這個接受到的channel稱作child。通過給這個child channel新增childHandler,childOptions和childAttrs,一個能夠處理child channel請求的邏輯就形成了。

最後將child channel註冊到childGroup中,至此整個ServerBootstrapAcceptor接受channel的任務就完成了。

這裡最妙的部分就是將客戶端的channel通過server端的channel傳到server端,然後在server端為child channel配備handler進行具體的業務處理,非常巧妙。

總結

通過具體分析AbstractBootstrap,Bootstrap和ServerBootstrap的結構和實現邏輯,相信大家對netty服務的啟動流程有了大概的認識,後面我們會詳細講解netty中的channel和非常重要的eventLoop。

本文已收錄於 http://www.flydean.com/03-1-netty-boots…-serverbootstrap/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章