天天看點

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

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

工作了5年,你真的了解Netty以及為什麼要用嗎?(深度幹貨)
簡單來說可以分為以下幾個步驟:

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

在這個過程中,會涉及到網絡IO通信,在傳統的BIO模式下,用戶端向服務端發起一個資料讀取請求,用戶端在收到服務端傳回資料之前,一直處于阻塞狀态,直到服務端傳回資料後完成本次會話。這個過程就叫同步阻塞IO,在BIO模型中如果想實作異步操作,就隻能使用多線程模型,也就是一個請求對應一個線程,這樣就能夠避免服務端的連結被一個用戶端占用導緻連接配接數無法提高。

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

  • 服務端接收用戶端連接配接時的阻塞。
  • 用戶端和服務端的IO通信時,資料未就緒的情況下的阻塞。
工作了5年,你真的了解Netty以及為什麼要用嗎?(深度幹貨)

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

工作了5年,你真的了解Netty以及為什麼要用嗎?(深度幹貨)
那麼,服務端應該如何優化呢?

非阻塞IO

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

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

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

工作了5年,你真的了解Netty以及為什麼要用嗎?(深度幹貨)
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,然後執行相關的處理即可。

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

異步IO

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

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

圖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 :執行非阻塞讀/寫
工作了5年,你真的了解Netty以及為什麼要用嗎?(深度幹貨)

圖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所示。

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

圖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 等就是采用這種多線程模型,雖然不同的項目實作細節略有差別,但總體來說模式是一緻的。
工作了5年,你真的了解Netty以及為什麼要用嗎?(深度幹貨)

圖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生态中提供的功能說明。後續内容中會逐漸的分析這些功能。

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

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

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

圖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模型和協定類型。

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

圖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所示的流程。

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

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

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

圖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結構圖。
    工作了5年,你真的了解Netty以及為什麼要用嗎?(深度幹貨)

    圖2-6 ChannelPipeline

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

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

圖2-7 InBound和OutBound的關系

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

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

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

圖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組成的鍊中傳播,最後到達用戶端。
工作了5年,你真的了解Netty以及為什麼要用嗎?(深度幹貨)

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

Netty中核心元件的詳細介紹

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

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

  • 配置線程池
  • Channel初始化
  • Handler處理器建構
版權聲明:本部落格所有文章除特别聲明外,均采用 CC BY-NC-SA 4.0 許可協定。轉載請注明來自

Mic帶你學架構

如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注「跟着Mic學架構」公衆号公衆号擷取更多技術幹貨!

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