深入學習Netty(4)——Netty編程入門

前言

  從學習過BIO、NIO、AIO編程之後,就能很清楚Netty編程的優勢,爲什麼選擇Netty,而不是傳統的NIO編程。本片博文是Netty的一個入門級別的教程,同時結合時序圖與源碼分析,以便對Netty編程有更深的理解。

  在此博文前,可以先學習瞭解前幾篇博文:

  參考資料《Netty In Action》、《Netty權威指南》(有需要的小夥伴可以評論或者私信我)

  博文中所有的代碼都已上傳到Github,歡迎Star、Fork

 

一、服務端創建

Netty屏蔽了NIO通信的底層細節,減少了開發成本,降低了難度。ServerBootstrap可以方便地創建Netty的服務端

1.服務端代碼示例

public void bind (int port) throws Exception {
        // NIO 線程組
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        // Java序列化編解碼 ObjectDecoder ObjectEncoder
                        // ObjectDecoder對POJO對象解碼,有多個構造函數,支持不同的ClassResolver,所以使用weakCachingConcurrentResolver
                        // 創建線程安全的WeakReferenceMap對類加載器進行緩存SubReqServer
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 半包處理 ProtobufVarint32FrameDecoder
                            socketChannel.pipeline().addLast(new ProtobufVarint32FrameDecoder());
                            // 添加ProtobufDecoder解碼器,需要解碼的目標類是SubscribeReq
                            socketChannel.pipeline().addLast(
                                    new ProtobufDecoder(SubscribeReqProto.SubscribeReq.getDefaultInstance()));
                            socketChannel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
                            socketChannel.pipeline().addLast(new ProtobufEncoder());
                            socketChannel.pipeline().addLast(new SubReqServerHandler());
                        }
                    });
            // 綁定端口,同步等待成功
            ChannelFuture f = bootstrap.bind(port).sync();
            // 等待所有服務端監聽端口關閉
            f.channel().closeFuture().sync();
        } finally {
            // 優雅退出,釋放線程池資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();

        }

    }

2.服務端時序圖

(1)創建ServerBootstrap實例

  是Netty服務端啓動的輔助類,提供了一系列的方法用於設置服務端自動相關參數,降低開發難度;

(2)設置並綁定Reactor線程池

  Netty的Reactor線程池(I/O 複用 + 線程池)是EventLoopGroup,實際上就是EventLoop數組,EventLoop處理所有註冊到本線程的多路複用器Selector上的Channel,Selector的輪詢操作由綁定的EventLoop線程run方法驅動,在一個循環體內循環執行。EventLoop不僅執行I/O事件,也能執行用戶自定義的Task和定時任務Task

(3)設置並綁定服務端Channel

需要創建ServerSocketChannel,對應的實現類就是NioServerSocketChannel。ServerBootstrap的channel方法用於指定服務端的Channel類型

 

通過反射創建NioServerSocketChannel對象

  

通過調用無參默認的構造方法生成channel

 

(4)創建並初始化ChannelPipeline

  本質上是一個負責處理網絡事件的職責鏈,負責管理與執行ChannelHandler。 ChannelPipeline爲ChannelHandler鏈提供了容器,並定義了用於在該鏈上傳播入站和出站事件流的API。當Channel被創建時,他會自動的分配到它專屬的ChannelPipeline。典型的網絡事件包括:

  • 鏈路註冊
  • 鏈路激活
  • 鏈路斷開
  • 接收到請求消息
  • 處理請求消息
  • 發送應答消息
  • 鏈路發生異常
  • 發送用戶自定義事件

(5)添加ChannelHandler

  這是Netty提供給用戶定製與擴展的關鍵接口,利用此可以完成大部分的功能定製。如:碼流日誌打印LoggingHandler、基於長度的半包解碼器LengthFiledBasedFrameDecoder...

(6)綁定並啓動監聽端口

  將ServerSocketChannel註冊到Selector上監聽客戶端連接

 

 

(7)Selector輪詢

  由Reactor線程NioEventLoop負責調度和執行Selector輪詢操作,選擇準備好就緒的Channel集合。

