本文基於Netty4.1展開介紹相關理論模型,使用場景,基本組件、總體架構,知其然且知其因此然,但願給讀者提供學習實踐參考。html
Netty是 一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端。java
JDK原生也有一套網絡應用程序API,可是存在一系列問題,主要以下:react
Netty的對JDK自帶的NIO的API進行封裝,解決上述問題,主要特色有:編程
Netty常見的使用場景以下:緩存
有興趣的讀者能夠了解一下目前有哪些開源項目使用了 Netty:Related projects安全
Netty做爲異步事件驅動的網絡,高性能之處主要來自於其I/O模型和線程處理模型,前者決定如何收發數據,後者決定如何處理數據bash
用什麼樣的通道將數據發送給對方,BIO、NIO或者AIO,I/O模型在很大程度上決定了框架的性能服務器
傳統阻塞型I/O(BIO)能夠用下圖表示: 微信
特色markdown
問題
在I/O複用模型中,會用到select,這個函數也會使進程阻塞,與阻塞I/O所不一樣的,這個函數能夠同時阻塞多個I/O操做,可同時對多個讀操寫操做的I/O函數進行檢測,直到有數據可讀或可寫時,才真正調用I/O操做函數
Netty的非阻塞I/O的實現關鍵是基於I/O複用模型,這裏用Selector對象表示:
Netty的IO線程NioEventLoop因爲聚合了多路複用器Selector,能夠同時併發處理成百上千個客戶端鏈接。當線程從某客戶端Socket通道進行讀寫數據時,若沒有數據可用時,該線程能夠進行其餘任務。線程一般將非阻塞 IO 的空閒時間用於在其餘通道上執行 IO 操做,因此單獨的線程能夠管理多個輸入和輸出通道。
因爲讀寫操做都是非阻塞的,這就能夠充分提高IO線程的運行效率,避免因爲頻繁I/O阻塞致使的線程掛起,一個I/O線程能夠併發處理N個客戶端鏈接和讀寫操做,這從根本上解決了傳統同步阻塞I/O一鏈接一線程模型,架構的性能、彈性伸縮能力和可靠性都獲得了極大的提高。
傳統的I/O是面向字節流或字符流的,以流式的方式順序地從一個Stream 中讀取一個或多個字節, 所以也就不能隨意改變讀取指針的位置。
在NIO中, 拋棄了傳統的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能從Channel中讀取數據到Buffer中或將數據 Buffer 中寫入到 Channel。
基於buffer操做不像傳統IO的順序操做, NIO 中能夠隨意地讀取任意位置的數據
數據報如何讀取?讀取以後的編解碼在哪一個線程進行,編解碼後的消息如何派發,線程模型的不一樣,對性能的影響也很是大。
一般,咱們設計一個事件處理模型的程序有兩種思路
以GUI的邏輯處理爲例,說明兩種邏輯的不一樣:
這裏借用O'Reilly 大神關於事件驅動模型解釋圖
主要包括4個基本組件:能夠看出,相對傳統輪詢模式,事件驅動有以下優勢:
Reactor是反應堆的意思,Reactor模型,是指經過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅動處理模式。 服務端程序處理傳入多路請求,並將它們同步分派給請求對應的處理線程,Reactor模式也叫Dispatcher模式,即I/O多路複用統一監聽事件,收到事件後分發(Dispatch給某進程),是編寫高性能網絡服務器的必備技術之一。
Reactor模型中有2個關鍵組成:
Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對IO事件作出反應。 它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯繫人
Handlers 處理程序執行I/O事件要完成的實際事件,相似於客戶想要與之交談的公司中的實際官員。Reactor經過調度適當的處理程序來響應I/O事件,處理程序執行非阻塞操做
取決於Reactor的數量和Handler線程數量的不一樣,Reactor模型有3個變種
能夠這樣理解,Reactor就是一個執行while (true) { selector.select(); ...}循環的線程,會源源不斷的產生新的事件,稱做反應堆很貼切。
篇幅關係,這裏再也不具體展開Reactor特性、優缺點比較,有興趣的讀者能夠參考我以前另一篇文章:《理解高性能網絡模型》
Netty主要基於主從Reactors多線程模型(以下圖)作了必定的修改,其中主從Reactor多線程模型有多個Reactor:MainReactor和SubReactor:
這裏引用Doug Lee大神的Reactor介紹:Scalable IO in Java裏面關於主從Reactor多線程模型的圖
特別說明的是: 雖然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均是線程池
異步的概念和同步相對。當一個異步過程調用發出後,調用者不能馬上獲得結果。實際處理這個調用的部件在完成後,經過狀態、通知和回調來通知調用者。
Netty中的I/O操做是異步的,包括bind、write、connect等操做會簡單的返回一個ChannelFuture,調用者並不能馬上得到結果,經過Future-Listener機制,用戶能夠方便的主動獲取或者經過通知機制得到IO操做結果。
當future對象剛剛建立時,處於非完成狀態,調用者能夠經過返回的ChannelFuture來獲取操做執行的狀態,註冊監聽函數來執行完成後的操做,常見有以下:
例以下面的代碼中綁定端口是異步操做,當綁定操做處理完,將會調用相應的監聽器處理邏輯
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操做期間能夠執行別的程序,在高併發情形下會更穩定和更高的吞吐量。
前面介紹完Netty相關一些理論介紹,下面從功能特性、模塊組件、運做過程來介紹Netty的架構設計
Bootstrap意思是引導,一個Netty應用一般由一個Bootstrap開始,主要做用是配置整個Netty程序,串聯各個組件,Netty中Bootstrap類是客戶端程序的啓動引導類,ServerBootstrap是服務端啓動引導類。
正如前面介紹,在Netty中全部的IO操做都是異步的,不能馬上得知消息是否被正確處理,可是能夠過一會等它執行完成或者直接註冊一個監聽,具體的實現就是經過Future和ChannelFutures,他們能夠註冊一個監聽,當操做執行成功或失敗時監聽會自動觸發註冊的監聽事件。
Netty網絡通訊的組件,可以用於執行網絡I/O操做。 Channel爲用戶提供:
不一樣協議、不一樣的阻塞類型的鏈接都有不一樣的 Channel 類型與之對應,下面是一些經常使用的 Channel 類型
Netty基於Selector對象實現I/O多路複用,經過 Selector, 一個線程能夠監聽多個鏈接的Channel事件, 當向一個Selector中註冊Channel 後,Selector 內部的機制就能夠自動不斷地查詢(select) 這些註冊的Channel是否有已就緒的I/O事件(例如可讀, 可寫, 網絡鏈接完成等),這樣程序就能夠很簡單地使用一個線程高效地管理多個 Channel 。
NioEventLoop中維護了一個線程和任務隊列,支持異步提交執行任務,線程啓動時會調用NioEventLoop的run方法,執行I/O任務和非I/O任務:
兩種任務的執行時間比由變量ioRatio控制,默認爲50,則表示容許非IO任務執行的時間與IO任務的執行時間相等。
NioEventLoopGroup,主要管理eventLoop的生命週期,能夠理解爲一個線程池,內部維護了一組線程,每一個線程(NioEventLoop)負責處理多個Channel上的事件,而一個Channel只對應於一個線程。
ChannelHandler是一個接口,處理I/O事件或攔截I/O操做,並將其轉發到其ChannelPipeline(業務處理鏈)中的下一個處理程序。
ChannelHandler自己並無提供不少方法,由於這個接口有許多的方法須要實現,方便使用期間,能夠繼承它的子類:
或者使用如下適配器類:
保存Channel相關的全部上下文信息,同時關聯一個ChannelHandler對象
保存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 + "]綁定失敗!"); } }); } 複製代碼
結合上面的介紹的Netty Reactor模型,介紹服務端Netty的工做架構圖:
server端包含1個Boss NioEventLoopGroup和1個Worker NioEventLoopGroup,NioEventLoopGroup至關於1個事件循環組,這個組裏包含多個事件循環NioEventLoop,每一個NioEventLoop包含1個selector和1個事件循環線程。
每一個Boss NioEventLoop循環執行的任務包含3步:
每一個Worker NioEventLoop循環執行的任務包含3步:
其中任務隊列中的task有3種典型使用場景
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); 複製代碼
如今穩定推薦使用的主流版本仍是Netty4,Netty5 中使用了 ForkJoinPool,增長了代碼的複雜度,可是對性能的改善卻不明顯,因此這個版本不推薦使用,官網也沒有提供下載連接。
Netty 入門門檻相對較高,實際上是由於這方面的資料較少,並非由於他有多難,你們其實均可以像搞透 Spring 同樣搞透 Netty。在學習以前,建議先理解透整個框架原理結構,運行過程,能夠少走不少彎路。
software-architecture-patterns.pdf
《Netty In Action》
《Netty權威指南》