從一個Demo開始,揭開Netty的神祕面紗

阿丸發表於2021-04-19

本文是Netty系列第5篇

上一篇文章我們對於I/O多路複用、Java NIO包 和 Netty 的關係有了全面的認識。

到目前為止,我們已經從I/O模型出發,逐步接觸到了Netty框架。這個過程中,基本解答了Netty是什麼、為什麼使用Netty等前置問題。給我們學習Netty提供了最原始的背景知識。

有了這些做基礎,下面我們可以開始慢慢去揭開Netty的神祕面紗了。

從一個Demo開始,揭開Netty的神祕面紗

 

本文預計閱讀時間約 5分鐘,將重點圍繞以下幾個問題展開:

  • 如何用Netty編寫一個Server端服務Demo
  • 從Demo看Netty的邏輯架構,初識各個元件

1.編寫一個Server端Demo

1.1 基於主從Reactor模式的Demo實現

如果從來沒用過Netty,那麼瞭解一下用Netty編寫的Server端Demo是必不可少的。

還記得我們上一篇說的 “主從Reactor模式” 嗎?可以構建兩個 Reactor,主 Reactor 單獨監聽server socket,accept新連線,然後將建立的 SocketChannel 註冊給指定的從 Reactor,從Reactor再執行事件的讀寫、分發,把業務處理就扔給worker執行緒池完成。

從一個Demo開始,揭開Netty的神祕面紗

 

我們就按照這個模式,用Netty編寫一個服務端程式吧。

直接上程式碼!

一個簡單的自定義ChannelHandler類,用來自定義業務處理邏輯:

從一個Demo開始,揭開Netty的神祕面紗

 

一個包含Bootstrap的服務端啟動類:

public class EchoServer {
    private int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        new EchoServer(8833).start();
    }

    public void start() throws Exception {
        //1.Reactor模型的主、從多執行緒
        EventLoopGroup mainGroup = new NioEventLoopGroup();
        EventLoopGroup childGroup = new NioEventLoopGroup();

        try {
            //2.構造引導器例項ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();
            b.group(mainGroup, childGroup)
                    .channel(NioServerSocketChannel.class) //2.1 設定NIO的channel
                    .localAddress(new InetSocketAddress(port)) //2.2 配置本地監聽埠
                    .childHandler(new ChannelInitializer<SocketChannel>() { //2.3 初始化channel的時候,配置Handler
                        @Override
                        protected void initChannel(final SocketChannel socketChannel) {
                            socketChannel.pipeline()
                                    .addLast("codec", new HttpServerCodec())
                                    .addLast("compressor", new HttpContentCompressor())
                                    .addLast("aggregator", new HttpObjectAggregator(65536))
                                    .addLast("handler", new EchoServerHandler()); //2.4 加入自定義業務邏輯ChannelHandler
                        }
                    });
            ChannelFuture f = b.bind().sync(); //3.啟動監聽
            System.out.println("Http Server started, Listening on " + port);
            f.channel().closeFuture().sync();
        } finally {
            mainGroup.shutdownGracefully().sync();
            childGroup.shutdownGracefully().sync();
        }
    }
}

啟動後,通過curl呼叫,得到響應。

從一個Demo開始,揭開Netty的神祕面紗

 

Demo完成了!

對於之前覺得用Java NIO包實現起來很複雜的的 “主從Reactor模式” ,用Netty簡簡單單就完成了。

從一個Demo開始,揭開Netty的神祕面紗

 

只需要建立兩個EventLoopGroup,然後繫結到引導器ServerBootstrap上就好了.

mainGroup 是主 Reactor,childGroup 是從 Reactor。它們分別使用不同的 NioEventLoopGroup,主 Reactor 負責處理 Accept,然後把 Channel 註冊到從 Reactor 上,從 Reactor 主要負責 Channel 生命週期內的所有 I/O 事件。

1.2 Demo分析

從上面的Demo程式碼可以看出,對於所有用Netty編寫的服務端程式,至少需要兩個部分:

  • 至少一個ChannelHandler
  • Bootstrapping

1)ChannelHandler

這個元件用來實現對客戶端傳送過來的資料進行處理,可能包括編解碼、自定義業務邏輯處理等等。

對於ChannelHandler來說,有非常多的實現。在Demo中我們簡單使用了幾個Netty自帶的Handler,包括HttpServerCodec、HttpContentCompressor、HttpObjectAggregator,也使用了一個自定義的EchoServerHandler。

