深入學習Netty(4)——Netty程式設計入門

JJian發表於2021-07-20

前言

  從學習過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、高效能的序列化框架等。

 

相關文章