一文理解Netty模型架構

本文基於Netty4.1展開介紹相關理論模型,使用場景,基本組件、總體架構,知其然且知其因此然,但願給讀者提供學習實踐參考。html

1 Netty簡介

Netty是 一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端。java

JDK原生NIO程序的問題

JDK原生也有一套網絡應用程序API,可是存在一系列問題,主要以下:react

  • NIO的類庫和API繁雜,使用麻煩,你須要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
  • 須要具有其它的額外技能作鋪墊,例如熟悉Java多線程編程,由於NIO編程涉及到Reactor模式,你必須對多線程和網路編程很是熟悉,才能編寫出高質量的NIO程序
  • 可靠性能力補齊,開發工做量和難度都很是大。例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等等,NIO編程的特色是功能開發相對容易,可是可靠性能力補齊工做量和難度都很是大
  • JDK NIO的BUG,例如臭名昭著的epoll bug,它會致使Selector空輪詢,最終致使CPU 100%。官方聲稱在JDK1.6版本的update18修復了該問題,可是直到JDK1.7版本該問題仍舊存在,只不過該bug發生機率下降了一些而已,它並無被根本解決

Netty的特色

Netty的對JDK自帶的NIO的API進行封裝,解決上述問題,主要特色有:編程

  • 設計優雅 適用於各類傳輸類型的統一API - 阻塞和非阻塞Socket 基於靈活且可擴展的事件模型,能夠清晰地分離關注點 高度可定製的線程模型 - 單線程,一個或多個線程池 真正的無鏈接數據報套接字支持(自3.1起)
  • 使用方便 詳細記錄的Javadoc,用戶指南和示例 沒有其餘依賴項,JDK 5(Netty 3.x)或6(Netty 4.x)就足夠了
  • 高性能 吞吐量更高,延遲更低 減小資源消耗 最小化沒必要要的內存複製
  • 安全 完整的SSL / TLS和StartTLS支持
  • 社區活躍,不斷更新 社區活躍,版本迭代週期短,發現的BUG能夠被及時修復,同時,更多的新功能會被加入

Netty常見使用常見

Netty常見的使用場景以下:緩存

  • 互聯網行業 在分佈式系統中,各個節點之間須要遠程服務調用,高性能的RPC框架必不可少,Netty做爲異步高新能的通訊框架,每每做爲基礎通訊組件被這些RPC框架使用。 典型的應用有:阿里分佈式服務框架Dubbo的RPC框架使用Dubbo協議進行節點間通訊,Dubbo協議默認使用Netty做爲基礎通訊組件,用於實現各進程節點之間的內部通訊。
  • 遊戲行業 不管是手遊服務端仍是大型的網絡遊戲,Java語言獲得了愈來愈普遍的應用。Netty做爲高性能的基礎通訊組件,它自己提供了TCP/UDP和HTTP協議棧。 很是方便定製和開發私有協議棧,帳號登陸服務器,地圖服務器之間能夠方便的經過Netty進行高性能的通訊
  • 大數據領域 經典的Hadoop的高性能通訊和序列化組件Avro的RPC框架,默認採用Netty進行跨界點通訊,它的Netty Service基於Netty框架二次封裝實現

有興趣的讀者能夠了解一下目前有哪些開源項目使用了 Netty:Related projects安全

2 Netty高性能設計

Netty做爲異步事件驅動的網絡,高性能之處主要來自於其I/O模型和線程處理模型,前者決定如何收發數據,後者決定如何處理數據bash

I/O模型

用什麼樣的通道將數據發送給對方,BIO、NIO或者AIO,I/O模型在很大程度上決定了框架的性能服務器

阻塞I/O

傳統阻塞型I/O(BIO)能夠用下圖表示: 微信

Blocking I/O

特色markdown

  • 每一個請求都須要獨立的線程完成數據read,業務處理,數據write的完整操做

問題

  • 當併發數較大時,須要建立大量線程來處理鏈接,系統資源佔用較大
  • 鏈接創建後,若是當前線程暫時沒有數據可讀,則線程就阻塞在read操做上,形成線程資源浪費

I/O複用模型

在I/O複用模型中,會用到select,這個函數也會使進程阻塞,與阻塞I/O所不一樣的,這個函數能夠同時阻塞多個I/O操做,可同時對多個讀操寫操做的I/O函數進行檢測,直到有數據可讀或可寫時,才真正調用I/O操做函數

Netty的非阻塞I/O的實現關鍵是基於I/O複用模型,這裏用Selector對象表示:

Nonblocking I/O

