[年薪60W的技巧]工作了5年,你真的理解Netty以及為什麼要用嗎?(深度乾貨)

跟著Mic學架構發表於2021-11-15

來看下面這個圖,當客戶端發起一次Http請求時,服務端的處理流程時怎麼樣的?

image-20211109084258499

簡單來說可以分為以下幾個步驟:

  1. 基於TCP協議建立網路通訊。
  2. 開始向服務端端傳輸資料。
  3. 服務端接受到資料進行解析,開始處理本次請求邏輯。
  4. 服務端處理完成後返回結果給客戶端。

在這個過程中,會涉及到網路IO通訊,在傳統的BIO模式下,客戶端向服務端發起一個資料讀取請求,客戶端在收到服務端返回資料之前,一直處於阻塞狀態,直到服務端返回資料後完成本次會話。這個過程就叫同步阻塞IO,在BIO模型中如果想實現非同步操作,就只能使用多執行緒模型,也就是一個請求對應一個執行緒,這樣就能夠避免服務端的連結被一個客戶端佔用導致連線數無法提高。

同步阻塞IO主要體現在兩個阻塞點

  • 服務端接收客戶端連線時的阻塞。
  • 客戶端和服務端的IO通訊時,資料未就緒的情況下的阻塞。

image-20210811170350557

在這種傳統BIO模式下,會造成一個非常嚴重的問題,如下圖所示,如果同一時刻有N個客戶端發起請求,按照BIO模型的特點,服務端在同一時刻只能處理一個請求。將導致客戶端請求需要排隊處理,帶來的影響是,使用者在等待一次請求處理返回的時間非常長。意味著服務端沒有併發處理能力,這顯然不合適。

image-20211109084710182

那麼,服務端應該如何優化呢?

非阻塞IO

從前面的分析發現,服務端在處理一次請求時,會處於阻塞狀態無法處理後續請求,那是否能夠讓被阻塞的地方優化成不阻塞呢?於是就有了非阻塞IO(NIO)

非阻塞IO,就是客戶端向服務端發起請求時,如果服務端的資料未就緒的情況下, 客戶端請求不會被阻塞,而是直接返回。但是有可能服務端的資料還未準備好的時候,客戶端收到的返回是一個空的, 那客戶端怎麼拿到最終的資料呢?

如圖所示,客戶端只能通過輪詢的方式來獲得請求結果。NIO相比BIO來說,少了阻塞的過程在效能和連線數上都會有明顯提高。

image-20210708165359843

NIO仍然有一個弊端,就是輪詢過程中會有很多空輪詢,而這個輪詢會存在大量的系統呼叫(發起核心指令從網路卡緩衝區中載入資料,使用者空間到核心空間的切換),隨著連線數量的增加,會導致效能問題。

多路複用機制

I/O多路複用的本質是通過一種機制(系統核心緩衝I/O資料),讓單個程式可以監視多個檔案描述符,一旦某個描述符就緒(一般是讀就緒或寫就緒),能夠通知程式進行相應的讀寫操作

什麼是fd:在linux中,核心把所有的外部裝置都當成是一個檔案來操作,對一個檔案的讀寫會呼叫核心提供的系統命令,返回一個fd(檔案描述符)。而對於一個socket的讀寫也會有相應的檔案描述符,成為socketfd。

常見的IO多路複用方式有【select、poll、epoll】,都是Linux API提供的IO複用方式,那麼接下來重點講一下select、和epoll這兩個模型

  • select:程式可以通過把一個或者多個fd傳遞給select系統呼叫,程式會阻塞在select操作上,這樣select可以幫我們檢測多個fd是否處於就緒狀態,這個模式有兩個缺點

    • 由於他能夠同時監聽多個檔案描述符,假如說有1000個,這個時候如果其中一個fd 處於就緒狀態了,那麼當前程式需要線性輪詢所有的fd,也就是監聽的fd越多,效能開銷越大。
    • 同時,select在單個程式中能開啟的fd是有限制的,預設是1024,對於那些需要支援單機上萬的TCP連線來說確實有點少
  • epoll:linux還提供了epoll的系統呼叫,epoll是基於事件驅動方式來代替順序掃描,因此效能相對來說更高,主要原理是,當被監聽的fd中,有fd就緒時,會告知當前程式具體哪一個fd就緒,那麼當前程式只需要去從指定的fd上讀取資料即可,另外,epoll所能支援的fd上線是作業系統的最大檔案控制程式碼,這個數字要遠遠大於1024
