自頂向下深入分析Netty(三)--Bootstrap源碼分析

長文預警,本文為源碼分析部分,夾雜大量源碼可能會引起不適,請選擇性閱讀。如果你只想知道Bootstrap的使用,可以閱讀前一篇文章:自頂向下深入分析Netty(三)--Bootstrap


2.源碼分析

Bootstrap類圖
Bootstrap類圖

首先看Bootstrap類圖,可見類圖比較簡單。在分析時也使用自頂向下的方法,首先分析頂層的AbstractBootstrap,然后分析其子類Bootstrap和ServerBootstrap。

2.1 AbstractBootstrap

首先看其中的類簽名:

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

這個泛型這么復雜,我已經不想繼續了。別急,這個泛型其實也是一個標準用法,在JDK的Enum中就有類似的使用方法:Enum<E extends Enum<E>>。還記得在ServerBootstarp中可以使用.group().channel().option().handler()這樣的形式嗎?這正是這個泛型的功能:在子類中返回子類本身,無需轉型。更多的細節,可到這個連接查看。

再看其中的字段含義:

    // reactor線程池
    volatile EventLoopGroup group;
    // 通道工廠,主要用來創建初始的Channel,比如服務端的第一個執行bind()方法的serverChannel,
    // 客戶端第一個執行connect()方法的Channel
    private volatile ChannelFactory<? extends C> channelFactory;
    private volatile SocketAddress localAddress;
    // channel相關的選項參數
    private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<>();
    // 初始化channel的屬性值
    private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<>();
    // 業務邏輯Handler,主要是HandlerInitializer,也可能是普通Handler
    private volatile ChannelHandler handler;

接著看其中的關鍵方法說明:

    // 設置Channel的無參構造工廠
    public B channel(Class<? extends C> channelClass);
    // 設置Channel工廠
    public B channelFactory(ChannelFactory<? extends C> channelFactory);
    // 創建一個Channel并綁定到本地端口
    public ChannelFuture bind(SocketAddress localAddress);
    // 初始化Channel并且注冊到線程池
    final ChannelFuture initAndRegister();
    // 初始化一個Channel
    abstract void init(Channel channel) throws Exception;

2.1.1 channelFactory方法

bootstrap包中有一個ChannelFactory接口,代碼如下:

    public interface ChannelFactory<T extends Channel> {
        T newChannel();
    }

其中僅聲明了一個newChannel()方法用來創建一個Channel,一般使用時,會有以下兩種情況:

  1. 在服務端創建一個ServerChannel用于接受客戶端的連接
  2. 在客戶端創建一個Channel用于連接服務端

在這兩種情況下,僅僅創建一個Channel似乎使用Factory過于大材小用。考慮這種情況:在構造一個代理服務器時,服務端需要創建大量的Channel連接目標服務器,這樣使用Factory就很好。并且,當Channel的無參構造方法不能滿足需求時,可以方便用戶定義自己獨有的工廠創建滿足需求的Channel。

接下來分析channelFacotry()方法,這是一個簡單的setter方法(代碼中的null檢查不再列出):

    public B channelFactory(ChannelFactory<? extends C> channelFactory) {
        this.channelFactory = channelFactory;
        return (B) this;
    }

以及另一個默認提供無參構造方法Channel的工廠setter方法:

    public B channel(Class<? extends C> channelClass) {
        return channelFactory(new BootstrapChannelFactory<C>(channelClass));
    }

其中的BootstrapChannelFactory用于反射調用無參構造方法創建一個Chanel,代碼如下:

    public T newChannel() {
        try {
            return clazz.newInstance();
        } catch (Throwable t) {
            throw new ChannelException("Unable to create Channel from class ");
        }
    }

2.1.2 bind方法

bind()方法是AbstarctBootstrap的核心方法,用于綁定前述工廠創建的Channel到本地的一個端口,其中有很多變體方法,關鍵的一個如下:

    public ChannelFuture bind(SocketAddress localAddress) {
        validate(); //確保關鍵參數設置正確
        return doBind(localAddress);
    }