(8)調度執行ChannelHandler

  當輪詢到準備就緒的Channel之後,就由Reactor線程NioEventLoop執行ChannelPipeline的相應方法,最終調度並執行ChannelHandler。

     

(9)執行網絡事件ChannelHandler

  執行用戶自定義的ChannelHandler或系統ChannelHandler,ChannelPipeline會根據事件類型,調度並執行ChannelHandler。

    

3.服務端源碼分析

(1)創建NioEventLoopGroup線程組

 首先通過構造函數創建ServerBootstrap實例,隨後創建兩個EventLoopGroup:

   

  NioEventLoopGroup其實就是Reactor線程池,負責調度和執行客戶端接入、網絡讀寫事件,用戶自定義任務和定時任務的執行,通過ServerBootstrap的group方法傳入

 

 其中父NioEventLoopGroup被傳入父構造函數中

 

 該方法主要是處理各種設置I/O線程、執行和調度網絡事件的讀寫。

(2)創建NioServerSocketChannel

  線程組設置完成後,需要創建NioServerSocketChannel。根據Channel的類型(channelClass)通過反射創建Channel實例(調用newInstance()方法)

  

      

     

(3)設置TCP參數

 作爲服務端主要是設置TCP backlog參數:

 int listen(int sockfd, int backlog);

  

 

      

  backlog指定了內核爲此套接口排隊的最大連接個數。在服務端要接收多個客戶端發起的連接,因此必不可少要使用隊列來管理這些連接。其中在TCP三次握手中有兩個隊列,分別是半連接狀態隊列和全連接隊列

  • 半連接狀態隊列:每個客戶端發來的SYN報文,服務器都會把這個報文放到隊列裏管理,這個隊列就是半連接隊列,即SYN隊列,此時服務器端口處於SYN_RCVD狀態。之後服務器會向客戶端發送SYN+ACK報文。
  • 全連接狀態隊列:當服務器接收到客戶端的ACK報文後,就會將上述半連接隊列裏面對應的報文轉移(注:其實不是同一個結構,會新建一個結構掛到全連接隊列裏)到另一個隊列裏管理,這個隊列就是全連接隊列,即ACCEPT隊列,此時服務器端口處於ESTABLISHED狀態。

 放一張來自網絡的圖:

    

   backlog被規定爲兩個隊列總和的最大值,Netty默認的目的backlog200

 

(4)爲啓動輔助類和其父類分別設置Handler

  childHandler是NioServerSocketChannel對應ChannelPipeline的Handler;父類中的Handler是客戶端新接入的連接SocketChannel對應的ChannelPipeline的Handler

 

  本質區別就是:ServerBootstrap中的Handler是NioServerSocketChannel使用的,所有連接該監聽端口的客戶端都會執行它;父類AbstractBootstrap中的Handler是個工廠類,會爲每個新接入的客戶端創建一個新的Handler

二、客戶端創建

1.客戶端代碼示例

public void connect (String host, int port) throws Exception {
        // NIO 線程組
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 處理半包的ProtobufVarint32FrameDecoder一定要在解碼器前面
                            socketChannel.pipeline().addLast(new ProtobufVarint32FrameDecoder());
                            // 添加ProtobufDecoder解碼器,需要解碼的目標類是SubscribeResp
                            socketChannel.pipeline().addLast(
                                    new ProtobufDecoder(SubscribeRespProto.SubscribeResp.getDefaultInstance()));
                            socketChannel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
                            socketChannel.pipeline().addLast(new ProtobufEncoder());
                            socketChannel.pipeline().addLast(new SubReqClientHandler());

                        }
                    });

            // 發起異步連接操作
            ChannelFuture f = bootstrap.connect(host, port).sync();
            // 等待所有服務端監聽端口關閉
            f.channel().closeFuture().sync();
        } finally {
            // 優雅退出,釋放線程池資源
            group.shutdownGracefully();

        }

    }

負責處理網絡讀寫、連接和客戶端請求接入的Reactor線程就是NioEventLoop

2.客戶端時序圖

(1)創建Bootstrap實例

(2)創建客戶端連接,創建線程組NioEventLoopGroup(線程數默認爲CPU內核數2倍)