Netty的IO線程NioEventLoop因爲聚合了多路複用器Selector,能夠同時併發處理成百上千個客戶端鏈接。當線程從某客戶端Socket通道進行讀寫數據時,若沒有數據可用時,該線程能夠進行其餘任務。線程一般將非阻塞 IO 的空閒時間用於在其餘通道上執行 IO 操做,因此單獨的線程能夠管理多個輸入和輸出通道。

因爲讀寫操做都是非阻塞的,這就能夠充分提高IO線程的運行效率,避免因爲頻繁I/O阻塞致使的線程掛起,一個I/O線程能夠併發處理N個客戶端鏈接和讀寫操做,這從根本上解決了傳統同步阻塞I/O一鏈接一線程模型,架構的性能、彈性伸縮能力和可靠性都獲得了極大的提高。

基於buffer

傳統的I/O是面向字節流或字符流的,以流式的方式順序地從一個Stream 中讀取一個或多個字節, 所以也就不能隨意改變讀取指針的位置。

在NIO中, 拋棄了傳統的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能從Channel中讀取數據到Buffer中或將數據 Buffer 中寫入到 Channel。

基於buffer操做不像傳統IO的順序操做, NIO 中能夠隨意地讀取任意位置的數據

線程模型

數據報如何讀取?讀取以後的編解碼在哪一個線程進行,編解碼後的消息如何派發,線程模型的不一樣,對性能的影響也很是大。

事件驅動模型

一般,咱們設計一個事件處理模型的程序有兩種思路

  • 輪詢方式 線程不斷輪詢訪問相關事件發生源有沒有發生事件,有發生事件就調用事件處理邏輯。
  • 事件驅動方式 發生事件,主線程把事件放入事件隊列,在另外線程不斷循環消費事件列表中的事件,調用事件對應的處理邏輯處理事件。事件驅動方式也被稱爲消息通知方式,實際上是發佈-訂閱模式的思路。

以GUI的邏輯處理爲例,說明兩種邏輯的不一樣:

  • 輪詢方式 線程不斷輪詢是否發生按鈕點擊事件,若是發生,調用處理邏輯
  • 事件驅動方式 發生點擊事件把事件放入事件隊列,在另外線程消費的事件列表中的事件,根據事件類型調用相關事件處理邏輯

這裏借用O'Reilly 大神關於事件驅動模型解釋圖

事件驅動模型
主要包括4個基本組件:

  • 事件隊列(event queue):接收事件的入口,存儲待處理事件
  • 分發器(event mediator):將不一樣的事件分發到不一樣的業務邏輯單元
  • 事件通道(event channel):分發器與處理器之間的聯繫渠道
  • 事件處理器(event processor):實現業務邏輯,處理完成後會發出事件,觸發下一步操做

能夠看出,相對傳統輪詢模式,事件驅動有以下優勢:

  • 可擴展性好,分佈式的異步架構,事件處理器之間高度解耦,能夠方便擴展事件處理邏輯
  • 高性能,基於隊列暫存事件,能方便並行異步處理事件

Reactor線程模型

Reactor是反應堆的意思,Reactor模型,是指經過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅動處理模式。 服務端程序處理傳入多路請求,並將它們同步分派給請求對應的處理線程,Reactor模式也叫Dispatcher模式,即I/O多路複用統一監聽事件,收到事件後分發(Dispatch給某進程),是編寫高性能網絡服務器的必備技術之一。

Reactor模型中有2個關鍵組成:

  • Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對IO事件作出反應。 它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯繫人

  • Handlers 處理程序執行I/O事件要完成的實際事件,相似於客戶想要與之交談的公司中的實際官員。Reactor經過調度適當的處理程序來響應I/O事件,處理程序執行非阻塞操做

Reactor模型

取決於Reactor的數量和Handler線程數量的不一樣,Reactor模型有3個變種

  • 單Reactor單線程
  • 單Reactor多線程
  • 主從Reactor多線程

能夠這樣理解,Reactor就是一個執行while (true) { selector.select(); ...}循環的線程,會源源不斷的產生新的事件,稱做反應堆很貼切。

篇幅關係,這裏再也不具體展開Reactor特性、優缺點比較,有興趣的讀者能夠參考我以前另一篇文章:《理解高性能網絡模型》

Netty線程模型

Netty主要基於主從Reactors多線程模型(以下圖)作了必定的修改,其中主從Reactor多線程模型有多個Reactor:MainReactor和SubReactor:

  • MainReactor負責客戶端的鏈接請求,並將請求轉交給SubReactor
  • SubReactor負責相應通道的IO讀寫請求
  • 非IO請求(具體邏輯處理)的任務則會直接寫入隊列,等待worker threads進行處理