【由於epoll能夠通過事件告知應用程式哪個fd是可讀的,所以我們也稱這種IO為非同步非阻塞IO,當然它是偽非同步的,因為它還需要去把資料從核心同步複製到使用者空間中,真正的非同步非阻塞,應該是資料已經完全準備好了,我只需要從使用者空間讀就行】

I/O多路複用的好處是可以通過把多個I/O的阻塞複用到同一個select的阻塞上,從而使得系統在單執行緒的情況下可以同時處理多個客戶端請求。它的最大優勢是系統開銷小,並且不需要建立新的程式或者執行緒,降低了系統的資源開銷,它的整體實現思想如圖2-3所示。

客戶端請求到服務端後,此時客戶端在傳輸資料過程中,為了避免Server端在read客戶端資料過程中阻塞,服務端會把該請求註冊到Selector復路器上,服務端此時不需要等待,只需要啟動一個執行緒,通過selector.select()阻塞輪詢復路器上就緒的channel即可,也就是說,如果某個客戶端連線資料傳輸完成,那麼select()方法會返回就緒的channel,然後執行相關的處理即可。

image-20210708203509498

非同步IO

非同步IO和多路複用機制,最大的區別在於:當資料就緒後,客戶端不需要傳送核心指令從核心空間讀取資料,而是系統會非同步把這個資料直接拷貝到使用者空間,應用程式只需要直接使用該資料即可。

image-20210811172034569

<center>圖2-4 非同步IO</center>

在Java中,我們可以使用NIO的api來完成多路複用機制,實現偽非同步IO。在網路通訊演進模型分析這篇文章中演示了Java API實現多路複用機制的程式碼,發現程式碼不僅僅繁瑣,而且使用起來很麻煩。

所以Netty出現了,Netty的I/O模型是基於非阻塞IO實現的,底層依賴的是JDK NIO框架的多路複用器Selector來實現。

一個多路複用器Selector可以同時輪詢多個Channel,採用epoll模式後,只需要一個執行緒負責Selector的輪詢,就可以接入成千上萬個客戶端連線。

Reactor模型

http://gee.cs.oswego.edu/dl/c...

瞭解了NIO多路複用後,就有必要再和大家說一下Reactor多路複用高效能I/O設計模式,Reactor本質上就是基於NIO多路複用機制提出的一個高效能IO設計模式,它的核心思想是把響應IO事件和業務處理進行分離,通過一個或者多個執行緒來處理IO事件,然後將就緒得到事件分發到業務處理handlers執行緒去非同步非阻塞處理,如圖2-5所示。

Reactor模型有三個重要的元件:

  • Reactor :將I/O事件發派給對應的Handler
  • Acceptor :處理客戶端連線請求
  • Handlers :執行非阻塞讀/寫

image-20210708212057895

<center>圖2-5 Reactor模型</center>

這是最基本的單Reactor單執行緒模型(整體的I/O操作是由同一個執行緒完成的)

其中Reactor執行緒,負責多路分離套接字,有新連線到來觸發connect 事件之後,交由Acceptor進行處理,有IO讀寫事件之後交給hanlder 處理。

Acceptor主要任務就是構建handler ,在獲取到和client相關的SocketChannel之後 ,繫結到相應的hanlder上,對應的SocketChannel有讀寫事件之後,基於racotor 分發,hanlder就可以處理了(所有的IO事件都繫結到selector上,有Reactor分發)

Reactor 模式本質上指的是使用 I/O 多路複用(I/O multiplexing) + 非阻塞 I/O(non-blocking I/O) 的模式。

多執行緒單Reactor模型

單執行緒Reactor這種實現方式有存在著缺點,從例項程式碼中可以看出,handler的執行是序列的,如果其中一個handler處理執行緒阻塞將導致其他的業務處理阻塞。由於handler和reactor在同一個執行緒中的執行,這也將導致新的無法接收新的請求,我們做一個小實驗:

  • 在上述Reactor程式碼的DispatchHandler的run方法中,增加一個Thread.sleep()。
  • 開啟多個客戶端視窗連線到Reactor Server端,其中一個視窗傳送一個資訊後被阻塞,另外一個視窗再發資訊時由於前面的請求阻塞導致後續請求無法被處理。