validate()對參數進行驗證,確保關鍵參數設置正確,由于其實現簡單,不貼出代碼。再分析關鍵的doBind()方法:

    private ChannelFuture doBind(final SocketAddress localAddress) {
        final ChannelFuture regFuture = initAndRegister(); // 創建Channel并注冊到線程池
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        if (regFuture.isDone()) {
            // 一般情況下,channel注冊完成且注冊成功
            ChannelPromise promise = channel.newPromise();
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
            // 由于注冊是異步事件,可能此時沒有注冊完成,那么使用異步操作
            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.executor = channel.eventLoop(); // 設置為注冊到的線程
                        doBind0(regFuture, channel, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }

為了更好的理解這段代碼,先回顧Netty的特點:事件驅動,比如上面代碼中的兩個動作:注冊和綁定,在Netty實現中是兩個異步事件,其中注冊是指Channel注冊到reactor線程池,綁定是指Channel獲得了本機的一個TCP端口。如果你熟悉javascript或者GUI編程,對回調函數也不會陌生,Netty也采用類似的機制,所以能在Netty源代碼里看到大量的ChannelFuture(細節可看這一章),當一個事件完成時,會回調注冊到ChannelFuture上的FutureListener從而實現異步操作。此外,Netty4為了簡化邏輯處理邏輯,遵循這樣一條規則:一個Channel產生的IO事件始終由其注冊到的線程處理,可知注冊和綁定事件都將在同一個線程也就是Channel注冊到的線程執行。
從代碼中可以看出bind分為兩步:initAndRegister()以及doBind0()initAndRegister()用于創建Channel、綁定用戶定義的Handler、以及將該Chanel注冊到一個Reactor中,代碼如下:

    final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
            // 創建一個Channel
            channel = channelFactory().newChannel();
            // 初始化處理器Handler
            init(channel);
        } catch (Throwable t) {
            if (channel != null) {
                channel.unsafe().closeForcibly();
            }
            // 還沒有注冊到線程池,使用默認線程GlobalEventExecutor
            return new DefaultChannelPromise(channel,
                            GlobalEventExecutor.INSTANCE).setFailure(t);
        }
        // 將channel注冊到Reactor線程池
        ChannelFuture regFuture = group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }
        return regFuture;
    }
    
    abstract void init(Channel channel) throws Exception;

其中的init()主要用于初始化處理器Handler,可視為一個模板方法,由子類Bootstrap或ServerBootstrap實現具體的初始化細節。

接著分析實現綁定本地端口的doBind0()方法:

    private static void doBind0(final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {
        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());
                }
            }
        });
    }

如果你對channel.eventLoop().execute()的用法有疑問,再次回顧這條規則:一個Channel產生的IO事件始終由其注冊到的線程處理。綁定事件是一個類似write的出站事件,所以需要由channel注冊到的線程完成。為什么不使用regFuture直接添加Futurelistener完成綁定處理呢?代碼中的解釋是注冊不一定成功,失敗后可能執行的線程并不是注冊的線程(我查看代碼在這里僅僅調用register(Channel)并不會有這樣的情況)。

這個bind過程夾雜很多私貨,總結一下流程:

  1. 使用ChannelFactory創建一個Channel
  2. 調用init()方法初始化Channel,主要是綁定處理器
  3. 注冊到一個Reactor線程池
  4. 對注冊的異步結果處理:注冊成功進行綁定操作,此時綁定失敗將會關閉Channel返回異步綁定失敗,綁定成功返回異步成功;注冊失敗直接返回異步注冊失敗。

2.2 ServerBootstrap