(3)通過ChannelFactory工廠和指定的NioSocketChannel.class類型創建用於客戶端連接的NioSocketChannel

(4)創建默認的ChannelHandlerPipeline,用於調度與執行網絡事件

(5)異步發起TCP連接,判斷連接結果,如果成功則將NioSocketChannel註冊到Selector上並置selectionKeyOP_READ,監聽讀操作,如果沒有立即成功,則可能是服務端還沒有立刻返回ACK,所以此時將連接監聽位註冊到Selector上,同時selectionKeyOP_CONNECT,監聽連接,等待結果

(6)註冊對應的監聽狀態位到Selector

(7)Selector輪詢各NioSocketChannel,處理連接結果

(8)如果連接成功則發送成功事件,觸發ChannelPipeline執行

(9)ChannelPipeline調度執行ChannelHandler(包括系統與用戶自定義),執行具體業務邏輯

3.客戶端源碼分析

(1)客戶端連接輔助類Bootstrap

BootstrapNetty提供的客戶端連接工具類,用於簡化客戶端的創建

1)設置I/O線程組:

客戶端相對於服務端,只需要一個處理I/O讀寫的線程組即可。由Bootstrapgroup方法提供,主要設置EventLoopGroup

 

          

2)設置TCP參數

創建客戶端套接字的時候通常都會設置連接參數:接收和發送緩衝區大小、連接超時時間等。

主要的TCP參數如下:

 

3)指定Channel

對於TCP客戶端連接,默認使用NioSocketChannel,創建過程跟服務端是大同小異的。

4)發起客戶端連接

具體請看下面

(2)客戶端連接操作

1)創建初始化NioSocketChannel,主要邏輯是initAndRegister方法

 

   

2)註冊到Selector上,主要邏輯是register方法

 

        

3)鏈路成功後發起TCP連接

先獲取EventLoop線程組

然後進入doConnect()方法,調用NioSocketChannel異步發起connection

Connect操作後有三種可能:

第一是連接成功

第二種是暫時沒連接上,服務端沒有返回ACK,結果暫時不確定,這時候需要將selectionKey設置爲OP_CONNET,監聽連接結果。

 

第三種是連接失敗,直接拋出異常

 

異步連接成功以後,調用fulfillConnectPromise方法,觸發鏈路激活事件,如果連接成功則觸發ChannelActive事件

此時ChannelActive事件的主要作用就是將selectionKey設置爲OP_READ事件

 

(3)異步連接結果通知

調用processSelectedKey方法,Selector輪詢客戶端連接Channel

當服務端返回握手應答以後,對連接結果進行判斷,主要調用finishConnect方法

進入finishConnect方法:

 

doFinishConnect方法主要判斷JDKSocketChannel連接結果

連接成功後進入fullfillConnectPromise方法,調用fulfillConnectPromise方法,觸發鏈路激活事件,如果連接成功則觸發ChannelActive事件:

(4)客戶端連接超時機制

JDK沒有提供連接超時機制,Netty利用定時器提供客戶端連接超時控制

option方法中傳入TCP超時配置

 

一旦定時器執行超時,說明客戶端連接超時,這時候就構造超時異常,同時關閉客戶端連接,釋放句柄

  如果連接超時被設置,但是定時器執行的時候並沒有超時執行(在超時時間內完成),則此時connectedTimeoutFuture是不會爲null的,根據此判斷是否在超時時間內完成,如果完成則取消,避免再次觸發定時器,實際上不管連接成功與否,只要獲取到連接結果,都會刪除定時器

  

三、選擇Netty的好處

之所以選擇Netty編程,主要Netty的以下幾種優勢:

(1)API使用簡單,開發門檻低

(2)功能強大,預置了很多編解碼功能,支持多種主流協議

(3)定製能力強,可以通過ChannelHandler對通信框架進行靈活擴展

(4)性能高

(5)成熟、穩定,修復了已知所有的JDK NIO BUG

(6)社區活躍

(7)經過了大規模的商業應用考驗

當然,這些是顯而易見的優勢,但是需要從源碼中分析其優勢,比如Netty的零拷貝、基於內存池的ByteBuf、高性能的序列化框架等。

 

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