為了解決這種問題,有人提出使用多執行緒的方式來處理業務,也就是在業務處理的地方加入執行緒池非同步處理,將reactor和handler在不同的執行緒來執行,如圖4-7所示。

image-20210709154534593

<center>圖2-6</center>

多執行緒多Reactor模型

在多執行緒單Reactor模型中,我們發現所有的I/O操作是由一個Reactor來完成,而Reactor執行在單個執行緒中,它需要處理包括Accept()/read()/write/connect操作,對於小容量的場景,影響不大。但是對於高負載、大併發或大資料量的應用場景時,容易成為瓶頸,主要原因如下:

  • 一個NIO執行緒同時處理成百上千的鏈路,效能上無法支撐,即便NIO執行緒的CPU負荷達到100%,也無法滿足海量訊息的讀取和傳送;
  • 當NIO執行緒負載過重之後,處理速度將變慢,這會導致大量客戶端連線超時,超時之後往往會進行重發,這更加重了NIO執行緒的負載,最終會導致大量訊息積壓和處理超時,成為系統的效能瓶頸;

所以,我們還可以更進一步優化,引入多Reactor多執行緒模式,如圖2-7所示,Main Reactor負責接收客戶端的連線請求,然後把接收到的請求傳遞給SubReactor(其中subReactor可以有多個),具體的業務IO處理由SubReactor完成。

Multiple Reactors 模式通常也可以等同於 Master-Workers 模式,比如 Nginx 和 Memcached 等就是採用這種多執行緒模型,雖然不同的專案實現細節略有區別,但總體來說模式是一致的。

image-20210709162516832

<center>圖2-7</center>

  • Acceptor,請求接收者,在實踐時其職責類似伺服器,並不真正負責連線請求的建立,而只將其請求委託 Main Reactor 執行緒池來實現,起到一個轉發的作用。
  • Main Reactor,主 Reactor 執行緒組,主要負責連線事件,並將IO讀寫請求轉發到 SubReactor 執行緒池
  • Sub Reactor,Main Reactor 通常監聽客戶端連線後會將通道的讀寫轉發到 Sub Reactor 執行緒池中一個執行緒(負載均衡),負責資料的讀寫。在 NIO 中 通常註冊通道的讀(OP_READ)、寫事件(OP_WRITE)。

高效能通訊框架之Netty

在Java中,網路程式設計框架有很多,比如Java NIO、Mina、Netty、Grizzy等。但是在大家接觸到的所有中介軟體中,絕大部分都是採用Netty。

原因是Netty是目前最流行的一款高效能Java網路程式設計框架,它被廣泛引用在中介軟體、直播、社交、遊戲等領域。談及到開源中介軟體,大家熟知的Dubbo、RocketMQ、Elasticsearch、Hbase、RocketMQ等都是採用Netty實現。

在實際開發中,今天來聽課的同學,99%的人都不會涉及到使用Netty做網路程式設計開發,但是為什麼還要花精力給大家講呢?原因有幾個

  • 在很多大廠面試的時候,會涉及到相關的知識點

    • Netty高效能表現在哪些方面
    • Netty中有哪些重要元件
    • Netty的記憶體池、物件池的設計
  • 很多中介軟體都是用netty來做網路通訊,那麼我們在分析這些中介軟體的原始碼時,降低網路通訊的理解難度
  • 提升Java知識體系,儘可能的實現對技術體系理解的全面性。

為什麼選擇Netty

Netty其實就是一個高效能NIO框架,所以它是基於NIO基礎上的封裝,本質上是提供高效能網路IO通訊的功能。由於前面的課程中我們已經詳細的對網路通訊做了分析,因此在學習Netty時,學習起來應該是更輕鬆的。

