1. 程式人生 > >【Netty】(5)原始碼 Bootstrap

【Netty】(5)原始碼 Bootstrap

【Netty】5 原始碼 Bootstrap

上一篇講了AbstractBootstrap,為這篇做了個鋪墊。

一、概述

Bootstrap 是 Netty 提供的一個便利的工廠類, 我們可以通過它來完成 Netty 的客戶端或伺服器端的 Netty 初始化.
Bootstrap: 用於客戶端,只需要一個單獨的Channel,來與服務端進行資料互動,對應server端的子Channel。
作用職責:EventLoop初始化,channel的註冊過程 ,關於pipeline的初始化,handler的新增過程,客戶端連線分析。

Netty客戶端原始碼部分

  EventLoopGroup group = new NioEventLoopGroup();
         try {
             Bootstrap b = new Bootstrap();
             b.group(group) // 註冊執行緒池
              .channel(NioSocketChannel.class) // 使用NioSocketChannel來作為連線用的channel類
              .handler(new ChannelInitializer<SocketChannel>() { // 繫結連線初始化器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                                     //這裡放入自定義助手類
                                     ch.pipeline().addLast(new EchoClientHandler());
                                 }
                             });
         
             ChannelFuture cf = b.connect(host, port).sync(); // 非同步連線伺服器
             cf.channel().closeFuture().sync(); // 非同步等待關閉連線channel
 
         } finally {
             group.shutdownGracefully().sync(); // 釋放執行緒池資源
         }
     }

從上面的客戶端程式碼雖然簡單, 但是卻展示了 Netty 客戶端初始化時所需的所有內容:

1. EventLoopGroup: 不論是伺服器端還是客戶端, 都必須指定 EventLoopGroup. 在這個例子中, 
   指定了 NioEventLoopGroup, 表示一個 NIO   的EventLoopGroup.
2. ChannelType: 指定 Channel 的型別. 因為是客戶端, 因此使用了 NioSocketChannel.
3. Handler: 設定資料的處理器.
4. 這裡的option,提供了一系列的TCP引數

下面我們深入程式碼, 看一下客戶端通過 Bootstrap 啟動後, 都做了哪些工作.

二、原始碼分析


1、group(group)

 /**
  * 直接呼叫父類AbstractBootstrap的方法
  */
public B group(EventLoopGroup group) {
    if (group == null) {
        throw new NullPointerException("group");
    }
    if (this.group != null) {
        throw new IllegalStateException("group set already");
    }
    this.group = group;
    return self();
}

直接呼叫父類的方法 ,說明該EventLoopGroup,作為客戶端 Connector 執行緒,負責註冊監聽連線操作位,用於判斷非同步連線結果。


2、channel(NioServerSocketChannel.class)

在 Netty 中, Channel是一個Socket的抽象, 它為使用者提供了關於 Socket 狀態(是否是連線還是斷開) 以及對 Socket 的讀寫等操作. 每當 Netty 建立了一個連線後, 都會有一個對應的 Channel 例項。

2.1原始碼

/**
 * 同樣也是直接呼叫父類AbstractBootstrap的方法
 */
    public B channel(Class<? extends C> channelClass) {
        if (channelClass == null) {
            throw new NullPointerException("channelClass");
        }
        return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
    }

我們再來看下ReflectiveChannelFactory類

    public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {

        private final Class<? extends T> clazz;

        /**
         * 通過建構函式 傳入 clazz
         */
        public ReflectiveChannelFactory(Class<? extends T> clazz) {
            if (clazz == null) {
                throw new NullPointerException("clazz");
            }
            this.clazz = clazz;
        }

        /**
         * 只用這一個方法 通過傳入不同的Channel.class 建立不同的Channel 物件。
         * newChannel() 什麼時候呼叫呢 仔細追原始碼 發現是在繫結 IP 和 埠的 doResolveAndConnect方法裡會呼叫
         */
        @Override
        public T newChannel() {
            try {
                return clazz.getConstructor().newInstance();
            } catch (Throwable t) {
                throw new ChannelException("Unable to create Channel from class " + clazz, t);
            }
        }

在看channelFactory(new ReflectiveChannelFactory (channelClass)) 方法

   /**
     * 建立好Channel後,返回物件Bootstrap本身
     */
    @Deprecated
    public B channelFactory(ChannelFactory<? extends C> channelFactory) {
        if (channelFactory == null) {
            throw new NullPointerException("channelFactory");
        }
        if (this.channelFactory != null) {
            throw new IllegalStateException("channelFactory set already");
        }
        this.channelFactory = channelFactory;
        return self();
    }

因此對於我們這個例子中的客戶端的 Bootstrap 而言, 生成的的 Channel 例項就是 NioSocketChannel。

2.2 Channel 型別

除了 TCP 協議以外, Netty 還支援很多其他的連線協議, 並且每種協議還有 NIO(非同步 IO) 和 OIO(Old-IO, 即傳統的阻塞 IO) 版本的區別. 不同協議不同的阻塞型別的連線都有不同的 Channel 型別與之對應下面是一些常用的 Channel 型別:

- NioSocketChannel, 代表非同步的客戶端 TCP Socket 連線.
- NioServerSocketChannel, 非同步的伺服器端 TCP Socket 連線.
- NioDatagramChannel, 非同步的 UDP 連線
- NioSctpChannel, 非同步的客戶端 Sctp 連線.
- NioSctpServerChannel, 非同步的 Sctp 伺服器端連線.
- OioSocketChannel, 同步的客戶端 TCP Socket 連線.
- OioServerSocketChannel, 同步的伺服器端 TCP Socket 連線.
- OioDatagramChannel, 同步的 UDP 連線
- OioSctpChannel, 同步的 Sctp 伺服器端連線.
- OioSctpServerChannel, 同步的客戶端 TCP Socket 連線.


3、handler(ChannelHandler handler)

Netty 的一個強大和靈活之處就是基於 Pipeline 的自定義 handler 機制. 基於此, 我們可以像新增外掛一樣自由組合各種各樣的 handler 來完成業務邏輯. 例如我們需要處理 HTTP 資料, 那麼就可以在 pipeline 前新增一個 Http 的編解碼的 Handler, 然後接著新增我們自己的業務邏輯的 handler, 這樣網路上的資料流就向通過一個管道一樣, 從不同的 handler 中流過並進行編解碼, 最終在到達我們自定義的 handler 中。

/**
 * 同樣也是 直接呼叫父類 AbstractBootstrap 的方法
 */
public B handler(ChannelHandler handler) {
    if (handler == null) {
        throw new NullPointerException("handler");
    }
    this.handler = handler;
    return self();
}

不過我們看到程式碼 一般都是這樣寫的

.handler(new ChannelInitializer<SocketChannel>() {
         @Override
         public void initChannel(SocketChannel ch) throws Exception {
             ChannelPipeline p = ch.pipeline();
             p.addLast(new EchoClientHandler());
        }
     })

那是因為Bootstrap.handler 方法接收一個 ChannelHandler, 而我們傳遞的是一個 派生於 ChannelInitializer 的匿名類, 它正好也實現了 ChannelHandler 介面. 我們來看一下, ChannelInitializer 類部分程式碼:

    /**
     * ChannelInboundHandlerAdapter 父類的父類 最終會繼承 ChannelHandler 
     * 那麼ChannelInitializer 也就是 ChannelHandler的 子類
     */
    public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter {

        private static final InternalLogger logger
            =InternalLoggerFactory.getInstance(ChannelInitializer.class);

        /**
         * 這裡只有這一個抽象類 所以我們只需重寫這一個方法就可以了
         */
        protected abstract void initChannel(C ch) throws Exception;

        @Override
        @SuppressWarnings("unchecked")
        public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
            initChannel((C) ctx.channel());
            ctx.pipeline().remove(this);
            ctx.fireChannelRegistered();
        }
   
    }

ChannelInitializer 是一個抽象類, 它有一個抽象的方法 initChannel, 我們正是實現了這個方法, 並新增的自定義的 handler 的. 那麼 initChannel 是哪裡被呼叫的呢?
答案是 ChannelInitializer.channelRegistered 方法中。
我們來關注一下 channelRegistered 方法. 從上面的原始碼中, 我們可以看到, 在 channelRegistered 方法中, 會呼叫 initChannel 方法, 將自定義的 handler 新增到 ChannelPipeline 中, 然後呼叫 ctx.pipeline().remove(this) 將自己從 ChannelPipeline 中刪除. 上面的分析過程, 可以用如下圖片展示:
一開始, ChannelPipeline 中只有三個 handler, head, tail 和我們新增的 ChannelInitializer.

接著 initChannel 方法呼叫後, 添加了自定義的 handler

最後將 ChannelInitializer 刪除


4、ChannelPipeline物件

   /**
     * 我們在initChannel抽象方法的實現方法中 通過 SocketChannel獲得 ChannelPipeline物件
     */
    ChannelPipeline p = ch.pipeline();
    p.addLast(newEchoClientHandler());

在例項化一個 Channel 時, 會伴隨著一個 ChannelPipeline 的例項化, 並且此 Channel 會與這個 ChannelPipeline 相互關聯, 這一點可以通過NioSocketChannel 的父類 AbstractChannel 的構造器:

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    unsafe = newUnsafe();
    //這個可以看出
    pipeline = new DefaultChannelPipeline(this);
}

當例項化一個 Channel(這裡以 EchoClient 為例, 那麼 Channel 就是 NioSocketChannel), 其 pipeline 欄位就是我們新建立的 DefaultChannelPipeline 物件, 那麼我們就來看一下 DefaultChannelPipeline 的構造方法。

public DefaultChannelPipeline(AbstractChannel channel) {
    if (channel == null) {
        throw new NullPointerException("channel");
    }
    this.channel = channel;

    tail = new TailContext(this);
    head = new HeadContext(this);

    head.next = tail;
    tail.prev = head;
}

我們呼叫 DefaultChannelPipeline 的構造器, 傳入了一個 channel, 而這個 channel 其實就是我們例項化的 NioSocketChannel, DefaultChannelPipeline 會將這個 NioSocketChannel 物件儲存在channel 欄位中。DefaultChannelPipeline 中, 還有兩個特殊的欄位, 即headtail, 而這兩個欄位是一個雙向連結串列的頭和尾. 其實在 DefaultChannelPipeline 中, 維護了一個以 AbstractChannelHandlerContext 為節點的雙向連結串列, 這個連結串列是 Netty 實現 Pipeline 機制的關鍵。

5、.connect(host, port)

經過上面的各種分析後, 我們大致瞭解了 Netty 初始化時, 所做的工作, 接下來 分析一下客戶端是如何發起 TCP 連線的。

   /**
     * 1、 這裡 終於是Bootstrap 自己的方法了。 傳入IP 地址 和 埠號
     */
    public ChannelFuture connect(String inetHost, int inetPort) {
        //通過InetSocketAddress 建構函式 繫結 IP地址+埠號
        return connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
    }

   /**
     * 2、上面呼叫該方法 ,該方法在呼叫 doResolveAndConnect方法
     */
    public ChannelFuture connect(SocketAddress remoteAddress) {
        if (remoteAddress == null) {
            throw new NullPointerException("remoteAddress");
        }
        validate();
        return doResolveAndConnect(remoteAddress, config.localAddress());
    }

   /**
     * 3、這步 例項化 Channer
     */
    private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
      
        //注意 這裡 initAndRegister()方法就是例項化 Channer 的方法 上面說過 真正獲取Channer 物件 是在這步獲取的
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();

           // 這裡省略的 很大一部分邏輯判斷的程式碼
            return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());   
    }

    /**
     * 3.1 這裡 就開始 調 newChannel() 方法 也就建立了 Channel 物件
     */
    final ChannelFuture initAndRegister() {
        Channel channel = null;
                channel = channelFactory.newChannel();
        return regFuture;
    }

    /**
     * 4、在看doResolveAndConnect0方法
     *    這一步還是對一些 引數資料 進行校驗  省略了校驗程式碼
     */
    private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
                                               final SocketAddress localAddress, final ChannelPromise promise) {
           // 獲取 當前 EventLoop執行緒
            final EventLoop eventLoop = channel.eventLoop();
            final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop);
            final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress);
                   //這一步 才是 連線的關鍵
                    doConnect(resolveFuture.getNow(), localAddress, promise);
           
        return promise;
    }

接下來看重要的方法,在 connect 中, 會進行一些引數檢查後, 最終呼叫的是 doConnect 方法,有關doConnect之後接下來原始碼,等自己對Netty瞭解更細緻之後 ,再來寫吧。

這裡推薦一個博主,有關Netty原始碼分析的蠻好的:原始碼之下無祕密 ── 做最好的 Netty 原始碼分析教程






如果一個人充滿快樂,正面的思想,那麼好的人事物就會和他共鳴,而且被他吸引過來。同樣,一個人老帶悲傷,倒黴的事情也會跟過來。

                                                  ——在自己心情低落的時候,告誡自己不要把負能量帶給別人。(大校13)