關鍵字段如下:

    // 為accept的客戶端channel設置的選項參數
    private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<>();
    // 為accept的客戶端channel設置的屬性鍵值對
    private final Map<AttributeKey<?>, Object> childAttrs = new LinkedHashMap<>();
    // subReactor線程池,父類的group成為mainReactor線程池
    private volatile EventLoopGroup childGroup;
    // subReactor線程池的事件處理器,一般為ChannelInitializer初始化處理器鏈
    private volatile ChannelHandler childHandler;

ServerBootstrap中的關鍵方法只有父類中的模板方法init(channel)

2.2.1 init方法

ServerBootstrap的init(channel)方法,完成的工作有:

  1. 設置serverChannel的選項參數
  2. 設置serverChannel的屬性鍵值對
  3. 添加處理serverChannel的IO事件處理器

其中1和2兩條都比較簡單,不再列出代碼,主要看3的處理,代碼如下:

    void init(Channel channel) throws Exception {
        // 1.設置serverChannel的選項參數
        // 2.設置serverChannel的屬性鍵值對
        // 3.添加處理serverChannel事件的處理器
        ChannelPipeline p = channel.pipeline();
        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = handler();
                if (handler != null) {
                    pipeline.addLast(handler);
                }
                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                currentChildGroup,currentChildHandler, 
                                currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

可見,向serverChannel添加是一個初始化處理器ChannelInitializer,完成的主要工作是將用戶Handler以及一個ServerBootstrapAcceptor添加到serverChannel的處理器鏈中。此處需要注意的是,結合文章最開始的示例,p.addLast()方法在main線程中執行,而initChannel()方法將在Channel注冊到的線程中執行,執行的時機是該ChannelInitializer被添加到ChannelPipeline中時但晚于p.addLast()方法。明白了這點,繼續分析ch.eventLoop().execute()的使用,這是因為需要保證ServerBootstrapAcceptor被添加到處理器鏈的最尾部以便不破壞mainReactor將accept接受的Channel連接傳遞給subReactor。但是當通過handler()獲得的用戶Handler也是一個ChannelInitializer,如果只是常規的使用pipeline.addLast(acceptor)將導致acceptor并不在處理器鏈的最尾部。

2.2.2 ServerBootstrapAcceptor靜態內部類

在前面的分析中,不斷提到了ServerBootstrapAcceptor,正對應文章最開始圖中綠色的acceptor。Netty默認實現了這個acceptor處理器,主要功能是將mainReactor接受的Channel傳遞給subReactor。該類的字段如下:

    private final EventLoopGroup childGroup;
    private final ChannelHandler childHandler;
    private final Entry<ChannelOption<?>, Object>[] childOptions;
    private final Entry<AttributeKey<?>, Object>[] childAttrs;

ServerBootstrap類的關鍵字段一致,由此可見這個內部類是一個關鍵點,首先看該類的類簽名:

    private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter

該類繼承自ChannelInboundHandlerAdapter,作為Netty的使用者對這個類很熟悉,如果你只是初學,可在學完本章之后繼續查看這個鏈接。首先明確這個Handler處理器是一個inbound事件處理器,需要注意的是:Netty將ServerChannel接受客戶端連接的accept事件抽象為Read讀取事件。因此,我們重點關注channelRead()方法,其完成的工作有:

  1. 配置Channel,包括Channel上的處理器鏈,Channel的選項參數及屬性鍵值對。
  2. 將服務端accept的客戶端Channel注冊到subReactor線程池的一個線程上

其中的代碼清晰易懂,代碼如下:

    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 服務器accept的客戶端channel
        final Channel child = (Channel) msg;    
        // 設置處理器鏈
        child.pipeline().addLast(childHandler);
        // 設置channel的選項參數
        for (Entry<ChannelOption<?>, Object> e: childOptions) {
            try {
                if (!child.config().setOption(
                             (ChannelOption<Object>) e.getKey(), e.getValue())) {
                    logger.warn("Unknown channel option: " + e);
                }
            } catch (Throwable t) {
                logger.warn("Failed to set a channel option: " + child, t);
            }
        }
        // 設置channel的屬性鍵值對
        for (Entry<AttributeKey<?>, Object> e: childAttrs) {
            child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
        }

        // 將客戶端channel注冊到subReactor線程池,注冊失敗或者拋出異常則關閉channel
        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);
        }
    }

    private static void forceClose(Channel child, Throwable t) {
        child.unsafe().closeForcibly();
        logger.warn("Failed to register an accepted channel: " + child, t);
    }