這裏引用Doug Lee大神的Reactor介紹:Scalable IO in Java裏面關於主從Reactor多線程模型的圖

主從Rreactor多線程模型

特別說明的是: 雖然Netty的線程模型基於主從Reactor多線程,借用了MainReactor和SubReactor的結構,可是實際實現上,SubReactor和Worker線程在同一個線程池中:

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
複製代碼

上面代碼中的bossGroup 和workerGroup是Bootstrap構造方法中傳入的兩個對象,這兩個group均是線程池

  • bossGroup線程池則只是在bind某個端口後,得到其中一個線程做爲MainReactor,專門處理端口的accept事件,每一個端口對應一個boss線程
  • workerGroup線程池會被各個SubReactor和worker線程充分利用

異步處理

異步的概念和同步相對。當一個異步過程調用發出後,調用者不能馬上獲得結果。實際處理這個調用的部件在完成後,經過狀態、通知和回調來通知調用者。

Netty中的I/O操做是異步的,包括bind、write、connect等操做會簡單的返回一個ChannelFuture,調用者並不能馬上得到結果,經過Future-Listener機制,用戶能夠方便的主動獲取或者經過通知機制得到IO操做結果。

當future對象剛剛建立時,處於非完成狀態,調用者能夠經過返回的ChannelFuture來獲取操做執行的狀態,註冊監聽函數來執行完成後的操做,常見有以下:

  • 經過isDone方法來判斷當前操做是否完成
  • 經過isSuccess方法來判斷已完成的當前操做是否成功
  • 經過getCause方法來獲取已完成的當前操做失敗的緣由
  • 經過isCancelled方法來判斷已完成的當前操做是否被取消
  • 經過addListener方法來註冊監聽器,當操做已完成(isDone方法返回完成),將會通知指定的監聽器;若是future對象已完成,則理解通知指定的監聽器

例以下面的代碼中綁定端口是異步操做,當綁定操做處理完,將會調用相應的監聽器處理邏輯

serverBootstrap.bind(port).addListener(future -> {
        if (future.isSuccess()) {
            System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");
        } else {
            System.err.println("端口[" + port + "]綁定失敗!");
        }
    });
複製代碼

相比傳統阻塞I/O,執行I/O操做後線程會被阻塞住, 直到操做完成;異步處理的好處是不會形成線程阻塞,線程在I/O操做期間能夠執行別的程序,在高併發情形下會更穩定和更高的吞吐量。

3 Netty架構設計

前面介紹完Netty相關一些理論介紹,下面從功能特性、模塊組件、運做過程來介紹Netty的架構設計

功能特性

Netty功能特性圖

  • 傳輸服務 支持BIO和NIO
  • 容器集成 支持OSGI、JBossMC、Spring、Guice容器
  • 協議支持 HTTP、Protobuf、二進制、文本、WebSocket等一系列常見協議都支持。 還支持經過實行編碼解碼邏輯來實現自定義協議
  • Core核心 可擴展事件模型、通用通訊API、支持零拷貝的ByteBuf緩衝對象

模塊組件

Bootstrap、ServerBootstrap

Bootstrap意思是引導,一個Netty應用一般由一個Bootstrap開始,主要做用是配置整個Netty程序,串聯各個組件,Netty中Bootstrap類是客戶端程序的啓動引導類,ServerBootstrap是服務端啓動引導類。

Future、ChannelFuture

正如前面介紹,在Netty中全部的IO操做都是異步的,不能馬上得知消息是否被正確處理,可是能夠過一會等它執行完成或者直接註冊一個監聽,具體的實現就是經過Future和ChannelFutures,他們能夠註冊一個監聽,當操做執行成功或失敗時監聽會自動觸發註冊的監聽事件。

Channel

Netty網絡通訊的組件,可以用於執行網絡I/O操做。 Channel爲用戶提供:

  • 當前網絡鏈接的通道的狀態(例如是否打開?是否已鏈接?)
  • 網絡鏈接的配置參數 (例如接收緩衝區大小)
  • 提供異步的網絡I/O操做(如創建鏈接,讀寫,綁定端口),異步調用意味着任何I/O調用都將當即返回,而且不保證在調用結束時所請求的I/O操做已完成。調用當即返回一個ChannelFuture實例,經過註冊監聽器到ChannelFuture上,能夠I/O操做成功、失敗或取消時回調通知調用方。
  • 支持關聯I/O操做與對應的處理程序

