工作了5年,你真的理解Netty以及為什么要用嗎?(深度干貨)


來看下面這個圖,當客戶端發起一次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

圖2-4 異步IO

在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/cpjslides/nio.pdf

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

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

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

image-20210708212057895

圖2-5 Reactor模型

這是最基本的單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

圖2-6

多線程多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

圖2-7
  • 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

圖2-1 Netty功能生態

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

圖2-2

網絡通信層

網絡通信層主要的職責是執行網絡的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

圖2-3 Channel的類關系圖

隨着連接和數據的變化,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

圖2-4,EventLoop的工作機制

從圖中可知

  • 一個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。

image-20210812221329760
圖2-5 EventLoopGroup類關系圖

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結構圖。

    image-20210812223234507
    圖2-6 ChannelPipeline

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

image-20210812224219710

圖2-7 InBound和OutBound的關系
  • ChannelHandler, 針對IO數據的處理器,數據接收后,通過指定的Handler進行處理。

  • ChannelHandlerContext,ChannelHandlerContext用來保存ChannelHandler的上下文信息,也就是說,當事件被觸發后,多個handler之間的數據,是通過ChannelHandlerContext來進行傳遞的。ChannelHandler和ChannelHandlerContext之間的關系,如圖2-8所示。

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

image-20210812230122911

圖2-8 ChannelHandler和ChannelHandlerContext關系

以上就是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

圖2-9 Netty各個組件的工作原理

Netty中核心組件的詳細介紹

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

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

  • 配置線程池
  • Channel初始化
  • Handler處理器構建

版權聲明:本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 4.0 許可協議。轉載請注明來自 Mic帶你學架構
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注「跟着Mic學架構」公眾號公眾號獲取更多技術干貨!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM