Netty源碼分析系列之服務端Channel初始化

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,即可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。

微信公衆號

問題

老規矩,Netty的源碼很難、很複雜,爲了更快的學懂新的知識,所以還是帶着問題來學習源碼。
Netty作爲一款基於事件驅動的高性能網絡框架,其底層實際上仍然使用的是JDK裏面的NIO,Netty在JDK的NIO上做了大量優化,以及封裝,降低了開發人員使用NIO的難度。

使用JDK原生的NIO進行網絡編程時,首先得做兩件事:1. 創建以及初始化ServerSocketChannel,2. 將ServerSocketChannel綁定到多路複用器Selector上。既然說Netty對JDK的NIO做了封裝,那麼在Netty中是什麼時候進行這兩步操作的呢?(在Netty中服務端的Channel是NioServerSocketChannel)

答案顯然就是:在Netty服務端啓動的時候進行的。本文接下來就結合源碼來看看Netty是如何進行服務端Channel的初始化,關於綁定到Selector的源碼分析會發布在下一篇文章中。

組件說明

在看Netty服務端啓動流程的源碼之前,先來簡單介紹下Netty中相關的組件。這些組件很重要,後面會單獨針對每一個組件寫文章進行詳細說明,今天只是先簡單介紹一下。

首先是NioEventLoopGroup,看英文名,翻譯過來就是一個NIO事件輪詢組。可以簡單理解爲它是一個線程組,它裏面包含了一組線程,這些線程後續用來執行客戶端的接入、IO數據讀寫等任務。

NioEventLoop,可以簡單理解爲它是一個線程,多個NioEventLoop合起來就是一個NioEventLoopGroup。

Pipeline,它是由ChannelHandlerContext類型的元素組成的一個雙向鏈表,ChannelHandlerContext裏面包裝了一個ChannelHandler。對於服務端而言,每當有新連接接入時,都會通過Pipeline來傳播。這個組件十分重要,在實際工作中,使用Netty實現自己自定義的業務邏輯,就是通過修改Pipeline來實現的。

服務端啓動

先來看一段服務端啓動的demo代碼。
Demo

在示例代碼中,①~⑤都是爲Netty服務端設置一些屬性,比較簡單。需要額外說明的是第②步。在第②步中,對服務端而言,需要設置channel的類型爲NioServerSocketChannel。當調用channle(NioServerSocketChannel.class)方法時,會調用到AbstractBootstrap類的channel()方法,它乾的事情就是創建一個一個ReflectiveChannelFactory工廠,並將ReflectiveChannelFactory實例賦值給AbstractBootstrap類的channelFactory屬性。

對於ReflectiveChannelFactory而言,它有一個Constructor類型的屬性,叫做constructor,它就是後續用來創建Channel的反射構造器。對於此處而言,constructor就是NioServerSocketChannel.class的反射構造器,通過它就能創建出一個NioServerSocketChannel的實例對象。
channel()方法的源碼
channel()

ReflectiveChannelFactory構造器的源碼
ReflectiveChannelFactory

在第⑥步中,會調用ServerBootstrap的bind()方法,這個方法中傳入了一個端口號:8080。從外面看,這個方法及其簡單,但是它乾的事情非常多。在這個方法中會初始化服務端的channel,註冊channel到selector上,然後綁定端口,啓動服務端。下面來通過分析它的源碼,來看下服務端Channel是如何進行初始化和綁定操作的。

當調用ServerBootstrap.bind(8080)時,會調用到ServerBootstrap.doBind(),下面我簡化了一下doBind()方法的源碼,只保留了核心代碼,如下。
doBind()

在doBind()方法中調用了兩個非常重要的方法,一個是initAndRegister()方法,它用來進行Channel的初始化和註冊到Selector上的操作;另外一個是doBind0()方法,它用來進行端口號的綁定操作。今天只分析initAndRegister()方法的前半部分。

channel初始化

initAndRegister()的源碼如下。
initAndRegister()

在initAndRegister()方法中,會先通過調用channelFactory.newChannel()來創建一個Channle,對於服務端而言,這裏創建的就是NioServerSocketChannel。那麼它是如何創建的呢?

首先這裏的channelFactory就是前面在②處提到的ReflectiveChannelFactory,它裏面包含一個屬性constructor。這個constructor就是NioServerSocketChannel.class的反射構造器,通過調用constructor.newInstance()方法,就會調用到NioServerSocketChannel類的無參構造方法。NioServerSocketChannel的無參構造方法如下。

public NioServerSocketChannel() {
    // DEFAULT_SELECTOR_PROVIDER的值就是SelectorProvider.provider()
    // 先調用newSocket()
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

在無參構造方法中會先調用newSocket()方法,該方法就是通過JDK原生的API創建一個ServerSocketChannel。

private static ServerSocketChannel newSocket(SelectorProvider provider) {
    try {
        // provider的值就是SelectorProvider.provider();
        // 這裏就是直接調用JDK中NIO的原生API,創建ServerSocketChannel
        return provider.openServerSocketChannel();
    } catch (IOException e) {
        throw new ChannelException(
                "Failed to open a server socket.", e);
    }
}

當調用完newSocket()方法後,會返回一個ServerSocketChannel,然後接着會調用this(ServerSocketChannel),即NioServerSocketChannel類的一個有參構造器。

public NioServerSocketChannel(ServerSocketChannel channel) {
	// 調用父類的構造器,SelectionKey.OP_ACCEPT的值爲1,表示的是服務的channel感興趣的事件是接收事件
    super(null, channel, SelectionKey.OP_ACCEPT);
    // config是用來保存服務端channel相關的配置
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

接着會一直往上調用父類的構造器,最終會調用到AbstractNioChannel類的構造器中。

protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    // 繼續調用父類
    super(parent);
    this.ch = ch;
    // 保存感興趣的事件,前面傳過來的值是OP_ACCEPT,表示感興趣的時間是接收事件,
    // 這裏只是保存,並不是意味着此時就是要開始接收新連接了
    this.readInterestOp = readInterestOp;
    try {
    	// 設置爲非阻塞模式,這個JDK的原生的NIO是一致的
        ch.configureBlocking(false);
    } catch (IOException e) {
        // ......
    }
}

在這裏會先調用父類的構造器,再保存channle、readInterestOp以及設置channel爲非阻塞模式。注意這裏只是保存了readInterestOp的值,並不是意味着此時就是要開始接收新連接了,因爲此時服務端還沒有綁定端口。再繼續往上看父類的構造器幹了哪些事情。
最終會調用到AbstractChannel的構造器。

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}

在AbstractChannel的構造器主要乾了三件事。
第一:給channel的id賦值,這個id就是一個唯一標識符,用處不大。
第二:通過newUnsafe()創建了一個NioMessageUnsafe對象,這個對象是服務端後面負責用來接收連接等操作的。看到Unsafe是不是立馬能想到JDK中的Unsafe類,它的作用類似,可以直接操作內存,功能很強大。
第三:通過newChannelPipeline()創建了一個Pipeline,後面所有新連接的接入,都需要經過該Pipeline進行傳播。Pipeline的初始化如下,會初始化一個頭結點和尾結點。

protected DefaultChannelPipeline(Channel channel) {
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
    succeededFuture = new SucceededChannelFuture(channel, null);
    voidPromise =  new VoidChannelPromise(channel, true);

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

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

此時Pipeline的結構如下。
pipeline結構圖

此時,NioServerSocketChannel的構造方法終於執行完了,回到initAndRegister()方法中,此時channel對象已經有值了,接下來就是調用init()方法進行channel的初始化操作了。當調用init()方法時,會調用ServerBootstrap的init()方法。在init()方法中,先是獲取到了前面②等步驟中設置的一些和服務端Channel相關的option、attrs(本文demo中沒設置)等配置,以及和客戶端channel相關的childOptions、childAttrs等配置,代碼比較簡單,這裏就不一一展開了。

init()方法最核心的邏輯在最後一行:p.addLast()。前面在創建NioServerSocketChannel時,對channel中的pipeline進行的初始化操作,這裏當調用p.addLast()時,會向pipeline中在添加一個ChannelHandler。下面是init()方法簡化後的代碼。

void init(Channel channel) throws Exception {
    // ......
    ChannelPipeline p = channel.pipeline();
	// 添加了一個匿名類   
    p.addLast(new ChannelInitializer<Channel>() {
    	// 後面會回調到initChannel()方法
        @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            // config.handler()獲取到handler就是我們在步驟④中指定的NettyServerHandler類
            ChannelHandler handler = config.handler();
            if (handler != null) {
            	// 添加到pipeline中
                pipeline.addLast(handler);
            }
            // 添加成功後,向NioEventLoop中添加一個任務,最終就執行到裏面的run()方法
            ch.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                	// 在run()方法中又向pipeline中添加了一個ChannelHandler
                    pipeline.addLast(new ServerBootstrapAcceptor(
                            ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                }
            });
        }
    });
}

此時向pipeline中添加的是一個匿名類:new ChannelInitializer<Channel>(){},注意此時並不會執行這個匿名類中的initChannel()方法。添加完成後,pipeline的結構變成了如下圖所示的結構。
pipeline結構圖

當後面channel註冊到Selector上後,會回調到ChannelInitializer的initChannel(ChannelHandlerContext ctx)方法上(注意這裏initChannel()方法是一個重載方法)。源碼如下。

private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
    if (initMap.add(ctx)) { // Guard against re-entrance.
        try {
        	// 回調匿名類的initChannel()方法
            initChannel((C) ctx.channel());
        } catch (Throwable cause) {
            // Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...).
            // We do so to prevent multiple calls to initChannel(...).
            exceptionCaught(ctx, cause);
        } finally {
        	// 最後將pipeline中的這個匿名類刪除
            ChannelPipeline pipeline = ctx.pipeline();
            if (pipeline.context(this) != null) {
                pipeline.remove(this);
            }
        }
        return true;
    }
    return false;
}

在initChannel(ChannelHandlerContext ctx方法上,就會回調到上面pipeline中匿名類的initChannel(final Channel ch)方法。在匿名類的initChannel()方法的邏輯中,會先向pipeline中添加一個我們在步驟④中指定的NettyServerHandler類,添加完後,pipeline就會變成如下結構。
pipeline結構圖

當添加完NettyServerHandler後,接着通過ch.eventLoop().execute(new Runnable(){})向NioEventLoop中添加了一個任務,這個任務此時不會立刻執行,而是會在另外的線程中異步執行。當匿名類的initChannel(final Channel ch)方法執行完成後,就會回到initChannel(ChannelHandlerContext ctx)方法中,最後會執行finally語句塊。在finally中執行了一段代碼。

ChannelPipeline pipeline = ctx.pipeline();
if (pipeline.context(this) != null) {
    pipeline.remove(this);
}

這段代碼的作用,就是將前面創建的那個匿名類,從pipeline中移除。移除完以後pipeline的結構就變爲了如下圖所示。爲什麼要刪除呢?因爲這個匿名類的只是用來在啓動階段向pipeline中添加元素的,當服務器啓動以後,pipeline的結構已經固定了,這個匿名類就沒有存在的意義了,因此會把它刪除。
pipeline結構圖

由於前面向NioEventLoop中添加了一個任務,當這個任務被執行時,又會向pipeline中添加一個元素:ServerBootstrapAcceptor。這個類很重要,它就是後面用來負責所有新連接的接入的。因爲後面所有的新連接都會先經過服務端channel的pipeline,而ServerBootstrapAcceptor又是pipeline中一個節點,所以後面所有的新連接都要經過ServerBootstrapAcceptor的處理。最終pipeline的結構如下圖所示。(重要的事情說三遍:​這個圖很重要,牢記!牢記!牢記!​)

pipeline結構圖

總結

  • 本文主要結合源碼分析了服務端的NioServerSocketChannel的初始化過程,但沒有分析NioServerSocketChannel如何註冊到Selector上,以及服務端端口綁定的流程也沒有分析,這兩部分的內容會在後面兩篇文章中詳細分析。
  • 在分析NioServerSocketChannel初始化的過程中,着重分析了pipeline的變化過程以及最終的結構,這對於後面分析新連接的接入過程會有很大的幫助。
  • 最後,Netty源碼很複雜,它的啓動流程非常重要,代碼也很複雜。類的繼承關係十分複雜,所以建議讀者親自動手Debug調試,這對學習Netty會有很大幫助。

微信公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章