不一樣協議、不一樣的阻塞類型的鏈接都有不一樣的 Channel 類型與之對應,下面是一些經常使用的 Channel 類型

  • NioSocketChannel,異步的客戶端 TCP Socket 鏈接
  • NioServerSocketChannel,異步的服務器端 TCP Socket 鏈接
  • NioDatagramChannel,異步的 UDP 鏈接
  • NioSctpChannel,異步的客戶端 Sctp 鏈接
  • NioSctpServerChannel,異步的 Sctp 服務器端鏈接 這些通道涵蓋了 UDP 和 TCP網絡 IO以及文件 IO.

Selector

Netty基於Selector對象實現I/O多路複用,經過 Selector, 一個線程能夠監聽多個鏈接的Channel事件, 當向一個Selector中註冊Channel 後,Selector 內部的機制就能夠自動不斷地查詢(select) 這些註冊的Channel是否有已就緒的I/O事件(例如可讀, 可寫, 網絡鏈接完成等),這樣程序就能夠很簡單地使用一個線程高效地管理多個 Channel 。

NioEventLoop

NioEventLoop中維護了一個線程和任務隊列,支持異步提交執行任務,線程啓動時會調用NioEventLoop的run方法,執行I/O任務和非I/O任務:

  • I/O任務 即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法觸發。
  • 非IO任務 添加到taskQueue中的任務,如register0、bind0等任務,由runAllTasks方法觸發。

兩種任務的執行時間比由變量ioRatio控制,默認爲50,則表示容許非IO任務執行的時間與IO任務的執行時間相等。

NioEventLoopGroup

NioEventLoopGroup,主要管理eventLoop的生命週期,能夠理解爲一個線程池,內部維護了一組線程,每一個線程(NioEventLoop)負責處理多個Channel上的事件,而一個Channel只對應於一個線程。

ChannelHandler

ChannelHandler是一個接口,處理I/O事件或攔截I/O操做,並將其轉發到其ChannelPipeline(業務處理鏈)中的下一個處理程序。

ChannelHandler自己並無提供不少方法,由於這個接口有許多的方法須要實現,方便使用期間,能夠繼承它的子類:

  • ChannelInboundHandler用於處理入站I/O事件
  • ChannelOutboundHandler用於處理出站I/O操做

或者使用如下適配器類:

  • ChannelInboundHandlerAdapter用於處理入站I/O事件
  • ChannelOutboundHandlerAdapter用於處理出站I/O操做
  • ChannelDuplexHandler用於處理入站和出站事件

ChannelHandlerContext

保存Channel相關的全部上下文信息,同時關聯一個ChannelHandler對象

ChannelPipline

保存ChannelHandler的List,用於處理或攔截Channel的入站事件和出站操做。 ChannelPipeline實現了一種高級形式的攔截過濾器模式,使用戶能夠徹底控制事件的處理方式,以及Channel中各個的ChannelHandler如何相互交互。

下圖引用Netty的Javadoc4.1中ChannelPipline的說明,描述了ChannelPipeline中ChannelHandler一般如何處理I/O事件。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler處理,並經過調用ChannelHandlerContext中定義的事件傳播方法(例如ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object))轉發到其最近的處理程序。

I/O Request
                                            via Channel or
                                        ChannelHandlerContext
                                                      |
  +---------------------------------------------------+---------------+
  |                           ChannelPipeline         |               |
  |                                                  \|/              |
  |    +---------------------+            +-----------+----------+    |
  |    | Inbound Handler  N  |            | Outbound Handler  1  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  |               |                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler N-1 |            | Outbound Handler  2  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  .               |
  |               .                                   .               |
  | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
  |        [ method call]                       [method call]         |
  |               .                                   .               |
  |               .                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler  2  |            | Outbound Handler M-1 |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  |               |                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler  1  |            | Outbound Handler  M  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  +---------------+-----------------------------------+---------------+
                  |                                  \|/
  +---------------+-----------------------------------+---------------+
  |               |                                   |               |
  |       [ Socket.read() ]                    [ Socket.write() ]     |
  |                                                                   |
  |  Netty Internal I/O Threads (Transport Implementation)            |
  +-------------------------------------------------------------------+
複製代碼

入站事件由自下而上方向的入站處理程序處理,如圖左側所示。 入站Handler處理程序一般處理由圖底部的I/O線程生成的入站數據。 一般經過實際輸入操做(例如SocketChannel.read(ByteBuffer))從遠程讀取入站數據。

出站事件由上下方向處理,如圖右側所示。 出站Handler處理程序一般會生成或轉換出站傳輸,例如write請求。 I/O線程一般執行實際的輸出操做,例如SocketChannel.write(ByteBuffer)。