Netty提供了上述三種Reactor模型的支援,我們可以通過Netty封裝好的API來快速完成不同Reactor模型的開發,這也是為什麼大家都選擇Netty的原因之一,除此之外,Netty相比於NIO原生API,它有以下特點:

  • 提供了高效的I/O模型、執行緒模型和時間處理機制
  • 提供了非常簡單易用的API,相比NIO來說,針對基礎的Channel、Selector、Sockets、Buffers等api提供了更高層次的封裝,遮蔽了NIO的複雜性
  • 對資料協議和序列化提供了很好的支援
  • 穩定性,Netty修復了JDK NIO較多的問題,比如select空轉導致的cpu消耗100%、TCP斷線重連、keep-alive檢測等問題。
  • 可擴充套件性在同型別的框架中都是做的非常好的,比如一個是可定製化的執行緒模型,使用者可以在啟動引數中選擇Reactor模型、 可擴充套件的事件驅動模型,將業務和框架的關注點分離。
  • 效能層面的優化,作為網路通訊框架,需要處理大量的網路請求,必然就面臨網路物件需要建立和銷燬的問題,這種對JVM的GC來說不是很友好,為了降低JVM垃圾回收的壓力,引入了兩種優化機制

    • 物件池複用,
    • 零拷貝技術

Netty的生態介紹

首先,我們需要去了解Netty到底提供了哪些功能,如圖2-1所示,表示Netty生態中提供的功能說明。後續內容中會逐步的分析這些功能。

image-20210811151520387

<center>圖2-1 Netty功能生態</center>

Netty的基本使用

需要說明一下,我們講解的Netty版本是4.x版本,之前有一段時間netty釋出了一個5.x版本,但是被官方捨棄了,原因是:使用ForkJoinPool增加了複雜性,並且沒有顯示出明顯的效能優勢。同時保持所有的分支同步是相當多的工作,沒有必要。

新增jar包依賴

使用4.1.66版本

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
</dependency>

建立Netty Server服務

大部分場景中,我們使用的主從多執行緒Reactor模型,Boss執行緒是住Reactor,Worker是從Reactor。他們分別使用不同的NioEventLoopGroup

主Reactor負責處理Accept,然後把Channel註冊到從Reactor,從Reactor主要負責Channel生命週期內的所有I/O事件。

public class NettyBasicServerExample {

    public void bind(int port){
        // 我們要建立兩個EventLoopGroup,
        // 一個是boss專門用來接收連線,可以理解為處理accept事件,
        // 另一個是worker,可以關注除了accept之外的其它事件,處理子任務。
        //上面注意,boss執行緒一般設定一個執行緒,設定多個也只會用到一個,而且多個目前沒有應用場景,
        // worker執行緒通常要根據伺服器調優,如果不寫預設就是cpu的兩倍。
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        EventLoopGroup workerGroup=new NioEventLoopGroup();
        try {
            //服務端要啟動,需要建立ServerBootStrap,
            // 在這裡面netty把nio的模板式的程式碼都給封裝好了
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup) //配置boss和worker執行緒
                //配置Server的通道,相當於NIO中的ServerSocketChannel
                .channel(NioServerSocketChannel.class)
                //childHandler表示給worker那些執行緒配置了一個處理器,
                // 配置初始化channel,也就是給worker執行緒配置對應的handler,當收到客戶端的請求時,分配給指定的handler處理
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new NormalMessageHandler()); //新增handler,也就是具體的IO事件處理器
                    }
                });
            //由於預設情況下是NIO非同步非阻塞,所以繫結埠後,通過sync()方法阻塞直到連線建立
            //繫結埠並同步等待客戶端連線(sync方法會阻塞,直到整個啟動過程完成)
            ChannelFuture channelFuture=bootstrap.bind(port).sync();
            System.out.println("Netty Server Started,Listening on :"+port);
            //等待服務端監聽埠關閉
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //釋放執行緒資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new NettyBasicServerExample().bind(8080);
    }
}

上述程式碼說明如下:

  • EventLoopGroup,定義執行緒組,相當於我們之前在寫NIO程式碼時定義的執行緒。這裡定義了兩個執行緒組分別是boss執行緒和worker執行緒,boss執行緒負責接收連線,worker執行緒負責處理IO事件。boss執行緒一般設定一個執行緒,設定多個也只會用到一個,而且多個目前沒有應用場景。而worker執行緒通常要根據伺服器調優,如果不寫預設就是cpu的兩倍。
  • ServerBootstrap,服務端要啟動,需要建立ServerBootStrap,在這裡面netty把nio的模板式的程式碼都給封裝好了。
  • ChannelOption.SO_BACKLOG

設定Channel型別

NIO模型是Netty中最成熟也是被廣泛引用的模型,因此在使用Netty的時候,我們會採用NioServerSocketChannel作為Channel型別。

