主頁 > 後端開發 > 作業了5年,你真的理解Netty以及為什么要用嗎?(深度干貨)

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

2021-11-10 06:16:58 後端開發

來看下面這個圖,當客戶端發起一次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學架構」公眾號公眾號獲取更多技術干貨!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/354410.html

標籤:Java

上一篇:開源專案|Go 開發的一款分布式唯一 ID 生成系統

下一篇:【JAVA】編程(2)---時間管理

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more