Netty服務端啓動源碼解析

什麼是Netty

  •  異步事件驅動框架,用於快速開發高性能服務端和客戶端
  •  封裝了JDK底層BIONIO模型,提供高度可用的API
  •  自帶編解碼器解決拆包粘包問題,用戶只用關心業務邏輯
  •  精心設計的reactor線程模型支持高併發海量連接
  •  自帶各種協議棧讓你處理任何一種通用協議都幾乎不用親自動手

Netty的抽象流程

   首先,我們給出一張簡單的socket運行流程,並用netty進行抽象描述,可如下圖表示:

   圖中的每一個步驟,下面都對應的netty的一個組件,此處就不多做描述了,各位自行理解。

Demo代碼

public final class Server {

    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue")
                    .handler(new ServerHandler())
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new AuthHandler());
                            //..

                        }
                    });

            ChannelFuture f = b.bind(8888).sync();

            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

  上面即是我本次用到的demo,採用的maven管理jar包,netty版本用到的是4.1.6.Final。

Netty服務端啓動源碼分析

1. 創建服務端Channel

  首先調用bind()方法設置綁定端口,即用戶代碼的入口,bind()方法中調用initAndRegister()方法初始化並註冊,而在initAndRegister()方法中,則使用了newChannel()方法創建服務端channel。在netty中創建服務端channel的方法,採用到了反射的機制。我們追蹤源碼跟進去就可以看到,如下:

       bind方法中,returndoBind方法,doBand方法中調用initAndRegister()方法,點進去看initAndRegister()內,可以看到如下:

  在initAndRegister()方法中,又調用了newChannel()方法,創建完服務端的channel後,再調用init(channel)用來初始化,我們先追蹤newChannel方法,如下圖:


  從newChannel()方法,我們可以知道,clazz.newInstance()是通過反射機制來創建channel,而我們的DEMO中,clazz類是傳過來的,如圖:

  從上圖,可知道我們將NioServerSocketChannel.class傳遞給了channel方法處理,點進去channel方法,我們可以看到如下:

  該方法,會將channelClass封裝成一個ReflectiveChannelFactory,即把channelClass這個類傳遞給RefectiveChannelFactory構造方法,通過反射的方式來創建channel,而這個channelClass就是上面講到的NioServerSocketChannel.class,我們先進去ReflectiveChannelFactory的構造方法,如下:

  可以看出該方法實際上就是返回了傳過來的NioServerSocketChannel,即調用了其無參構造函數。而至於在這個構造函數將幹什麼事,我們可以追蹤下NioServerSocketChannel這個類。如下所示:

  在這個無參構造函數,通過調用newSocket()方法,來創建底層的JDK底層的channel,點進去可以看到該方法,主要是通過SelectorProvider去調用JDK底層的openServerSocketChannel方法,從而去創建服務端的ServerSocketChannel,而這個ServerSocketChannel實際上就是JDK底層的channel

  上述基本就將服務端的channel創建完畢了,接下了查看其TCP參數的配置類,在NioServerSocketChannel下,同樣可以看到下圖代碼:

  該配置類也主要是通過構造函數來完成,將JDK創建完成的channel傳進去,這個Config就是爲了後續對TCP底層參數獲取的時候,可以通過這個Config配置,config可以是serverSocket配置的一個抽象。

  在config配置上方,我們可以看到該構造函數調用了父類的構造函數,點擊進去,接下來分下下父類的構造函數,我們對父類構造函數進行追蹤:


  此處傳進來的channel,是jdk底層創建完成的channelch.configureBlocking(false)這句話表明設置服務端的channel是一個非阻塞的過程。另外要注意的是,不過服務端的channel還是客戶端的channel都繼承一個AbstractChannel,另外點進去查看super(parent),如下所示:

  該構造函數主要是創建了三個屬性,一個id就是channel的唯一標識,unsafe就是對應channel底層tcp讀寫相關操作的一個類(不建議直接拿來用),pipeline是服務端和客戶端邏輯相關的一個邏輯鏈。

  最後給出一個大綱,來粗略瞭解下NioServerSocketChannel類的構造函數主要做哪些事情。如下圖所示:

  首先通過newSocket()方法創建JDL底層的channel,接下來創建跟channel相關的config類,這個config類主要是跟channel相關的tcp參數的一些配置,然後,調用configureBlocking(false)這個方法,將服務端channel設置成非阻塞模式,最後在channel上創建pipelin到此,創建Channel的部分就講解完畢。

2. 初始化服務端Channel

  初始化的大體方法調用,如下:

  從上面這個大綱圖,一一進行源碼追蹤。我們從上一小節中,initAndRegister()方法中的init()方法用來初始化channel,現在我們來追蹤下init的源碼,點擊進入serverBootStrap的實現類:


  這方法中,主要包含兩個過程,一個獲取用戶自定義配置的Options,然後綁定到channelconfig配置中,一個獲取用戶自定義的屬性,然後綁定channel的屬性中。然後看下面的:

  上標記的操作其實都是一些簡單操作,主要也是將用戶自定義的選項和屬性,用兩個局部變量進行保存。屬性配置完成之後,拿到服務端的piplinepipline是在創建serverSocketChannel的時候創建的,把上面保存的兩個屬性傳進去給pipline,代碼如下:


      pipeline還需要將用戶自定義的handler處理鏈添加進去,配置進去後,在接下來的代碼中,我們可以看到netty將自動添加連接器ServerBootstrapAcceptor,也相當於自帶的handler,其中後面兩個屬性,是之前所定義的,而currentChildHandlernetty使用最外層鏈式調用的childHandler()方法,也是用戶自定義的,而currentChildGroup,也是由最外層用戶自定義確定的,如下圖:

  這圖上的代碼,也是我用來分析NettyDemo。總的來說,初始化channel,保存用戶自定義的屬性,根據這zdn屬性創建一個連接接入器,這個鏈接接入器每次accept上新的連接之後,都會使用這些屬性對新的連接進行配置。