bootstrap.channel(NioServerSocketChannel.class);

除了NioServerSocketChannel以外,還提供了

  • EpollServerSocketChannel,epoll模型只有在linux kernel 2.6以上才能支援,在windows和mac都是不支援的,如果設定Epoll在window環境下執行會報錯。
  • OioServerSocketChannel,用於服務端阻塞地接收TCP連線
  • KQueueServerSocketChannel,kqueue模型,是Unix中比較高效的IO複用技術,常見的IO複用技術有select, poll, epoll以及kqueue等等。其中epoll為Linux獨佔,而kqueue則在許多UNIX系統上存在。

註冊ChannelHandler

在Netty中可以通過ChannelPipeline註冊多個ChannelHandler,該handler就是給到worker執行緒執行的處理器,當IO事件就緒時,會根據這裡配置的Handler進行呼叫。

這裡可以註冊多個ChannelHandler,每個ChannelHandler各司其職,比如做編碼和解碼的handler,心跳機制的handler,訊息處理的handler等。這樣可以實現程式碼的最大化複用。

.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new NormalMessageHandler());
    }
});

ServerBootstrap中的childHandler方法需要註冊一個ChannelHandler,這裡配置了一個ChannelInitializer的實現類,通過例項化ChannelInitializer來配置初始化Channel。

當收到IO事件後,這個資料會在這多個handler中進行傳播。上述程式碼中配置了一個NormalMessageHandler,用來接收客戶端訊息並輸出。

繫結埠

完成Netty的基本配置後,通過bind()方法真正觸發啟動,而sync()方法會阻塞,直到整個啟動過程完成。

ChannelFuture channelFuture=bootstrap.bind(port).sync();

NormalMessageHandler

ServerHandler繼承了ChannelInboundHandlerAdapter,這是netty中的一個事件處理器,netty中的處理器分為Inbound(進站)和Outbound(出站)處理器,後面會詳細介紹。

public class NormalMessageHandler extends ChannelInboundHandlerAdapter {
    //channelReadComplete方法表示訊息讀完了的處理,writeAndFlush方法表示寫入併傳送訊息
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //這裡的邏輯就是所有的訊息讀取完畢了,在統一寫回到客戶端。Unpooled.EMPTY_BUFFER表示空訊息,addListener(ChannelFutureListener.CLOSE)表示寫完後,就關閉連線
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    //exceptionCaught方法就是發生異常的處理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    //channelRead方法表示讀到訊息以後如何處理,這裡我們把訊息列印出來
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in=(ByteBuf) msg;
        byte[] req=new byte[in.readableBytes()];
        in.readBytes(req); //把資料讀到byte陣列中
        String body=new String(req,"UTF-8");
        System.out.println("伺服器端收到訊息:"+body);
        //寫回資料
        ByteBuf resp=Unpooled.copiedBuffer(("receive message:"+body+"").getBytes());
        ctx.write(resp);
        //ctx.write表示把訊息再傳送回客戶端,但是僅僅是寫到緩衝區,沒有傳送,flush才會真正寫到網路上去
    }
}

通過上述程式碼發現,我們只需要通過極少的程式碼就完成了NIO服務端的開發,相比傳統的NIO原生類庫的服務端,程式碼量大大減少,開發難度也大幅度降低。

Netty和NIO的api對應

TransportChannel ----對應NIO中的channel

EventLoop---- 對應於NIO中的while迴圈

EventLoopGroup: 多個EventLoop,就是事件迴圈

ChannelHandler和ChannelPipeline---對應於NIO中的客戶邏輯實現handleRead/handleWrite(interceptor pattern)

ByteBuf---- 對應於NIO 中的ByteBuffer

Bootstrap 和 ServerBootstrap ---對應NIO中的Selector、ServerSocketChannel等的建立、配置、啟動等

Netty的整體工作機制

Netty的整體工作機制如下,整體設計就是前面我們講過的多執行緒Reactor模型,分離請求監聽和請求處理,通過多執行緒分別執行具體的handler。

image-20210812181454154

<center>圖2-2</center>

網路通訊層

網路通訊層主要的職責是執行網路的IO操作,它支援多種網路通訊協議和I/O模型的連結操作。當網路資料讀取到核心緩衝區後,會觸發讀寫事件,這些事件在分發給時間排程器來進行處理。

在Netty中,網路通訊的核心元件以下三個元件

  • Bootstrap, 客戶端啟動api,用來連結遠端netty server,只繫結一個EventLoopGroup
  • ServerBootStrap,服務端監聽api,用來監聽指定埠,會繫結兩個EventLoopGroup, bootstrap元件可以非常方便快捷的啟動Netty應用程式
  • Channel,Channel是網路通訊的載體,Netty自己實現的Channel是以JDK NIO channel為基礎,提供了更高層次的抽象,同時也遮蔽了底層Socket的複雜性,為Channel提供了更加強大的功能。

如圖2-3所示,表示的是Channel的常用實現實現類關係圖,AbstractChannel是整個Channel實現的基類,派生出了AbstractNioChannel(非阻塞io)、AbstractOioChannel(阻塞io),每個子類代表了不同的I/O模型和協議型別。

image-20210812213408836

<center>圖2-3 Channel的類關係圖</center>

隨著連線和資料的變化,Channel也會存在多種狀態,比如連線建立、連線註冊、連線讀寫、連線銷燬。隨著狀態的變化,Channel也會處於不同的生命週期,每種狀態會繫結一個相應的事件回撥。以下是常見的時間回撥方法。

  • channelRegistered, channel建立後被註冊到EventLoop上
  • channelUnregistered,channel建立後未註冊或者從EventLoop取消註冊
  • channelActive,channel處於就緒狀態,可以被讀寫
  • channelInactive,Channel處於非就緒狀態
  • channelRead,Channel可以從源端讀取資料
  • channelReadComplete,Channel讀取資料完成

簡單總結一下,Bootstrap和ServerBootStrap分別負責客戶端和服務端的啟動,Channel是網路通訊的載體,它提供了與底層Socket互動的能力。

而當Channel生命週期中的事件變化,就需要觸發進一步處理,這個處理是由Netty的事件排程器來完成。

事件排程器

事件排程器是通過Reactor執行緒模型對各類事件進行聚合處理,通過Selector主迴圈執行緒整合多種事件(I/O時間、訊號時間),當這些事件被觸發後,具體針對該事件的處理需要給到服務編排層中相關的Handler來處理。

事件排程器核心元件:

  • EventLoopGroup。相當於執行緒池
  • EventLoop。相當於執行緒池中的執行緒

EventLoopGroup本質上是一個執行緒池,主要負責接收I/O請求,並分配執行緒執行處理請求。為了更好的理解EventLoopGroup、EventLoop、Channel之間的關係,我們來看圖2-4所示的流程。

image-20210812220244801

<center>圖2-4,EventLoop的工作機制</center>

從圖中可知

  • 一個EventLoopGroup可以包含多個EventLoop,EventLoop用來處理Channel生命週期內所有的I/O事件,比如accept、connect、read、write等
  • EventLoop同一時間會與一個執行緒繫結,每個EventLoop負責處理多個Channel
  • 每新建一個Channel,EventLoopGroup會選擇一個EventLoop進行繫結,該Channel在生命週期內可以對EventLoop進行多次繫結和解綁。

圖2-5表示的是EventLoopGroup的類關係圖,可以看出Netty提供了EventLoopGroup的多種實現,如NioEventLoop、EpollEventLoop、NioEventLoopGroup等。

從圖中可以看到,EventLoop是EventLoopGroup的子介面,我們可以把EventLoop等價於EventLoopGroup,前提是EventLoopGroup中只包含一個EventLoop。

<img src="https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/202111090024225.png" alt="image-20210812221329760" style="zoom:80%;" />

<center>圖2-5 EventLoopGroup類關係圖</center>

EventLoopGroup是Netty的核心處理引擎,它和前面我們講解的Reactor執行緒模型有什麼關係呢?其實,我們可以簡單的把EventLoopGroup當成是Netty中Reactor執行緒模型的具體實現,我們可以通過配置不同的EventLoopGroup使得Netty支援多種不同的Reactor模型。

  • 單執行緒模型,EventLoopGroup只包含一個EventLoop,Boss和Worker使用同一個EventLoopGroup。
  • 多執行緒模型:EventLoopGroup包含多個EventLoop,Boss和Worker使用同一個EventLoopGroup。
  • 主從多執行緒模型:EventLoopGroup包含多個EventLoop,Boss是主Reactor,Worker是從Reactor模型。他們分別使用不同的EventLoopGroup,主Reactor負責新的網路連線Channel的建立(也就是連線的事件),主Reactor收到客戶端的連線後,交給從Reactor來處理。