在 Netty 中每一個 Channel 都有且僅有一個 ChannelPipeline 與之對應, 它們的組成關係以下:

一個 Channel 包含了一個 ChannelPipeline, 而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向鏈表, 而且每一個 ChannelHandlerContext 中又關聯着一個 ChannelHandler。入站事件和出站事件在一個雙向鏈表中,入站事件會從鏈表head日後傳遞到最後一個入站的handler,出站事件會從鏈表tail往前傳遞到最前一個出站的handler,兩種類型的handler互不干擾。

工做原理架構

初始化並啓動Netty服務端過程以下:

public static void main(String[] args) {
        // 建立mainReactor
        NioEventLoopGroup boosGroup = new NioEventLoopGroup();
        // 建立工做線程組
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        final ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap 
                 // 組裝NioEventLoopGroup 
                .group(boosGroup, workerGroup)
                 // 設置channel類型爲NIO類型
                .channel(NioServerSocketChannel.class)
                // 設置鏈接配置參數
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                // 配置入站、出站事件handler
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) {
                        // 配置入站、出站事件channel
                        ch.pipeline().addLast(...);
                        ch.pipeline().addLast(...);
                    }
    });

        // 綁定端口
        int port = 8080;
        serverBootstrap.bind(port).addListener(future -> {
            if (future.isSuccess()) {
                System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");
            } else {
                System.err.println("端口[" + port + "]綁定失敗!");
            }
        });
}
複製代碼
  • 基本過程以下:
  • 1 初始化建立2個NioEventLoopGroup,其中boosGroup用於Accetpt鏈接創建事件並分發請求, workerGroup用於處理I/O讀寫事件和業務邏輯
  • 2 基於ServerBootstrap(服務端啓動引導類),配置EventLoopGroup、Channel類型,鏈接參數、配置入站、出站事件handler
  • 3 綁定端口,開始工做

結合上面的介紹的Netty Reactor模型,介紹服務端Netty的工做架構圖:

服務端Netty Reactor工做架構圖

server端包含1個Boss NioEventLoopGroup和1個Worker NioEventLoopGroup,NioEventLoopGroup至關於1個事件循環組,這個組裏包含多個事件循環NioEventLoop,每一個NioEventLoop包含1個selector和1個事件循環線程。

每一個Boss NioEventLoop循環執行的任務包含3步:

  • 1 輪詢accept事件
  • 2 處理accept I/O事件,與Client創建鏈接,生成NioSocketChannel,並將NioSocketChannel註冊到某個Worker NioEventLoop的Selector上
  • 3 處理任務隊列中的任務,runAllTasks。任務隊列中的任務包括用戶調用eventloop.execute或schedule執行的任務,或者其它線程提交到該eventloop的任務

每一個Worker NioEventLoop循環執行的任務包含3步:

  • 1 輪詢read、write事件
  • 2 處理I/O事件,即read、write事件,在NioSocketChannel可讀、可寫事件發生時進行處理
  • 3 處理任務隊列中的任務,runAllTasks

其中任務隊列中的task有3種典型使用場景

  • 1 用戶程序自定義的普通任務
ctx.channel().eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        //...
    }
});
複製代碼
  • 2 非當前reactor線程調用channel的各類方法 例如在推送系統的業務線程裏面,根據用戶的標識,找到對應的channel引用,而後調用write類方法向該用戶推送消息,就會進入到這種場景。最終的write會提交到任務隊列中後被異步消費。

  • 3 用戶自定義定時任務

ctx.channel().eventLoop().schedule(new Runnable() {
    @Override
    public void run() {

    }
}, 60, TimeUnit.SECONDS);
複製代碼

4 總結

如今穩定推薦使用的主流版本仍是Netty4,Netty5 中使用了 ForkJoinPool,增長了代碼的複雜度,可是對性能的改善卻不明顯,因此這個版本不推薦使用,官網也沒有提供下載連接。

Netty 入門門檻相對較高,實際上是由於這方面的資料較少,並非由於他有多難,你們其實均可以像搞透 Spring 同樣搞透 Netty。在學習以前,建議先理解透整個框架原理結構,運行過程,能夠少走不少彎路。

參考

Netty入門與實戰:仿寫微信 IM 即時通信系統

Netty官網

Netty 4.x學習筆記 - 線程模型

Netty入門與實戰

理解高性能網絡模型

Netty基本原理介紹

software-architecture-patterns.pdf

Netty高性能之道 —— 李林鋒

《Netty In Action》

《Netty權威指南》

相關文章
相關標籤/搜索