再有人問你Netty是什麼,就把這篇文章發給他
前言
本文基於Netty4.1展開介紹相關理論模型,使用場景,基本元件、整體架構,知其然且知其所以然,希望給大家在實際開發實踐、學習開源專案提供參考。
這是一篇萬字長文,建議先收藏,轉發後再看。
Netty簡介
Netty是 一個非同步事件驅動的網路應用程式框架,用於快速開發可維護的高效能協議伺服器和客戶端。
JDK原生NIO程式的問題
JDK原生也有一套網路應用程式API,但是存在一系列問題,主要如下:
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模型和執行緒處理模型,前者決定如何收發資料,後者決定如何處理資料
I/O模型
用什麼樣的通道將資料傳送給對方,BIO、NIO或者AIO,I/O模型在很大程度上決定了框架的效能
阻塞I/O
傳統阻塞型I/O(BIO)可以用下圖表示:
Blocking I/O
特點
每個請求都需要獨立的執行緒完成資料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
Reactor在一個單獨的執行緒中執行,負責監聽和分發事件,分發給適當的處理程式來對IO事件做出反應。 它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯絡人
Handlers
處理程式執行I/O事件要完成的實際事件,類似於客戶想要與之交談的公司中的實際官員。Reactor通過排程適當的處理程式來響應I/O事件,處理程式執行非阻塞操作
Reactor模型
取決於Reactor的數量和Hanndler執行緒數量的不同,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操作期間可以執行別的程式,在高併發情形下會更穩定和更高的吞吐量。
Netty架