在分布式系統(tǒng)被廣泛應(yīng)用的今天含潘,服務(wù)有可能分布在網(wǎng)絡(luò)中的各個節(jié)點中。因此线婚,服務(wù)之間的調(diào)用對分布式系統(tǒng)來說遏弱,就顯得尤為重要。
對于高性能的 RPC 框架塞弊,Netty 作為異步通信框架磺芭,幾乎成為必備品崖瞭。例如,Dubbo 框架中通信組件,還有 RocketMQ 中生產(chǎn)者和消費者的通信予颤,都使用了 Netty涝婉。今天劲件,我們來看看 Netty 的基本架構(gòu)和原理未巫。
Netty 的特點與 NIO
Netty 是一個異步的、基于事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用框架蔗草,它可以用來開發(fā)高性能服務(wù)端和客戶端咒彤。
以前編寫網(wǎng)絡(luò)調(diào)用程序的時候,我們都會在客戶端創(chuàng)建一個 Socket咒精,通過這個 Socket 連接到服務(wù)端镶柱。
服務(wù)端根據(jù)這個 Socket 創(chuàng)建一個 Thread,用來發(fā)出請求模叙⌒穑客戶端在發(fā)起調(diào)用以后,需要等待服務(wù)端處理完成范咨,才能繼續(xù)后面的操作故觅。這樣線程會出現(xiàn)等待的狀態(tài)。
如果客戶端請求數(shù)越多渠啊,服務(wù)端創(chuàng)建的處理線程也會越多输吏,JVM 如此多的線程并不是一件容易的事。
使用阻賽 I/O 處理多個連接
為了解決上述的問題替蛉,推出了 NIO 的概念贯溅,也就是(Non-blocking I/O)拄氯。其中,Selector 機(jī)制就是 NIO 的核心它浅。
當(dāng)每次客戶端請求時译柏,會創(chuàng)建一個 Socket Channel,并將其注冊到 Selector 上(多路復(fù)用器)罚缕。
然后艇纺,Selector 關(guān)注服務(wù)端 IO 讀寫事件,此時客戶端并不用等待 IO 事件完成邮弹,可以繼續(xù)做接下來的工作。
一旦蚓聘,服務(wù)端完成了 IO 讀寫操作腌乡,Selector 會接到通知,同時告訴客戶端 IO 操作已經(jīng)完成夜牡。
接到通知的客戶端与纽,就可以通過 SocketChannel 獲取需要的數(shù)據(jù)了。
NIO 機(jī)制與 Selector
上面描述的過程有點異步的意思塘装,不過急迂,Selector 實現(xiàn)的并不是真正意義上的異步操作。
因為 Selector 需要通過線程阻塞的方式監(jiān)聽 IO 事件變更蹦肴,只是這種方式?jīng)]有讓客戶端等待僚碎,是 Selector 在等待 IO 返回,并且通知客戶端去獲取數(shù)據(jù)阴幌。真正“異步 IO”(AIO)這里不展開介紹勺阐,有興趣可以自行查找。
說好了 NIO 再來談?wù)?Netty矛双,Netty 作為 NIO 的實現(xiàn)渊抽,它適用于服務(wù)器/客戶端通訊的場景,以及針對于 TCP 協(xié)議下的高并發(fā)應(yīng)用议忽。
對于開發(fā)者來說懒闷,它具有以下特點:
- 對 NIO 進(jìn)行封裝,開發(fā)者不需要關(guān)注 NIO 的底層原理栈幸,只需要調(diào)用 Netty 組件就能夠完成工作愤估。
- 對網(wǎng)絡(luò)調(diào)用透明,從 Socket 建立 TCP 連接到網(wǎng)絡(luò)異常的處理都做了包裝侦镇。
- 對數(shù)據(jù)處理靈活灵疮, Netty 支持多種序列化框架,通過“ChannelHandler”機(jī)制壳繁,可以自定義“編/解碼器”震捣。
- 對性能調(diào)優(yōu)友好荔棉,Netty 提供了線程池模式以及 Buffer 的重用機(jī)制(對象池化),不需要構(gòu)建復(fù)雜的多線程模型和操作隊列蒿赢。
從一個簡單的例子開始
開篇講到了润樱,為了滿足高并發(fā)下網(wǎng)絡(luò)請求,引入了 NIO 的概念羡棵。Netty 是針對 NIO 的實現(xiàn)壹若,在 NIO 封裝,網(wǎng)絡(luò)調(diào)用皂冰,數(shù)據(jù)處理以及性能優(yōu)化等方面都有不俗的表現(xiàn)店展。
學(xué)習(xí)架構(gòu)最容易的方式就是從實例入手,從客戶端訪問服務(wù)端的代碼來看看 Netty 是如何運作的秃流。再一次介紹代碼中調(diào)用的組件以及組件的工作原理赂蕴。
假設(shè)有一個客戶端去調(diào)用一個服務(wù)端,假設(shè)服務(wù)端叫做 EchoServer舶胀,客戶端叫做 EchoClient概说,用 Netty 架構(gòu)實現(xiàn)代碼如下。
服務(wù)端代碼
構(gòu)建服務(wù)器端嚣伐,假設(shè)服務(wù)器接受客戶端傳來的信息糖赔,然后在控制臺打印。首先轩端,生成 EchoServer放典,在構(gòu)造函數(shù)中傳入需要監(jiān)聽的端口號
構(gòu)造函數(shù)中傳入需要監(jiān)聽的端口號
接下來就是服務(wù)的啟動方法:
啟動 NettyServer 的 Start 方法
Server 的啟動方法涉及到了一些組件的調(diào)用,例如 EventLoopGroup船万,Channel刻撒。這些會在后面詳細(xì)講解。
這里有個大致的印象就好:
- 創(chuàng)建 EventLoopGroup耿导。
- 創(chuàng)建 ServerBootstrap声怔。
- 指定所使用的 NIO 傳輸 Channel。
- 使用指定的端口設(shè)置套接字地址舱呻。
- 添加一個 ServerHandler 到 Channel 的 ChannelPipeline醋火。
- 異步地綁定服務(wù)器;調(diào)用 sync() 方法阻塞等待直到綁定完成箱吕。
- 獲取 Channel 的 CloseFuture芥驳,并且阻塞當(dāng)前線程直到它完成。
- 關(guān)閉 EventLoopGroup茬高,釋放所有的資源兆旬。
NettyServer 啟動以后會監(jiān)聽某個端口的請求,當(dāng)接受到了請求就需要處理了怎栽。在 Netty 中客戶端請求服務(wù)端丽猬,被稱為“入站”操作宿饱。
可以通過 ChannelInboundHandlerAdapter 實現(xiàn),具體內(nèi)容如下:
處理來自客戶端的請求
從上面的代碼可以看出脚祟,服務(wù)端處理的代碼包含了三個方法谬以。這三個方法都是根據(jù)事件觸發(fā)的。
他們分別是:
- 當(dāng)接收到消息時的操作由桌,channelRead为黎。
- 消息讀取完成時的方法,channelReadComplete行您。
- 出現(xiàn)異常時的方法铭乾,exceptionCaught。
客戶端代碼
客戶端和服務(wù)端的代碼基本相似娃循,在初始化時需要輸入服務(wù)端的 IP 和 Port片橡。
同樣在客戶端啟動函數(shù)中包括以下內(nèi)容:
客戶端啟動程序的順序:
- 創(chuàng)建 Bootstrap。
- 指定 EventLoopGroup 用來監(jiān)聽事件淮野。
- 定義 Channel 的傳輸模式為 NIO(Non-BlockingInputOutput)。
- 設(shè)置服務(wù)器的 InetSocketAddress吹泡。
- 在創(chuàng)建 Channel 時骤星,向 ChannelPipeline 中添加一個 EchoClientHandler 實例。
- 連接到遠(yuǎn)程節(jié)點爆哑,阻塞等待直到連接完成洞难。
- 阻塞,直到 Channel 關(guān)閉揭朝。
- 關(guān)閉線程池并且釋放所有的資源队贱。
客戶端在完成以上操作以后,會與服務(wù)端建立連接從而傳輸數(shù)據(jù)潭袱。同樣在接受到 Channel 中觸發(fā)的事件時柱嫌,客戶端會觸發(fā)對應(yīng)事件的操作。
例如 Channel 激活屯换,客戶端接受到服務(wù)端的消息编丘,或者發(fā)生異常的捕獲。
從代碼結(jié)構(gòu)上看還是比較簡單的彤悔。服務(wù)端和客戶端分別初始化創(chuàng)建監(jiān)聽和連接嘉抓。然后分別定義各自的 Handler 處理對方的請求。
服務(wù)端/客戶端初始化和事件處理
Netty 核心組件
通過上面的簡單例子晕窑,發(fā)現(xiàn)有些 Netty 組件在服務(wù)初始化以及通訊時被用到抑片,下面就來介紹一下這些組件的用途和關(guān)系。
①Channel
通過上面例子可以看出杨赤,當(dāng)客戶端和服務(wù)端連接的時候會建立一個 Channel敞斋。
這個 Channel 我們可以理解為 Socket 連接截汪,它負(fù)責(zé)基本的 IO 操作,例如:bind()渺尘,connect()挫鸽,read(),write() 等等鸥跟。
簡單的說丢郊,Channel 就是代表連接,實體之間的連接医咨,程序之間的連接枫匾,文件之間的連接,設(shè)備之間的連接拟淮。同時它也是數(shù)據(jù)入站和出站的載體干茉。
②EventLoop 和 EventLoopGroup
既然有了 Channel 連接服務(wù),讓信息之間可以流動很泊。如果服務(wù)發(fā)出的消息稱作“出站”消息角虫,服務(wù)接受的消息稱作“入站”消息。那么消息的“出站”/“入站”就會產(chǎn)生事件(Event)委造。
例如:連接已激活戳鹅;數(shù)據(jù)讀取昏兆;用戶事件枫虏;異常事件;打開鏈接爬虱;關(guān)閉鏈接等等隶债。
順著這個思路往下想,有了數(shù)據(jù)跑筝,數(shù)據(jù)的流動產(chǎn)生事件死讹,那么就有一個機(jī)制去監(jiān)控和協(xié)調(diào)事件。
這個機(jī)制(組件)就是 EventLoop继蜡。在 Netty 中每個 Channel 都會被分配到一個 EventLoop回俐。一個 EventLoop 可以服務(wù)于多個 Channel。
每個 EventLoop 會占用一個 Thread稀并,同時這個 Thread 會處理 EventLoop 上面發(fā)生的所有 IO 操作和事件(Netty 4.0)仅颇。
EventLoopGroup,EventLoop 和 Channel 的關(guān)系
在異步傳輸?shù)那闆r下碘举,一個 EventLoop 是可以處理多個 Channel 中產(chǎn)生的事件的忘瓦,它主要的工作就是事件的發(fā)現(xiàn)以及通知。
相對于以前一個 Channel 就占用一個 Thread 的情況。Netty 的方式就要合理多了耕皮。
客戶端發(fā)送消息到服務(wù)端境蜕,EventLoop 發(fā)現(xiàn)以后會告訴服務(wù)端:“你去獲取消息”,同時客戶端進(jìn)行其他的工作凌停。
當(dāng) EventLoop 檢測到服務(wù)端返回的消息粱年,也會通知客戶端:“消息返回了,你去取吧“罚拟√ㄊ客戶端再去獲取消息。整個過程 EventLoop 就是監(jiān)視器+傳聲筒赐俗。
③ChannelHandler拉队,ChannelPipeline 和 ChannelHandlerContext
如果說 EventLoop 是事件的通知者,那么 ChannelHandler 就是事件的處理者阻逮。
在 ChannelHandler 中可以添加一些業(yè)務(wù)代碼粱快,例如數(shù)據(jù)轉(zhuǎn)換,邏輯運算等等叔扼。
正如上面例子中展示的事哭,Server 和 Client 分別都有一個 ChannelHandler 來處理,讀取信息瓜富,網(wǎng)絡(luò)可用慷蠕,網(wǎng)絡(luò)異常之類的信息。
并且食呻,針對出站和入站的事件,有不同的 ChannelHandler澎现,分別是:
- ChannelInBoundHandler(入站事件處理器)
- ChannelOutBoundHandler(出站事件處理器)
假設(shè)每次請求都會觸發(fā)事件仅胞,而由 ChannelHandler 來處理這些事件,這個事件的處理順序是由 ChannelPipeline 來決定的剑辫。
ChannelHanlder 處理干旧,出站/入站的事件
ChannelPipeline 為 ChannelHandler 鏈提供了容器。到 Channel 被創(chuàng)建的時候妹蔽,會被 Netty 框架自動分配到 ChannelPipeline 上椎眯。
ChannelPipeline 保證 ChannelHandler 按照一定順序處理事件,當(dāng)事件觸發(fā)以后胳岂,會將數(shù)據(jù)通過 ChannelPipeline 按照一定的順序通過 ChannelHandler编整。
說白了,ChannelPipeline 是負(fù)責(zé)“排隊”的乳丰。這里的“排隊”是處理事件的順序掌测。
同時,ChannelPipeline 也可以添加或者刪除 ChannelHandler产园,管理整個隊列汞斧。
如上圖夜郁,ChannelPipeline 使 ChannelHandler 按照先后順序排列,信息按照箭頭所示方向流動并且被 ChannelHandler 處理粘勒。
說完了 ChannelPipeline 和 ChannelHandler竞端,前者管理后者的排列順序。那么它們之間的關(guān)聯(lián)就由 ChannelHandlerContext 來表示了庙睡。
每當(dāng)有 ChannelHandler 添加到 ChannelPipeline 時事富,同時會創(chuàng)建 ChannelHandlerContext 。
ChannelHandlerContext 的主要功能是管理 ChannelHandler 和 ChannelPipeline 的交互埃撵。
不知道大家注意到?jīng)]有赵颅,開始的例子中 ChannelHandler 中處理事件函數(shù),傳入的參數(shù)就是 ChannelHandlerContext暂刘。
ChannelHandlerContext 參數(shù)貫穿 ChannelPipeline饺谬,將信息傳遞給每個 ChannelHandler,是個合格的“通訊員”谣拣。
ChannelHandlerContext 負(fù)責(zé)傳遞消息
把上面提到的幾個核心組件歸納一下募寨,用下圖表示方便記憶他們之間的關(guān)系。
Netty 核心組件關(guān)系圖
Netty 的數(shù)據(jù)容器
前面介紹了 Netty 的幾個核心組件森缠,服務(wù)器在數(shù)據(jù)傳輸?shù)臅r候拔鹰,產(chǎn)生事件,并且對事件進(jìn)行監(jiān)控和處理贵涵。
接下來看看數(shù)據(jù)是如何存放以及是如何讀寫的列肢。Netty 將 ByteBuf 作為數(shù)據(jù)容器,來存放數(shù)據(jù)宾茂。
ByteBuf 工作原理
從結(jié)構(gòu)上來說瓷马,ByteBuf 由一串字節(jié)數(shù)組構(gòu)成。數(shù)組中每個字節(jié)用來存放信息跨晴。
ByteBuf 提供了兩個索引欧聘,一個用于讀取數(shù)據(jù),一個用于寫入數(shù)據(jù)端盆。這兩個索引通過在字節(jié)數(shù)組中移動怀骤,來定位需要讀或者寫信息的位置。
當(dāng)從 ByteBuf 讀取時焕妙,它的 readerIndex(讀索引)將會根據(jù)讀取的字節(jié)數(shù)遞增蒋伦。
同樣,當(dāng)寫 ByteBuf 時焚鹊,它的 writerIndex 也會根據(jù)寫入的字節(jié)數(shù)進(jìn)行遞增凉敲。
ByteBuf 讀寫索引圖例
需要注意的是極限的情況是 readerIndex 剛好讀到了 writerIndex 寫入的地方。
如果 readerIndex 超過了 writerIndex 的時候,Netty 會拋出 IndexOutOf-BoundsException 異常爷抓。
ByteBuf 使用模式
談了 ByteBuf 的工作原理以后势决,再來看看它的使用模式。
根據(jù)存放緩沖區(qū)的不同分為三類:
- 堆緩沖區(qū)蓝撇,ByteBuf 將數(shù)據(jù)存儲在 JVM 的堆中果复,通過數(shù)組實現(xiàn),可以做到快速分配渤昌。
- 由于在堆上被 JVM 管理虽抄,在不被使用時可以快速釋放《栏蹋可以通過 ByteBuf.array() 來獲取 byte[] 數(shù)據(jù)迈窟。
- 直接緩沖區(qū),在 JVM 的堆之外直接分配內(nèi)存忌栅,用來存儲數(shù)據(jù)车酣。其不占用堆空間,使用時需要考慮內(nèi)存容量索绪。
- 它在使用 Socket 傳遞時性能較好湖员,因為間接從緩沖區(qū)發(fā)送數(shù)據(jù),在發(fā)送之前 JVM 會先將數(shù)據(jù)復(fù)制到直接緩沖區(qū)再進(jìn)行發(fā)送瑞驱。
- 由于娘摔,直接緩沖區(qū)的數(shù)據(jù)分配在堆之外,通過 JVM 進(jìn)行垃圾回收唤反,并且分配時也需要做復(fù)制的操作凳寺,因此使用成本較高。
- 復(fù)合緩沖區(qū)彤侍,顧名思義就是將上述兩類緩沖區(qū)聚合在一起读第。Netty 提供了一個 CompsiteByteBuf,可以將堆緩沖區(qū)和直接緩沖區(qū)的數(shù)據(jù)放在一起拥刻,讓使用更加方便。
ByteBuf 的分配
聊完了結(jié)構(gòu)和使用模式父泳,再來看看 ByteBuf 是如何分配緩沖區(qū)的數(shù)據(jù)的般哼。
Netty 提供了兩種 ByteBufAllocator 的實現(xiàn),他們分別是:
- PooledByteBufAllocator惠窄,實現(xiàn)了 ByteBuf 的對象的池化蒸眠,提高性能減少內(nèi)存碎片。
- Unpooled-ByteBufAllocator杆融,沒有實現(xiàn)對象的池化楞卡,每次會生成新的對象實例。
對象池化的技術(shù)和線程池,比較相似蒋腮,主要目的是提高內(nèi)存的使用率淘捡。池化的簡單實現(xiàn)思路,是在 JVM 堆內(nèi)存上構(gòu)建一層內(nèi)存池池摧,通過 allocate 方法獲取內(nèi)存池中的空間焦除,通過 release 方法將空間歸還給內(nèi)存池。
對象的生成和銷毀作彤,會大量地調(diào)用 allocate 和 release 方法膘魄,因此內(nèi)存池面臨碎片空間回收的問題,在頻繁申請和釋放空間后竭讳,內(nèi)存池需要保證連續(xù)的內(nèi)存空間创葡,用于對象的分配。
基于這個需求绢慢,有兩種算法用于優(yōu)化這一塊的內(nèi)存分配:伙伴系統(tǒng)和 slab 系統(tǒng)灿渴。
伙伴系統(tǒng),用完全二叉樹管理內(nèi)存區(qū)域呐芥,左右節(jié)點互為伙伴逻杖,每個節(jié)點代表一個內(nèi)存塊。內(nèi)存分配將大塊內(nèi)存不斷二分思瘟,直到找到滿足所需的最小內(nèi)存分片荸百。
內(nèi)存釋放會判斷釋放內(nèi)存分片的伙伴(左右節(jié)點)是否空閑,如果空閑則將左右節(jié)點合成更大塊內(nèi)存滨攻。
slab 系統(tǒng)够话,主要解決內(nèi)存碎片問題,將大塊內(nèi)存按照一定內(nèi)存大小進(jìn)行等分光绕,形成相等大小的內(nèi)存片構(gòu)成的內(nèi)存集女嘲。
按照內(nèi)存申請空間的大小,申請盡量小塊內(nèi)存或者其整數(shù)倍的內(nèi)存诞帐,釋放內(nèi)存時欣尼,也是將內(nèi)存分片歸還給內(nèi)存集。
Netty 內(nèi)存池管理以 Allocate 對象的形式出現(xiàn)停蕉。一個 Allocate 對象由多個 Arena 組成愕鼓,每個 Arena 能執(zhí)行內(nèi)存塊的分配和回收。
Arena 內(nèi)有三類內(nèi)存塊管理單元:
- TinySubPage
- SmallSubPage
- ChunkList
Tiny 和 Small 符合 Slab 系統(tǒng)的管理策略慧起,ChunkList 符合伙伴系統(tǒng)的管理策略菇晃。
當(dāng)用戶申請內(nèi)存介于 tinySize 和 smallSize 之間時,從 tinySubPage 中獲取內(nèi)存塊蚓挤。
申請內(nèi)存介于 smallSize 和 pageSize 之間時磺送,從 smallSubPage 中獲取內(nèi)存塊驻子;介于 pageSize 和 chunkSize 之間時,從 ChunkList 中獲取內(nèi)存估灿;大于 ChunkSize(不知道分配內(nèi)存的大谐绾恰)的內(nèi)存塊不通過池化分配。
Netty 的 Bootstrap
說完了 Netty 的核心組件以及數(shù)據(jù)存儲甲捏。再回到最開始的例子程序演熟,在程序最開始的時候會 new 一個 Bootstrap 對象,后面所有的配置都是基于這個對象展開的司顿。
生成 Bootstrap 對象
Bootstrap 的作用就是將 Netty 核心組件配置到程序中芒粹,并且讓他們運行起來。
從 Bootstrap 的繼承結(jié)構(gòu)來看大溜,分為兩類分別是 Bootstrap 和 ServerBootstrap化漆,一個對應(yīng)客戶端的引導(dǎo),另一個對應(yīng)服務(wù)端的引導(dǎo)钦奋。
支持客戶端和服務(wù)端的程序引導(dǎo)
客戶端引導(dǎo) Bootstrap座云,主要有兩個方法 bind() 和 connect()。Bootstrap 通過 bind() 方法創(chuàng)建一個 Channel付材。
在 bind() 之后朦拖,通過調(diào)用 connect() 方法來創(chuàng)建 Channel 連接。
Bootstrap 通過 bind 和 connect 方法創(chuàng)建連接
服務(wù)端引導(dǎo) ServerBootstrap厌衔,與客戶端不同的是在 Bind() 方法之后會創(chuàng)建一個 ServerChannel璧帝,它不僅會創(chuàng)建新的 Channel 還會管理已經(jīng)存在的 Channel。
ServerBootstrap 通過 bind 方法創(chuàng)建/管理連接
通過上面的描述富寿,服務(wù)端和客戶端的引導(dǎo)存在兩個區(qū)別:
- ServerBootstrap(服務(wù)端引導(dǎo))綁定一個端口睬隶,用來監(jiān)聽客戶端的連接請求。而 Bootstrap(客戶端引導(dǎo))只要知道服務(wù)端 IP 和 Port 建立連接就可以了页徐。
- Bootstrap(客戶端引導(dǎo))需要一個 EventLoopGroup苏潜,但是 ServerBootstrap(服務(wù)端引導(dǎo))則需要兩個 EventLoopGroup。
- 因為服務(wù)器需要兩組不同的 Channel变勇。第一組 ServerChannel 自身監(jiān)聽本地端口的套接字恤左。第二組用來監(jiān)聽客戶端請求的套接字。
ServerBootstrap 有兩組 EventLoopGroup
總結(jié)
我們從 NIO 入手飞袋,談到了 Selector 的核心機(jī)制物咳。然后通過介紹 Netty 客戶端和服務(wù)端源代碼運行流程,讓大家對 Netty 編寫代碼有基本的認(rèn)識巷折。
在 Netty 的核心組件中锻拘,Channel 提供 Socket 的連接通道,EventLoop 會對應(yīng) Channel 監(jiān)聽其產(chǎn)生的事件推穷,并且通知執(zhí)行者馒铃。EventloopGroup 的容器痕惋,負(fù)責(zé)生成和管理 EventLoop议谷。
ChannelPipeline 作為 ChannelHandler 的容器會綁定到 Channel 上柿隙,然后由 ChannelHandler 提供具體事件處理禀崖。另外波附,ChannelHandlerContext 為 ChannelHandler 和 ChannelPipeline 提供信息共享掸屡。
ByteBuf 作為 Netty 的數(shù)據(jù)容器,通過字節(jié)數(shù)組的方式存儲數(shù)據(jù)盏求,并且通過讀索引和寫索引來引導(dǎo)讀寫操作碎罚。
上述的核心組件都是通過 Bootstrap 來配置并且引導(dǎo)啟動的拯勉,Bootstrap 啟動方式雖然一致宫峦,但是針對客戶端和服務(wù)端有些許的區(qū)別斗遏。
本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布诵次!