服務編排層

服務編排層的職責是負責組裝各類的服務,簡單來說,就是I/O事件觸發後,需要有一個Handler來處理,所以服務編排層可以通過一個Handler處理鏈來實現網路事件的動態編排和有序的傳播。

它包含三個元件

  • ChannelPipeline,它採用了雙向連結串列將多個Channelhandler連結在一起,當I/O事件觸發時,ChannelPipeline會依次呼叫組裝好的多個ChannelHandler,實現對Channel的資料處理。ChannelPipeline是執行緒安全的,因為每個新的Channel都會繫結一個新的ChannelPipeline。一個ChannelPipeline關聯一個EventLoop,而一個EventLoop只會繫結一個執行緒,如圖2-6所示,表示ChannelPIpeline結構圖。

    <img src="https://mic-blob-bucket.oss-cn-beijing.aliyuncs.com/202111090024172.png" alt="image-20210812223234507" style="zoom: 50%;" />

    <center>圖2-6 ChannelPipeline</center>

    從圖中可以看出,ChannelPipeline中包含入站ChannelInBoundHandler和出站ChannelOutboundHandler,前者是接收資料,後者是寫出資料,其實就是InputStream和OutputStream,為了更好的理解,我們來看圖2-7。

image-20210812224219710

<center>圖2-7 InBound和OutBound的關係</center>

  • ChannelHandler, 針對IO資料的處理器,資料接收後,通過指定的Handler進行處理。
  • ChannelHandlerContext,ChannelHandlerContext用來儲存ChannelHandler的上下文資訊,也就是說,當事件被觸發後,多個handler之間的資料,是通過ChannelHandlerContext來進行傳遞的。ChannelHandler和ChannelHandlerContext之間的關係,如圖2-8所示。

    每個ChannelHandler都對應一個自己的ChannelHandlerContext,它保留了ChannelHandler所需要的上下文資訊,多個ChannelHandler之間的資料傳遞,是通過ChannelHandlerContext來實現的。

image-20210812230122911

<center>圖2-8 ChannelHandler和ChannelHandlerContext關係</center>

以上就是Netty中核心的元件的特性和工作機制的介紹,後續的內容中還會詳細的分析這幾個元件。可以看出,Netty的架構分層設計是非常合理的,它遮蔽了底層NIO以及框架層的實現細節,對於業務開發者來說,只需要關心業務邏輯的編排和實現即可。

元件關係及原理總結

如圖2-9所示,表示Netty中關鍵的元件協調原理,具體的工作機制描述如下。

  • 服務單啟動初始化Boss和Worker執行緒組,Boss執行緒組負責監聽網路連線事件,當有新的連線建立時,Boss執行緒會把該連線Channel註冊繫結到Worker執行緒
  • Worker執行緒組會分配一個EventLoop負責處理該Channel的讀寫事件,每個EventLoop相當於一個執行緒。通過Selector進行事件迴圈監聽。
  • 當客戶端發起I/O事件時,服務端的EventLoop講就緒的Channel分發給Pipeline,進行資料的處理
  • 資料傳輸到ChannelPipeline後,從第一個ChannelInBoundHandler進行處理,按照pipeline鏈逐個進行傳遞
  • 服務端處理完成後要把資料寫回到客戶端,這個寫回的資料會在ChannelOutboundHandler組成的鏈中傳播,最後到達客戶端。

image-20210814151504091

<center>圖2-9 Netty各個元件的工作原理</center>

Netty中核心元件的詳細介紹

在2.5節中對Netty有了一個全域性認識後,我們再針對這幾個元件做一個非常詳細的說明,加深大家的理解。

啟動器Bootstrap和ServerBootstrap作為Netty構建客戶端和服務端的路口,是編寫Netty網路程式的第一步。它可以讓我們把Netty的核心元件像搭積木一樣組裝在一起。在Netty Server端構建的過程中,我們需要關注三個重要的步驟

  • 配置執行緒池
  • Channel初始化
  • Handler處理器構建
版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 Mic帶你學架構
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注同名微信公眾號獲取更多技術乾貨!

相關文章