再有人問你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架構設計

前面介紹完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))轉發到其最近的處理程序。

入站事件由自下而上方向的入站處理程序處理,如圖左側所示。 入站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服務端過程如下:

基本過程如下:

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 用戶程序自定義的普通任務

2 非當前reactor線程調用channel的各種方法

例如在推送系統的業務線程裏面,根據用戶的標識,找到對應的channel引用,然後調用write類方法向該用戶推送消息,就會進入到這種場景。最終的write會提交到任務隊列中後被異步消費。

3 用戶自定義定時任務

總結

現在穩定推薦使用的主流版本還是Netty4,Netty5 中使用了 ForkJoinPool,增加了代碼的複雜度,但是對性能的改善卻不明顯,所以這個版本不推薦使用,官網也沒有提供下載鏈接。

Netty 入門門檻相對較高,其實是因爲這方面的資料較少,並不是因爲他有多難,大家其實都可以像搞透 Spring 一樣搞透 Netty。在學習之前,建議先理解透整個框架原理結構,運行過程,可以少走很多彎路。

歡迎工作一到五年的Java工程師朋友們加入Java填坑之路:860113481

羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章