其中的exceptionCaught()方法也值得關注,當ServerChannel事件在執行中產生異常時,用戶并不希望ServerChannel被關閉,因為還有其他的客戶端連接需要處理。為此,Netty處理異常時使用這樣的極致:產生異常后暫停接受客戶端連接,1s以后再恢復接受連接。

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) 
                    throws Exception {
        final ChannelConfig config = ctx.channel().config();
        if (config.isAutoRead()) {
            // 停止accept客戶端channel的連接
            config.setAutoRead(false);
            // 1s以后重新接受客戶端channel連接
            ctx.channel().eventLoop().schedule(new Runnable() {
                @Override
                public void run() {
                   config.setAutoRead(true);
                }
            }, 1, TimeUnit.SECONDS);
        }
        ctx.fireExceptionCaught(cause);
    }

2.3 Bootstrap

Bootstrap是客戶端啟動器,只增加了一個字段:

    private volatile SocketAddress remoteAddress;   // 服務端地址

2.1 init方法

客戶端的init()方法很簡單,只完成配置Channel的工作,包括Channel上的處理器鏈,Channel的選項參數及屬性鍵值對。代碼如下:

    void init(Channel channel) throws Exception {
        ChannelPipeline p = channel.pipeline();
        // 設置處理器鏈
        p.addLast(handler());
        // 設置channel的選項參數
        final Map<ChannelOption<?>, Object> options = options();
        synchronized (options) {
            for (Entry<ChannelOption<?>, Object> e: options.entrySet()) {
                try {
                    if(!channel.config().setOption(
                            (ChannelOption<Object>)e.getKey(), e.getValue())) {
                        logger.warn("Unknown channel option: " + e);
                    }
                } catch (Throwable t) {
                    logger.warn("Failed to set a channel option: " + channel, t);
                }
            }
        }
        // 設置channel的屬性鍵值對
        final Map<AttributeKey<?>, Object> attrs = attrs();
        synchronized (attrs) {
            for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
                channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
            }
        }
    }

2.2 connect方法

客戶端需要連接到遠程服務端,才能進行網絡通訊,所以Bootstrap作為客戶端啟動器必然要有connect(remoteAddr)方法。該方法有很多變種,關鍵的一個如下:

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

繼續分析doConnect()方法:

    private ChannelFuture doConnect(final SocketAddress remoteAddress, 
                                                final SocketAddress localAddress) {
        final ChannelFuture regFuture = initAndRegister();  //創建Channel并注冊到線程池
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        final ChannelPromise promise = channel.newPromise();
        if (regFuture.isDone()) {
            // 注冊異步操作完成,再提交一個連接異步任務
            doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
        } else {
            // 注冊異步操作未完成,當注冊異步完成時提交一個連接異步任務
            regFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
                }
            });
        }
        return promise;
    }

此處的處理與ServerBootstrapbind()方法很類似,可類比分析。由此推知,doConnect0()方法將向channel注冊的線程池提交一個異步連接任務。的確如此,代碼如下:

    private static void doConnect0(final ChannelFuture regFuture, final Channel channel,
                                final SocketAddress remoteAddress, 
                                final SocketAddress localAddress,
                                final ChannelPromise promise) {
        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (regFuture.isSuccess()) {
                    if (localAddress == null) {
                        channel.connect(remoteAddress, promise);
                    } else {
                        channel.connect(remoteAddress, localAddress, promise);
                    }
                    promise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
                } else {
                    promise.setFailure(regFuture.cause());
                }
            }
        });
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容