本文基於 Netty 4.1 展開介紹相關理論模型,使用場景,基本組件、總體架構,知其然且知其因此然,但願給你們在實際開發實踐、學習開源項目方面提供參考。react
Netty 是一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端。編程
JDK 原生 NIO 程序的問題設計模式
JDK 原生也有一套網絡應用程序 API,可是存在一系列問題,主要以下:緩存
Netty 的特色安全
Netty 對 JDK 自帶的 NIO 的 API 進行封裝,解決上述問題,主要特色有:服務器
Netty 常見使用場景微信
Netty 常見的使用場景以下:網絡
有興趣的讀者能夠了解一下目前有哪些開源項目使用了 Netty:Related Projects。多線程
Netty 高性能設計架構
Netty 做爲異步事件驅動的網絡,高性能之處主要來自於其 I/O 模型和線程處理模型,前者決定如何收發數據,後者決定如何處理數據。
I/O 模型
用什麼樣的通道將數據發送給對方,BIO、NIO 或者 AIO,I/O 模型在很大程度上決定了框架的性能。
阻塞 I/O
傳統阻塞型 I/O(BIO)能夠用下圖表示:
Blocking I/O
特色以下:
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 個基本組件:
能夠看出,相對傳統輪詢模式,事件驅動有以下優勢:
Reactor 線程模型
Reactor 是反應堆的意思,Reactor 模型是指經過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅動處理模式。
服務端程序處理傳入多路請求,並將它們同步分派給請求對應的處理線程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多了複用統一監聽事件,收到事件後分發(Dispatch 給某進程),是編寫高性能網絡服務器的必備技術之一。
Reactor 模型中有 2 個關鍵組成:
Reactor 模型
取決於 Reactor 的數量和 Hanndler 線程數量的不一樣,Reactor 模型有 3 個變種:
能夠這樣理解,Reactor 就是一個執行 while (true) { selector.select(); …} 循環的線程,會源源不斷的產生新的事件,稱做反應堆很貼切。
篇幅關係,這裏再也不具體展開 Reactor 特性、優缺點比較,有興趣的讀者能夠參考我以前另一篇文章:《理解高性能網絡模型》。
Netty 線程模型
Netty 主要基於主從 Reactors 多線程模型(以下圖)作了必定的修改,其中主從 Reactor 多線程模型有多個 Reactor:
這裏引用 Doug Lee 大神的 Reactor 介紹:Scalable IO in Java 裏面關於主從 Reactor 多線程模型的圖:
主從 Rreactor 多線程模型
特別說明的是:雖然 Netty 的線程模型基於主從 Reactor 多線程,借用了 MainReactor 和 SubReactor 的結構。可是實際實現上 SubReactor 和 Worker 線程在同一個線程池中:
EventLoopGroup bossGroup = newNioEventLoopGroup();
EventLoopGroup workerGroup = newNioEventLoopGroup();
ServerBootstrap server= newServerBootstrap();
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( newDate() + ": 端口["+ port + "]綁定成功!");
} else{
System.err. println( "端口["+ port + "]綁定失敗!");
}
});
相比傳統阻塞 I/O,執行 I/O 操做後線程會被阻塞住, 直到操做完成;異步處理的好處是不會形成線程阻塞,線程在 I/O 操做期間能夠執行別的程序,在高併發情形下會更穩定和更高的吞吐量。
Netty 架構設計
前面介紹完 Netty 相關一些理論,下面從功能特性、模塊組件、運做過程來介紹 Netty 的架構設計。
功能特性
Netty 功能特性以下:
模塊組件
Bootstrap、ServerBootstrap
Bootstrap 意思是引導,一個 Netty 應用一般由一個 Bootstrap 開始,主要做用是配置整個 Netty 程序,串聯各個組件,Netty 中 Bootstrap 類是客戶端程序的啓動引導類,ServerBootstrap 是服務端啓動引導類。
Future、ChannelFuture
正如前面介紹,在 Netty 中全部的 IO 操做都是異步的,不能馬上得知消息是否被正確處理。
可是能夠過一會等它執行完成或者直接註冊一個監聽,具體的實現就是經過 Future 和 ChannelFutures,他們能夠註冊一個監聽,當操做執行成功或失敗時監聽會自動觸發註冊的監聽事件。
Channel
Netty 網絡通訊的組件,可以用於執行網絡 I/O 操做。Channel 爲用戶提供:
不一樣協議、不一樣的阻塞類型的鏈接都有不一樣的 Channel 類型與之對應。下面是一些經常使用的 Channel 類型:
Selector
Netty 基於 Selector 對象實現 I/O 多路複用,經過 Selector 一個線程能夠監聽多個鏈接的 Channel 事件。
當向一個 Selector 中註冊 Channel 後,Selector 內部的機制就能夠自動不斷地查詢(Select) 這些註冊的 Channel 是否有已就緒的 I/O 事件(例如可讀,可寫,網絡鏈接完成等),這樣程序就能夠很簡單地使用一個線程高效地管理多個 Channel 。
NioEventLoop
NioEventLoop 中維護了一個線程和任務隊列,支持異步提交執行任務,線程啓動時會調用 NioEventLoop 的 run 方法,執行 I/O 任務和非 I/O 任務:
兩種任務的執行時間比由變量 ioRatio 控制,默認爲 50,則表示容許非 IO 任務執行的時間與 IO 任務的執行時間相等。
NioEventLoopGroup
NioEventLoopGroup,主要管理 eventLoop 的生命週期,能夠理解爲一個線程池,內部維護了一組線程,每一個線程(NioEventLoop)負責處理多個 Channel 上的事件,而一個 Channel 只對應於一個線程。
ChannelHandler
ChannelHandler 是一個接口,處理 I/O 事件或攔截 I/O 操做,並將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程序。
ChannelHandler 自己並無提供不少方法,由於這個接口有許多的方法須要實現,方便使用期間,能夠繼承它的子類:
或者使用如下適配器類:
ChannelHandlerContext
保存 Channel 相關的全部上下文信息,同時關聯一個 ChannelHandler 對象。
ChannelPipline
保存 ChannelHandler 的 List,用於處理或攔截 Channel 的入站事件和出站操做。
ChannelPipeline 實現了一種高級形式的攔截過濾器模式,使用戶能夠徹底控制事件的處理方式,以及 Channel 中各個的 ChannelHandler 如何相互交互。
下圖引用 Netty 的 Javadoc 4.1 中 ChannelPipeline 的說明,描述了 ChannelPipeline 中 ChannelHandler 一般如何處理 I/O 事件。
I/O 事件由 ChannelInboundHandler 或 ChannelOutboundHandler 處理,並經過調用 ChannelHandlerContext 中定義的事件傳播方法。
例如 ChannelHandlerContext.fireChannelRead(Object)和 ChannelOutboundInvoker.write(Object)轉發到其最近的處理程序。
入站事件由自下而上方向的入站處理程序處理,如圖左側所示。入站 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 工做原理架構
初始化並啓動 Netty 服務端過程以下:
publicstaticvoidmain(String[] args) {
// 建立mainReactor
NioEventLoopGroup boosGroup = newNioEventLoopGroup();
// 建立工做線程組
NioEventLoopGroup workerGroup = newNioEventLoopGroup();
final ServerBootstrap serverBootstrap = newServerBootstrap();
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( newChannelInitializer<NioSocketChannel>() {
@ Override
protectedvoidinitChannel(NioSocketChannel ch) {
// 配置入站、出站事件channel
ch.pipeline().addLast(...);
ch.pipeline().addLast(...);
}
});
// 綁定端口
intport = 8080;
serverBootstrap.bind(port).addListener(future -> {
if(future.isSuccess()) {
System. out.println( newDate() + ": 端口["+ port + "]綁定成功!");
} else{
System.err.println( "端口["+ port + "]綁定失敗!");
}
});
}
基本過程以下:
結合上面介紹的 Netty Reactor 模型,介紹服務端 Netty 的工做架構圖:
服務端 Netty Reactor 工做架構圖
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( newRunnable() {
@Override
publicvoidrun(){
//...
}
});
②非當前 Reactor 線程調用 Channel 的各類方法
例如在推送系統的業務線程裏面,根據用戶的標識,找到對應的 Channel 引用,而後調用 Write 類方法向該用戶推送消息,就會進入到這種場景。最終的 Write 會提交到任務隊列中後被異步消費。
③用戶自定義定時任務
ctx.channel().eventLoop().schedule( newRunnable() {
@Override
publicvoidrun(){
}
}, 60, TimeUnit.SECONDS);
總結
如今穩定推薦使用的主流版本仍是 Netty4,Netty5 中使用了 ForkJoinPool,增長了代碼的複雜度,可是對性能的改善卻不明顯,因此這個版本不推薦使用,官網也沒有提供下載連接。
Netty 入門門檻相對較高,是由於這方面的資料較少,並非由於它有多難,你們其實均可以像搞透 Spring 同樣搞透 Netty。
在學習以前,建議先理解透整個框架原理結構,運行過程,能夠少走不少彎路。
參考資料:
做者:陳彩華
編輯:陶家龍、孫淑娟
出處:轉載自Hollis(ID:hollischuang)微信公衆號