3. 註冊selector

  服務端的channel創建初始化完成之後,需要將channel註冊到事件輪詢器seletor上面去,回到initAndRegister()方法,查看到下面這條語句:

  這個方法最終會調用到AbstractChannelregister方法,這個方法也是我們的入口:

        AbstractChannel.this.eventLoop = eventLoop這句簡單的賦值操作,告訴channel後續所有的IO事件相關的操作,都是交給eventLoop來處理, register0(promise)就是做一個實際的註冊,進入register0,會發現它實際就只做了三件事情:


  三件事就如同上述標註的一樣,關注點主要在register方法上,點進去看abstractNioChannel方法,如下:

  從中,我們可以看到netty的註冊最終調用的還是jdk底層的register方法,如上面講述到的,javachannel在創建服務端channel的時候,會創建jdk底層相關的channel,然後對這個channel保存到成員變量裏面,保存的代碼如下:

  然後,我們接着看javachannelregister方法,該方法三個參數,第一個是需要註冊到的selector,第二個opts是我們需要關心的事件,此處,不需要關注,輸入的0,代表不關心任何事件,只是單獨地把channel綁定到selector上面去,最後this屬性是就意味着是服務端的channel,當selector輪詢到java上面的讀寫事件,可以直接把這個this,也就是這個綁定的channel,針對這個nettyniochannel做一個事件的傳播。

  再回頭看後面兩個事情,其實,可以對應我們demo入口的handler

  進入ServerHandler(),如下:

  運行下代碼,會看到下面這輸出:


  這輸出,基本上就是對應pipeline的兩個過程,但要注意的是channelActive並不是對應pipelinefireChannelActive方法,因爲netty在這個過程只是做一個selector綁定操作,並沒有綁定端口,所以isActive返回的是false

  最後,總結下綁定的過程:

  先通過AbstractChannel.register(channel)作爲一個入口,register方法中,將調用到eventLoop進行線程的綁定,同時調用register0方法進行實際註冊,register0方法中,又實踐上實現doRegister方法調用jdk底層進行註冊,將當前channel綁定到selector上面去,同時調用invokeHandlerAddedIfNeeded方法和fireChannelRegistered方法進行事件傳播,最終傳播到我們的用戶方法。

4. 端口綁定

  先來張總結圖大綱:

  調用doBind()方法,將端口實際綁定到本地,doBind方法將會調用的jdkjavaChannel.doBind()方法,通過jdk底層的channel進行端口的綁定,端口綁定成功後,調用pipeline.fireChannelActive(),傳播一個active事件,在傳播的時候,會從HeadContext開始觸發,HeadContext把這個事件向前傳播的時候,最終會調用到HeadContext.readIfIsAutoRead()方法,而這個方法將會觸發read事件,而這個read事件通過層層調用,最終會調用到服務端channelbeginRead方法,而這個beginRead方法完成向selector註冊一個accept事件。

     HeadContext.readIfIsAutoRead()方法將一個之前註冊到selector上的事件,重新綁定成accept事件,有新連接進來,selector就會輪詢到accept事件,最終就會將這個鏈接交給netty處理。

  進入NioServerSocketChannel類中的doBind方法,如下:

  這個方法主要負責兩個事件,一個是doBind()方法,一個是調用pipeline方法fireChannelActive底層的事件,其中doBind()方法中代碼如下:

  要注意的是,當端口綁定之前wasActive的值爲false,綁定完成後,isActive()返回爲true,此時需要將active事件進行傳播,會調用到下面這方法:

  即先從HandlerContext方法先調用,傳播過程中,會傳播到:

  點進去看doBeginRead方法:

  首先拿到selectionKey,該值就是之前註冊服務端channelselector上去的返回值,接下來拿到selectionKey的感興趣事件,註冊服務端channel上的,實際上是綁定一個0seletor上去,所以獲取到的IntersetOp0,判斷即爲獲取到的變量是否和獲取到的accept事件判斷。前面註冊一個事件,在這個事件上再增加一個事件。也可理解爲,服務端端口綁定成功後,需要告訴seletor,我需要關心一個accept事件,selector如果輪詢到一個Accept事件的時候,就會把這個事件交給netty處理。

  當你端口綁定完成後,會觸發一個active事件,這個active事件最終會調用到channel的一個read事件,read對服務端channel來說,就是我可以讀一個新的連接了。

  以上就是對netty服務端demo啓動源碼的整體分析,可能有很多地方沒解釋周到,大家可以跟進源碼進行追蹤,共同交流。

  最後,謝謝閱讀。




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