可以看到,對於Handler的使用,是非常重要也是非常方便的一個環節。我們會在以後的文章中詳細展開。

2)Bootstrapping

啟動程式碼部分。用來配置服務端的啟動引數,包括監聽埠、服務端執行緒池配置、網路連線屬性配置、ChannelHandler配置等等。

結合Demo來看,主要分為這幾個步驟:

  • 建立一個ServerBootstrap例項,用來引導啟動。
  • 建立一個(當我們使用主從Reactor模式時,需要建立兩個)NioEventLoopGroup例項來處理事件, 比如接受一個新的客戶端連線、讀寫資料等。
  • 指定一個埠,用來作為服務端的監聽埠。
  • 使用一系列channelHandler來初始化每個Channel,包括自定義業務邏輯實現的channelHandler。
  • 呼叫ServerBootstrap.bind() 來真正觸發啟動。

2. Netty的邏輯架構

通過上面的Demo演示,我們對 Netty 的使用已經有了一個大概的印象。

下面,我們根據Demo中使用的幾個元件,一起梳理一下 Netty 的邏輯架構。

從一個Demo開始,揭開Netty的神祕面紗

 

結合我們的Demo和這個邏輯架構圖,我們梳理下各個元件的流轉過程:

  • 服務端利用ServerBootstrap進行啟動引導,繫結監聽埠
  • 啟動初始化時有 main EventLoopGroup 和 child EventLoopGroup 兩個元件,其中 main EventLoopGroup負責監聽網路連線事件。當有新的網路連線時,就將 Channel 註冊到 child EventLoopGroup。
  • child EventLoopGroup 會被分配一個 EventLoop 負責處理該 Channel 的讀寫事件。
  • 當客戶端發起 I/O 讀寫事件時,服務端 EventLoop 會進行資料的讀取,然後通過 ChannelPipeline 依次有序觸發各種ChannelHandler進行資料處理。
  • 客戶端資料會被依次傳遞到 ChannelPipeline 的 ChannelInboundHandler 中,在一個handler中處理完後就會傳入下一個handler。
  • 當資料寫回客戶端時,會將處理結果依次傳遞到 ChannelPipeline 的 ChannelOutboundHandler 中,在一個handler中處理完後就會傳入下一個handler,最後返回客戶端。

以上便是 Netty 各個元件的邏輯架構,我們暫時只需要瞭解個大致框架即可,後面我們會詳細介紹各個元件。

有幾個比較常見的問題在這裡總結下:

1)什麼是Channel
Channel 的字面意思是“通道”,它是網路通訊的載體,提供了基本的 API 用於網路 I/O 操作,如 register、bind、connect、read、write、flush 等。

Netty 實現的 Channel 是以 JDK NIO Channel 為基礎的,提供了更高層次的抽象,遮蔽了底層 Socket。

2)什麼是ChannleHandler和ChannelPipeline

ChannelHandler實現對客戶端傳送過來的資料進行處理,可能包括編解碼、自定義業務邏輯處理等等。

ChannelPipeline 負責組裝各種 ChannelHandler,當 I/O 讀寫事件觸發時,ChannelPipeline 會依次呼叫 ChannelHandler 列表對 Channel 的資料進行攔截和處理。

3)什麼是EventLoopGroup?

EventLoopGroup 本質是一個執行緒池, 是 Netty Reactor 執行緒模型的具體實現方式,主要負責接收 I/O 請求,並分配執行緒執行處理請求。我們在demo中使用了它的實現類 NioEventLoopGroup,也是 Netty 中最被推薦使用的執行緒模型。

我們還通過構建main EventLoopGroup 和 child EventLoopGroup 實現了 “主從Reactor模式”。

4)EventLoopGroup、EventLoop、Channel有什麼關係?

一個 EventLoopGroup 往往包含一個或者多個 EventLoop。

EventLoop 用於處理 Channel 生命週期內的所有 I/O 事件,如 accept、connect、read、write 等 I/O 事件。

EventLoop 同一時間會與一個執行緒繫結,每個 EventLoop 負責處理多個 Channel。


參考書目:
《Netty in Action》

 

都看到最後了,原創不易,點個關注,點個贊吧~
文章持續更新,可以微信搜尋「阿丸筆記 」第一時間閱讀,回覆【筆記】獲取Canal、MySQL、HBase、JAVA實戰筆記,回覆【資料】獲取一線大廠面試資料。
知識碎片重新梳理,構建Java知識圖譜:github.com/saigu/JavaK…(歷史文章查閱非常方便)

相關文章