Netty面試題(2021 最新版)

1.Netty 是什么珠月?

Netty是 一個異步事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用程序框架锣杂,用于快速開發(fā)可維護的高性能協(xié)議服務(wù)器和客戶端。Netty是基于nio的忍坷,它封裝了jdk的nio蛹锰,讓我們使用起來更加方法靈活深胳。

2.Netty 的特點是什么绰疤?

  • 高并發(fā):Netty 是一款基于 NIO(Nonblocking IO铜犬,非阻塞IO)開發(fā)的網(wǎng)絡(luò)通信框架,對比于 BIO(Blocking I/O轻庆,阻塞IO)癣猾,他的并發(fā)性能得到了很大提高。
  • 傳輸快:Netty 的傳輸依賴于零拷貝特性余爆,盡量減少不必要的內(nèi)存拷貝纷宇,實現(xiàn)了更高效率的傳輸。
  • 封裝好:Netty 封裝了 NIO 操作的很多細節(jié)蛾方,提供了易于使用調(diào)用接口像捶。

3.Netty 的優(yōu)勢有哪些?

  • 使用簡單:封裝了 NIO 的很多細節(jié)桩砰,使用更簡單拓春。
  • 功能強大:預(yù)置了多種編解碼功能,支持多種主流協(xié)議亚隅。
  • 定制能力強:可以通過 ChannelHandler 對通信框架進行靈活地擴展硼莽。
  • 性能高:通過與其他業(yè)界主流的 NIO 框架對比,Netty 的綜合性能最優(yōu)煮纵。
  • 穩(wěn)定:Netty 修復(fù)了已經(jīng)發(fā)現(xiàn)的所有 NIO 的 bug懂鸵,讓開發(fā)人員可以專注于業(yè)務(wù)本身。
  • 社區(qū)活躍:Netty 是活躍的開源項目行疏,版本迭代周期短匆光,bug 修復(fù)速度快。

4.Netty 的應(yīng)用場景有哪些酿联?

典型的應(yīng)用有:阿里分布式服務(wù)框架 Dubbo殴穴,默認使用 Netty 作為基礎(chǔ)通信組件,還有 RocketMQ 也是使用 Netty 作為通訊的基礎(chǔ)。

5.Netty 高性能表現(xiàn)在哪些方面采幌?

  • IO 線程模型:同步非阻塞劲够,用最少的資源做更多的事。
  • 內(nèi)存零拷貝:盡量減少不必要的內(nèi)存拷貝休傍,實現(xiàn)了更高效率的傳輸征绎。
  • 內(nèi)存池設(shè)計:申請的內(nèi)存可以重用,主要指直接內(nèi)存磨取。內(nèi)部實現(xiàn)是用一顆二叉查找樹管理內(nèi)存分配情況人柿。
  • 串形化處理讀寫:避免使用鎖帶來的性能開銷。
  • 高性能序列化協(xié)議:支持 protobuf 等高性能序列化協(xié)議忙厌。

6.BIO凫岖、NIO和AIO的區(qū)別?

BIO:一個連接一個線程逢净,客戶端有連接請求時服務(wù)器端就需要啟動一個線程進行處理哥放。線程開銷大。 偽異步IO:將請求連接放入線程池爹土,一對多甥雕,但線程還是很寶貴的資源。

NIO:一個請求一個線程胀茵,但客戶端發(fā)送的連接請求都會注冊到多路復(fù)用器上社露,多路復(fù)用器輪詢到連接有I/O請求時才啟動一個線程進行處理。

AIO:一個有效請求一個線程琼娘,客戶端的I/O請求都是由OS先完成了再通知服務(wù)器應(yīng)用去啟動線程進行處理峭弟,

BIO是面向流的,NIO是面向緩沖區(qū)的脱拼;BIO的各種流是阻塞的瞒瘸。而NIO是非阻塞的;BIO的Stream是單向的挪拟,而NIO的channel是雙向的挨务。

NIO的特點:事件驅(qū)動模型、單線程處理多任務(wù)玉组、非阻塞I/O谎柄,I/O讀寫不再阻塞,而是返回0惯雳、基于block的傳輸比基于流的傳輸更高效朝巫、更高級的IO函數(shù)zero-copy、IO多路復(fù)用大大提高了Java網(wǎng)絡(luò)應(yīng)用的可伸縮性和實用性石景∨常基于Reactor線程模型拙吉。

在Reactor模式中,事件分發(fā)器等待某個事件或者可應(yīng)用或個操作的狀態(tài)發(fā)生揪荣,事件分發(fā)器就把這個事件傳給事先注冊的事件處理函數(shù)或者回調(diào)函數(shù)筷黔,由后者來做實際的讀寫操作。如在Reactor中實現(xiàn)讀:注冊讀就緒事件和相應(yīng)的事件處理器仗颈、事件分發(fā)器等待事件佛舱、事件到來,激活分發(fā)器挨决,分發(fā)器調(diào)用事件對應(yīng)的處理器请祖、事件處理器完成實際的讀操作,處理讀到的數(shù)據(jù)脖祈,注冊新的事件肆捕,然后返還控制權(quán)。

7.NIO的組成盖高?

Buffer:與Channel進行交互慎陵,數(shù)據(jù)是從Channel讀入緩沖區(qū),從緩沖區(qū)寫入Channel中的

flip方法 : 反轉(zhuǎn)此緩沖區(qū)或舞,將position給limit荆姆,然后將position置為0蒙幻,其實就是切換讀寫模式

clear方法 :清除此緩沖區(qū)映凳,將position置為0,把capacity的值給limit邮破。

rewind方法 : 重繞此緩沖區(qū)诈豌,將position置為0

DirectByteBuffer可減少一次系統(tǒng)空間到用戶空間的拷貝。但Buffer創(chuàng)建和銷毀的成本更高抒和,不可控矫渔,通常會用內(nèi)存池來提高性能。直接緩沖區(qū)主要分配給那些易受基礎(chǔ)系統(tǒng)的本機I/O 操作影響的大型摧莽、持久的緩沖區(qū)庙洼。如果數(shù)據(jù)量比較小的中小應(yīng)用情況下,可以考慮使用heapBuffer镊辕,由JVM進行管理油够。

Channel:表示 IO 源與目標打開的連接,是雙向的征懈,但不能直接訪問數(shù)據(jù)石咬,只能與Buffer 進行交互。通過源碼可知卖哎,F(xiàn)ileChannel的read方法和write方法都導(dǎo)致數(shù)據(jù)復(fù)制了兩次鬼悠!

Selector可使一個單獨的線程管理多個Channel删性,open方法可創(chuàng)建Selector,register方法向多路復(fù)用器器注冊通道焕窝,可以監(jiān)聽的事件類型:讀蹬挺、寫、連接它掂、accept汗侵。注冊事件后會產(chǎn)生一個SelectionKey:它表示SelectableChannel 和Selector 之間的注冊關(guān)系,wakeup方法:使尚未返回的第一個選擇操作立即返回群发,喚醒的

原因是:注冊了新的channel或者事件晰韵;channel關(guān)閉,取消注冊熟妓;優(yōu)先級更高的事件觸發(fā)(如定時器事件)雪猪,希望及時處理。

Selector在Linux的實現(xiàn)類是EPollSelectorImpl起愈,委托給EPollArrayWrapper實現(xiàn)只恨,其中三個native方法是對epoll的封裝,而EPollSelectorImpl. implRegister方法抬虽,通過調(diào)用epoll_ctl向epoll實例中注冊事件官觅,還將注冊的文件描述符(fd)與SelectionKey的對應(yīng)關(guān)系添加到fdToKey中,這個map維護了文件描述符與SelectionKey的映射阐污。

fdToKey有時會變得非常大休涤,因為注冊到Selector上的Channel非常多(百萬連接);過期或失效的Channel沒有及時關(guān)閉笛辟。fdToKey總是串行讀取的功氨,而讀取是在select方法中進行的,該方法是非線程安全的手幢。

Pipe:兩個線程之間的單向數(shù)據(jù)連接捷凄,數(shù)據(jù)會被寫到sink通道,從source通道讀取

NIO的服務(wù)端建立過程:Selector.open():打開一個Selector围来;ServerSocketChannel.open():創(chuàng)建服務(wù)端的Channel跺涤;bind():綁定到某個端口上。并配置非阻塞模式监透;register():注冊Channel和關(guān)注的事件到Selector上桶错;select()輪詢拿到已經(jīng)就緒的事件

8.Netty的線程模型?

Netty通過Reactor模型基于多路復(fù)用器接收并處理用戶請求才漆,內(nèi)部實現(xiàn)了兩個線程池牛曹,boss線程池和work線程池,其中boss線程池的線程負責處理請求的accept事件醇滥,當接收到accept事件的請求時黎比,把對應(yīng)的socket封裝到一個NioSocketChannel中超营,并交給work線程池,其中work線程池負責請求的read和write事件阅虫,由對應(yīng)的Handler處理演闭。

單線程模型:所有I/O操作都由一個線程完成,即多路復(fù)用颓帝、事件分發(fā)和處理都是在一個Reactor線程上完成的米碰。既要接收客戶端的連接請求,向服務(wù)端發(fā)起連接,又要發(fā)送/讀取請求或應(yīng)答/響應(yīng)消息购城。一個NIO 線程同時處理成百上千的鏈路吕座,性能上無法支撐,速度慢瘪板,若線程進入死循環(huán)吴趴,整個程序不可用,對于高負載侮攀、大并發(fā)的應(yīng)用場景不合適锣枝。

多線程模型:有一個NIO 線程(Acceptor) 只負責監(jiān)聽服務(wù)端,接收客戶端的TCP 連接請求兰英;NIO 線程池負責網(wǎng)絡(luò)IO 的操作撇叁,即消息的讀取、解碼畦贸、編碼和發(fā)送陨闹;1 個NIO 線程可以同時處理N 條鏈路,但是1 個鏈路只對應(yīng)1 個NIO 線程家制,這是為了防止發(fā)生并發(fā)操作問題正林。但在并發(fā)百萬客戶端連接或需要安全認證時泡一,一個Acceptor 線程可能會存在性能不足問題颤殴。

主從多線程模型:Acceptor 線程用于綁定監(jiān)聽端口,接收客戶端連接鼻忠,將SocketChannel 從主線程池的Reactor 線程的多路復(fù)用器上移除涵但,重新注冊到Sub 線程池的線程上,用于處理I/O 的讀寫等操作帖蔓,從而保證mainReactor只負責接入認證矮瘟、握手等操作;

9.TCP 粘包/拆包的原因及解決方法塑娇?

TCP是以流的方式來處理數(shù)據(jù)澈侠,一個完整的包可能會被TCP拆分成多個包進行發(fā)送,也可能把小的封裝成一個大的數(shù)據(jù)包發(fā)送埋酬。

TCP粘包/分包的原因:

應(yīng)用程序?qū)懭氲淖止?jié)大小大于套接字發(fā)送緩沖區(qū)的大小哨啃,會發(fā)生拆包現(xiàn)象烧栋,而應(yīng)用程序?qū)懭霐?shù)據(jù)小于套接字緩沖區(qū)大小,網(wǎng)卡將應(yīng)用多次寫入的數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)上拳球,這將會發(fā)生粘包現(xiàn)象审姓;

進行MSS大小的TCP分段,當TCP報文長度-TCP頭部長度>MSS的時候?qū)l(fā)生拆包 以太網(wǎng)幀的payload(凈荷)大于MTU(1500字節(jié))進行ip分片祝峻。

解決方法

消息定長:FixedLengthFrameDecoder類

包尾增加特殊字符分割:

  • 行分隔符類:LineBasedFrameDecoder
  • 或自定義分隔符類 :DelimiterBasedFrameDecoder

將消息分為消息頭和消息體:LengthFieldBasedFrameDecoder類魔吐。分為有頭部的拆包與粘包、長度字段在前且有頭部的拆包與粘包莱找、多擴展頭部的拆包與粘包酬姆。

10.什么是 Netty 的零拷貝?

Netty 的零拷貝主要包含三個方面:

  • Netty 的接收和發(fā)送 ByteBuffer 采用 DIRECT BUFFERS奥溺,使用堆外直接內(nèi)存進行 Socket 讀寫轴踱,不需要進行字節(jié)緩沖區(qū)的二次拷貝。如果使用傳統(tǒng)的堆內(nèi)存(HEAP BUFFERS)進行 Socket 讀寫谚赎,JVM 會將堆內(nèi)存 Buffer 拷貝一份到直接內(nèi)存中淫僻,然后才寫入 Socket 中。相比于堆外直接內(nèi)存壶唤,消息在發(fā)送過程中多了一次緩沖區(qū)的內(nèi)存拷貝雳灵。
  • Netty 提供了組合 Buffer 對象,可以聚合多個 ByteBuffer 對象闸盔,用戶可以像操作一個 Buffer 那樣方便的對組合 Buffer 進行操作悯辙,避免了傳統(tǒng)通過內(nèi)存拷貝的方式將幾個小 Buffer 合并成一個大的 Buffer。
  • Netty 的文件傳輸采用了 transferTo 方法迎吵,它可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標 Channel躲撰,避免了傳統(tǒng)通過循環(huán) write 方式導(dǎo)致的內(nèi)存拷貝問題。

11.Netty 中有哪種重要組件击费?

  • Channel:Netty 網(wǎng)絡(luò)操作抽象類拢蛋,它除了包括基本的 I/O 操作,如 bind蔫巩、connect谆棱、read、write 等圆仔。
  • EventLoop:主要是配合 Channel 處理 I/O 操作垃瞧,用來處理連接的生命周期中所發(fā)生的事情。
  • ChannelFuture:Netty 框架中所有的 I/O 操作都為異步的坪郭,因此我們需要 ChannelFuture 的 addListener()注冊一個 ChannelFutureListener 監(jiān)聽事件个从,當操作執(zhí)行成功或者失敗時,監(jiān)聽就會自動觸發(fā)返回結(jié)果。
  • ChannelHandler:充當了所有處理入站和出站數(shù)據(jù)的邏輯容器嗦锐。ChannelHandler 主要用來處理各種事件鸵隧,這里的事件很廣泛,比如可以是連接意推、數(shù)據(jù)接收豆瘫、異常、數(shù)據(jù)轉(zhuǎn)換等菊值。
  • ChannelPipeline:為 ChannelHandler 鏈提供了容器外驱,當 channel 創(chuàng)建時,就會被自動分配到它專屬的 ChannelPipeline腻窒,這個關(guān)聯(lián)是永久性的昵宇。

12.Netty 發(fā)送消息有幾種方式?

Netty 有兩種發(fā)送消息的方式:

  • 直接寫入 Channel 中儿子,消息從 ChannelPipeline 當中尾部開始移動瓦哎;
  • 寫入和 ChannelHandler 綁定的 ChannelHandlerContext 中,消息從 ChannelPipeline 中的下一個 ChannelHandler 中移動柔逼。

13.默認情況 Netty 起多少線程蒋譬?何時啟動?

Netty 默認是 CPU 處理器數(shù)的兩倍愉适,bind 完之后啟動犯助。

14.了解哪幾種序列化協(xié)議?

序列化(編碼)是將對象序列化為二進制形式(字節(jié)數(shù)組)维咸,主要用于網(wǎng)絡(luò)傳輸剂买、數(shù)據(jù)持久化等;而反序列化(解碼)則是將從網(wǎng)絡(luò)癌蓖、磁盤等讀取的字節(jié)數(shù)組還原成原始對象瞬哼,主要用于網(wǎng)絡(luò)傳輸對象的解碼,以便完成遠程調(diào)用租副。

影響序列化性能的關(guān)鍵因素:序列化后的碼流大凶俊(網(wǎng)絡(luò)帶寬的占用)、序列化的性能(CPU資源占用)附井;是否支持跨語言(異構(gòu)系統(tǒng)的對接和開發(fā)語言切換)讨越。

Java默認提供的序列化:無法跨語言、序列化后的碼流太大永毅、序列化的性能差

XML,優(yōu)點:人機可讀性好人弓,可指定元素或特性的名稱沼死。缺點:序列化數(shù)據(jù)只包含數(shù)據(jù)本身以及類的結(jié)構(gòu),不包括類型標識和程序集信息崔赌;只能序列化公共屬性和字段意蛀;不能序列化方法耸别;文件龐大,文件格式復(fù)雜县钥,傳輸占帶寬秀姐。適用場景:當做配置文件存儲數(shù)據(jù),實時數(shù)據(jù)轉(zhuǎn)換若贮。

JSON省有,是一種輕量級的數(shù)據(jù)交換格式,優(yōu)點:兼容性高谴麦、數(shù)據(jù)格式比較簡單蠢沿,易于讀寫、序列化后數(shù)據(jù)較小匾效,可擴展性好舷蟀,兼容性好、與XML相比面哼,其協(xié)議比較簡單野宜,解析速度比較快。缺點:數(shù)據(jù)的描述性比XML差魔策、不適合性能要求為ms級別的情況速缨、額外空間開銷比較大。適用場景(可替代XML):跨防火墻訪問代乃、可調(diào)式性要求高旬牲、基于Web browser的Ajax請求、傳輸數(shù)據(jù)量相對小搁吓,實時性要求相對低(例如秒級別)的服務(wù)原茅。

Fastjson,采用一種“假定有序快速匹配”的算法堕仔。優(yōu)點:接口簡單易用擂橘、目前java語言中最快的json庫。缺點:過于注重快摩骨,而偏離了“標準”及功能性通贞、代碼質(zhì)量不高,文檔不全恼五。適用場景:協(xié)議交互昌罩、Web輸出、Android客戶端

Thrift灾馒,不僅是序列化協(xié)議茎用,還是一個RPC框架。優(yōu)點:序列化后的體積小, 速度快、支持多種語言和豐富的數(shù)據(jù)類型轨功、對于數(shù)據(jù)字段的增刪具有較強的兼容性旭斥、支持二進制壓縮編碼。缺點:使用者較少古涧、跨防火墻訪問時垂券,不安全、不具有可讀性羡滑,調(diào)試代碼時相對困難菇爪、不能與其他傳輸層協(xié)議共同使用(例如HTTP)、無法支持向持久層直接讀寫數(shù)據(jù)啄栓,即不適合做數(shù)據(jù)持久化序列化協(xié)議娄帖。適用場景:分布式系統(tǒng)的RPC解決方案

Avro尿褪,Hadoop的一個子項目舔株,解決了JSON的冗長和沒有IDL的問題缀蹄。優(yōu)點:支持豐富的數(shù)據(jù)類型允粤、簡單的動態(tài)語言結(jié)合功能孽拷、具有自我描述屬性音五、提高了數(shù)據(jù)解析速度料滥、快速可壓縮的二進制數(shù)據(jù)形式徘跪、可以實現(xiàn)遠程過程調(diào)用RPC淳梦、支持跨編程語言實現(xiàn)析砸。缺點:對于習慣于靜態(tài)類型語言的用戶不直觀。適用場景:在Hadoop中做Hive爆袍、Pig和MapReduce的持久化數(shù)據(jù)格式首繁。

Protobuf,將數(shù)據(jù)結(jié)構(gòu)以.proto文件進行描述陨囊,通過代碼生成工具可以生成對應(yīng)數(shù)據(jù)結(jié)構(gòu)的POJO對象和Protobuf相關(guān)的方法和屬性弦疮。優(yōu)點:序列化后碼流小,性能高蜘醋、結(jié)構(gòu)化數(shù)據(jù)存儲格式(XML JSON等)胁塞、通過標識字段的順序,可以實現(xiàn)協(xié)議的前向兼容压语、結(jié)構(gòu)化的文檔更容易管理和維護啸罢。缺點:需要依賴于工具生成代碼、支持的語言相對較少胎食,官方只支持Java 扰才、C++ 、python斥季。適用場景:對性能要求高的RPC調(diào)用训桶、具有良好的跨防火墻的訪問屬性累驮、適合應(yīng)用層對象的持久化

其它

protostuff 基于protobuf協(xié)議酣倾,但不需要配置proto文件舵揭,直接導(dǎo)包即可 Jboss marshaling 可以直接序列化java類, 無須實java.io.Serializable接口 Message pack 一個高效的二進制序列化格式 Hessian 采用二進制協(xié)議的輕量級remoting onhttp工具 kryo 基于protobuf協(xié)議躁锡,只支持java語言,需要注冊(Registration)午绳,然后序列化(Output),反序列化(Input)

15.如何選擇序列化協(xié)議映之?

具體場景

對于公司間的系統(tǒng)調(diào)用拦焚,如果性能要求在100ms以上的服務(wù),基于XML的SOAP協(xié)議是一個值得考慮的方案杠输。 基于Web browser的Ajax赎败,以及Mobile app與服務(wù)端之間的通訊,JSON協(xié)議是首選蠢甲。對于性能要求不太高僵刮,或者以動態(tài)類型語言為主,或者傳輸數(shù)據(jù)載荷很小的的運用場景鹦牛,JSON也是非常不錯的選擇搞糕。 對于調(diào)試環(huán)境比較惡劣的場景,采用JSON或XML能夠極大的提高調(diào)試效率曼追,降低系統(tǒng)開發(fā)成本窍仰。 當對性能和簡潔性有極高要求的場景,Protobuf礼殊,Thrift驹吮,Avro之間具有一定的競爭關(guān)系。 對于T級別的數(shù)據(jù)的持久化應(yīng)用場景晶伦,Protobuf和Avro是首要選擇碟狞。如果持久化后的數(shù)據(jù)存儲在hadoop子項目里,Avro會是更好的選擇坝辫。

對于持久層非Hadoop項目篷就,以靜態(tài)類型語言為主的應(yīng)用場景,Protobuf會更符合靜態(tài)類型語言工程師的開發(fā)習慣近忙。由于Avro的設(shè)計理念偏向于動態(tài)類型語言竭业,對于動態(tài)語言為主的應(yīng)用場景,Avro是更好的選擇及舍。 如果需要提供一個完整的RPC解決方案未辆,Thrift是一個好的選擇。 如果序列化之后需要支持不同的傳輸層協(xié)議锯玛,或者需要跨防火墻訪問的高性能場景咐柜,Protobuf可以優(yōu)先考慮兼蜈。 protobuf的數(shù)據(jù)類型有多種:bool、double拙友、float为狸、int32、int64遗契、string辐棒、bytes、enum牍蜂、message漾根。protobuf的限定符:required: 必須賦值,不能為空鲫竞、optional:字段可以賦值辐怕,也可以不賦值、repeated: 該字段可以重復(fù)任意次數(shù)(包括0次)从绘、枚舉寄疏;只能用指定的常量集中的一個值作為其值;

protobuf的基本規(guī)則:每個消息中必須至少留有一個required類型的字段顶考、包含0個或多個optional類型的字段赁还;repeated表示的字段可以包含0個或多個數(shù)據(jù);[1,15]之內(nèi)的標識號在編碼的時候會占用一個字節(jié)(常用)驹沿,[16,2047]之內(nèi)的標識號則占用2個字節(jié)艘策,標識號一定不能重復(fù)、使用消息類型渊季,也可以將消息嵌套任意多層朋蔫,可用嵌套消息類型來代替組。

protobuf的消息升級原則:不要更改任何已有的字段的數(shù)值標識却汉;不能移除已經(jīng)存在的required字段驯妄,optional和repeated類型的字段可以被移除,但要保留標號不能被重用合砂。新添加的字段必須是optional或repeated青扔。因為舊版本程序無法讀取或?qū)懭胄略龅膔equired限定符的字段。

編譯器為每一個消息類型生成了一個.java文件翩伪,以及一個特殊的Builder類(該類是用來創(chuàng)建消息類接口的)微猖。如:UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();

Netty中的使用:ProtobufVarint32FrameDecoder 是用于處理半包消息的解碼類缘屹;ProtobufDecoder(UserProto.User.getDefaultInstance())這是創(chuàng)建的UserProto.java文件中的解碼類凛剥;ProtobufVarint32LengthFieldPrepender 對protobuf協(xié)議的消息頭上加上一個長度為32的整形字段,用于標志這個消息的長度的類轻姿;ProtobufEncoder 是編碼類

將StringBuilder轉(zhuǎn)換為ByteBuf類型:copiedBuffer()方法

16.Netty 支持哪些心跳類型設(shè)置犁珠?

  • readerIdleTime:為讀超時時間(即測試端一定時間內(nèi)未接受到被測試端消息)逻炊。
  • writerIdleTime:為寫超時時間(即測試端一定時間內(nèi)向被測試端發(fā)送消息)。
  • allIdleTime:所有類型的超時時間犁享。

17.Netty 和 Tomcat 的區(qū)別余素?

  • 作用不同:Tomcat 是 Servlet 容器,可以視為 Web 服務(wù)器饼疙,而 Netty 是異步事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用程序框架和工具用于簡化網(wǎng)絡(luò)編程溺森,例如TCP和UDP套接字服務(wù)器慕爬。
  • 協(xié)議不同:Tomcat 是基于 http 協(xié)議的 Web 服務(wù)器窑眯,而 Netty 能通過編程自定義各種協(xié)議,因為 Netty 本身自己能編碼/解碼字節(jié)流医窿,所有 Netty 可以實現(xiàn)磅甩,HTTP 服務(wù)器、FTP 服務(wù)器姥卢、UDP 服務(wù)器卷要、RPC 服務(wù)器、WebSocket 服務(wù)器独榴、Redis 的 Proxy 服務(wù)器僧叉、MySQL 的 Proxy 服務(wù)器等等。

18.NIOEventLoopGroup源碼棺榔?

NioEventLoopGroup(其實是MultithreadEventExecutorGroup) 內(nèi)部維護一個類型為 EventExecutor children [], 默認大小是處理器核數(shù) * 2, 這樣就構(gòu)成了一個線程池瓶堕,初始化EventExecutor時NioEventLoopGroup重載newChild方法,所以children元素的實際類型為NioEventLoop症歇。

線程啟動時調(diào)用SingleThreadEventExecutor的構(gòu)造方法郎笆,執(zhí)行NioEventLoop類的run方法,首先會調(diào)用hasTasks()方法判斷當前taskQueue是否有元素忘晤。如果taskQueue中有元素宛蚓,執(zhí)行 selectNow() 方法,最終執(zhí)行selector.selectNow()设塔,該方法會立即返回凄吏。如果taskQueue沒有元素,執(zhí)行 select(oldWakenUp) 方法

select ( oldWakenUp) 方法解決了 Nio 中的 bug闰蛔,selectCnt 用來記錄selector.select方法的執(zhí)行次數(shù)和標識是否執(zhí)行過selector.selectNow()痕钢,若觸發(fā)了epoll的空輪詢bug,則會反復(fù)執(zhí)行selector.select(timeoutMillis)钞护,變量selectCnt 會逐漸變大盖喷,當selectCnt 達到閾值(默認512),則執(zhí)行rebuildSelector方法难咕,進行selector重建课梳,解決cpu占用100%的bug距辆。

rebuildSelector方法先通過openSelector方法創(chuàng)建一個新的selector。然后將old selector的selectionKey執(zhí)行cancel暮刃。最后將old selector的channel重新注冊到新的selector中跨算。rebuild后,需要重新執(zhí)行方法selectNow椭懊,檢查是否有已ready的selectionKey诸蚕。

接下來調(diào)用processSelectedKeys 方法(處理I/O任務(wù)),當selectedKeys != null時氧猬,調(diào)用processSelectedKeysOptimized方法背犯,迭代 selectedKeys 獲取就緒的 IO 事件的selectkey存放在數(shù)組selectedKeys中, 然后為每個事件都調(diào)用 processSelectedKey 來處理它,processSelectedKey 中分別處理OP_READ盅抚;OP_WRITE漠魏;OP_CONNECT事件。

最后調(diào)用runAllTasks方法(非IO任務(wù))妄均,該方法首先會調(diào)用fetchFromScheduledTaskQueue方法柱锹,把scheduledTaskQueue中已經(jīng)超過延遲執(zhí)行時間的任務(wù)移到taskQueue中等待被執(zhí)行,然后依次從taskQueue中取任務(wù)執(zhí)行丰包,每執(zhí)行64個任務(wù)禁熏,進行耗時檢查,如果已執(zhí)行時間超過預(yù)先設(shè)定的執(zhí)行時間邑彪,則停止執(zhí)行非IO任務(wù)瞧毙,避免非IO任務(wù)太多,影響IO任務(wù)的執(zhí)行锌蓄。

每個NioEventLoop對應(yīng)一個線程和一個Selector升筏,NioServerSocketChannel會主動注冊到某一個NioEventLoop的Selector上,NioEventLoop負責事件輪詢瘸爽。

Outbound 事件都是請求事件, 發(fā)起者是 Channel您访,處理者是 unsafe,通過 Outbound 事件進行通知灵汪,傳播方向是 tail到head。Inbound 事件發(fā)起者是 unsafe柑潦,事件的處理者是 Channel, 是通知事件享言,傳播方向是從頭到尾。

內(nèi)存管理機制渗鬼,首先會預(yù)申請一大塊內(nèi)存Arena览露,Arena由許多Chunk組成,而每個Chunk默認由2048個page組成譬胎。Chunk通過AVL樹的形式組織Page差牛,每個葉子節(jié)點表示一個Page命锄,而中間節(jié)點表示內(nèi)存區(qū)域,節(jié)點自己記錄它在整個Arena中的偏移地址偏化。當區(qū)域被分配出去后脐恩,中間節(jié)點上的標記位會被標記,這樣就表示這個中間節(jié)點以下的所有節(jié)點都已被分配了侦讨。大于8k的內(nèi)存分配在poolChunkList中驶冒,而PoolSubpage用于分配小于8k的內(nèi)存,它會把一個page分割成多段韵卤,進行內(nèi)存分配骗污。

ByteBuf的特點:支持自動擴容(4M),保證put方法不會拋出異常怜俐、通過內(nèi)置的復(fù)合緩沖類型身堡,實現(xiàn)零拷貝(zero-copy);不需要調(diào)用flip()來切換讀/寫模式拍鲤,讀取和寫入索引分開;方法鏈汞扎;引用計數(shù)基于AtomicIntegerFieldUpdater用于內(nèi)存回收季稳;PooledByteBuf采用二叉樹來實現(xiàn)一個內(nèi)存池,集中管理內(nèi)存的分配和釋放澈魄,不用每次使用都新建一個緩沖區(qū)對象景鼠。UnpooledHeapByteBuf每次都會新建一個緩沖區(qū)對象。

Netty簡介

Netty是 一個異步事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用程序框架痹扇,用于快速開發(fā)可維護的高性能協(xié)議服務(wù)器和客戶端铛漓。

JDK原生NIO程序的問題

JDK原生也有一套網(wǎng)絡(luò)應(yīng)用程序API,但是存在一系列問題鲫构,主要如下:

  • NIO的類庫和API繁雜浓恶,使用麻煩,你需要熟練掌握Selector结笨、ServerSocketChannel包晰、SocketChannel、ByteBuffer等
  • 需要具備其它的額外技能做鋪墊炕吸,例如熟悉Java多線程編程伐憾,因為NIO編程涉及到Reactor模式,你必須對多線程和網(wǎng)路編程非常熟悉赫模,才能編寫出高質(zhì)量的NIO程序
  • 可靠性能力補齊树肃,開發(fā)工作量和難度都非常大。例如客戶端面臨斷連重連瀑罗、網(wǎng)絡(luò)閃斷胸嘴、半包讀寫莉钙、失敗緩存、網(wǎng)絡(luò)擁塞和異常碼流的處理等等筛谚,NIO編程的特點是功能開發(fā)相對容易磁玉,但是可靠性能力補齊工作量和難度都非常大
  • JDK NIO的BUG,例如臭名昭著的epoll bug驾讲,它會導(dǎo)致Selector空輪詢蚊伞,最終導(dǎo)致CPU 100%。官方聲稱在JDK1.6版本的update18修復(fù)了該問題吮铭,但是直到JDK1.7版本該問題仍舊存在时迫,只不過該bug發(fā)生概率降低了一些而已,它并沒有被根本解決

Netty的特點

Netty的對JDK自帶的NIO的API進行封裝谓晌,解決上述問題掠拳,主要特點有:

  • 設(shè)計優(yōu)雅 適用于各種傳輸類型的統(tǒng)一API - 阻塞和非阻塞Socket 基于靈活且可擴展的事件模型,可以清晰地分離關(guān)注點 高度可定制的線程模型 - 單線程纸肉,一個或多個線程池 真正的無連接數(shù)據(jù)報套接字支持(自3.1起)
  • 使用方便 詳細記錄的Javadoc溺欧,用戶指南和示例 沒有其他依賴項,JDK 5(Netty 3.x)或6(Netty 4.x)就足夠了
  • 高性能 吞吐量更高柏肪,延遲更低 減少資源消耗 最小化不必要的內(nèi)存復(fù)制
  • 安全 完整的SSL / TLS和StartTLS支持
  • 社區(qū)活躍姐刁,不斷更新 社區(qū)活躍,版本迭代周期短烦味,發(fā)現(xiàn)的BUG可以被及時修復(fù)聂使,同時,更多的新功能會被加入

Netty常見使用場景

Netty常見的使用場景如下:

  • 互聯(lián)網(wǎng)行業(yè) 在分布式系統(tǒng)中谬俄,各個節(jié)點之間需要遠程服務(wù)調(diào)用柏靶,高性能的RPC框架必不可少,Netty作為異步高新能的通信框架,往往作為基礎(chǔ)通信組件被這些RPC框架使用溃论。 典型的應(yīng)用有:阿里分布式服務(wù)框架Dubbo的RPC框架使用Dubbo協(xié)議進行節(jié)點間通信屎蜓,Dubbo協(xié)議默認使用Netty作為基礎(chǔ)通信組件,用于實現(xiàn)各進程節(jié)點之間的內(nèi)部通信蔬芥。
  • 游戲行業(yè) 無論是手游服務(wù)端還是大型的網(wǎng)絡(luò)游戲梆靖,Java語言得到了越來越廣泛的應(yīng)用。Netty作為高性能的基礎(chǔ)通信組件笔诵,它本身提供了TCP/UDP和HTTP協(xié)議棧返吻。 非常方便定制和開發(fā)私有協(xié)議棧,賬號登錄服務(wù)器乎婿,地圖服務(wù)器之間可以方便的通過Netty進行高性能的通信
  • 大數(shù)據(jù)領(lǐng)域 經(jīng)典的Hadoop的高性能通信和序列化組件Avro的RPC框架测僵,默認采用Netty進行跨界點通信,它的Netty Service基于Netty框架二次封裝實現(xiàn)

有興趣的讀者可以了解一下目前有哪些開源項目使用了 Netty:Related projects

Netty高性能設(shè)計

Netty作為異步事件驅(qū)動的網(wǎng)絡(luò),高性能之處主要來自于其I/O模型和線程處理模型捍靠,前者決定如何收發(fā)數(shù)據(jù)沐旨,后者決定如何處理數(shù)據(jù)

I/O模型

用什么樣的通道將數(shù)據(jù)發(fā)送給對方,BIO榨婆、NIO或者AIO磁携,I/O模型在很大程度上決定了框架的性能

阻塞I/O

傳統(tǒng)阻塞型I/O(BIO)可以用下圖表示:

特點

  • 每個請求都需要獨立的線程完成數(shù)據(jù)read,業(yè)務(wù)處理良风,數(shù)據(jù)write的完整操作

問題

  • 當并發(fā)數(shù)較大時谊迄,需要創(chuàng)建大量線程來處理連接,系統(tǒng)資源占用較大
  • 連接建立后烟央,如果當前線程暫時沒有數(shù)據(jù)可讀统诺,則線程就阻塞在read操作上,造成線程資源浪費

I/O復(fù)用模型

在I/O復(fù)用模型中疑俭,會用到select粮呢,這個函數(shù)也會使進程阻塞,但是和阻塞I/O所不同的的钞艇,這兩個函數(shù)可以同時阻塞多個I/O操作啄寡,而且可以同時對多個讀操作,多個寫操作的I/O函數(shù)進行檢測香璃,直到有數(shù)據(jù)可讀或可寫時这难,才真正調(diào)用I/O操作函數(shù)

Netty的非阻塞I/O的實現(xiàn)關(guān)鍵是基于I/O復(fù)用模型,這里用Selector對象表示:

Netty的IO線程NioEventLoop由于聚合了多路復(fù)用器Selector葡秒,可以同時并發(fā)處理成百上千個客戶端連接。當線程從某客戶端Socket通道進行讀寫數(shù)據(jù)時嵌溢,若沒有數(shù)據(jù)可用時眯牧,該線程可以進行其他任務(wù)。線程通常將非阻塞 IO 的空閑時間用于在其他通道上執(zhí)行 IO 操作赖草,所以單獨的線程可以管理多個輸入和輸出通道学少。

由于讀寫操作都是非阻塞的,這就可以充分提升IO線程的運行效率秧骑,避免由于頻繁I/O阻塞導(dǎo)致的線程掛起版确,一個I/O線程可以并發(fā)處理N個客戶端連接和讀寫操作,這從根本上解決了傳統(tǒng)同步阻塞I/O一連接一線程模型乎折,架構(gòu)的性能绒疗、彈性伸縮能力和可靠性都得到了極大的提升。

基于buffer

傳統(tǒng)的I/O是面向字節(jié)流或字符流的骂澄,以流式的方式順序地從一個Stream 中讀取一個或多個字節(jié), 因此也就不能隨意改變讀取指針的位置吓蘑。

在NIO中, 拋棄了傳統(tǒng)的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能從Channel中讀取數(shù)據(jù)到Buffer中或?qū)?shù)據(jù) Buffer 中寫入到 Channel。

基于buffer操作不像傳統(tǒng)IO的順序操作, NIO 中可以隨意地讀取任意位置的數(shù)據(jù)

線程模型

數(shù)據(jù)報如何讀取磨镶?讀取之后的編解碼在哪個線程進行溃蔫,編解碼后的消息如何派發(fā),線程模型的不同琳猫,對性能的影響也非常大伟叛。

事件驅(qū)動模型

通常,我們設(shè)計一個事件處理模型的程序有兩種思路

  • 輪詢方式 線程不斷輪詢訪問相關(guān)事件發(fā)生源有沒有發(fā)生事件脐嫂,有發(fā)生事件就調(diào)用事件處理邏輯统刮。
  • 事件驅(qū)動方式 發(fā)生事件,主線程把事件放入事件隊列雹锣,在另外線程不斷循環(huán)消費事件列表中的事件网沾,調(diào)用事件對應(yīng)的處理邏輯處理事件。事件驅(qū)動方式也被稱為消息通知方式蕊爵,其實是設(shè)計模式中觀察者模式的思路辉哥。

以GUI的邏輯處理為例,說明兩種邏輯的不同:

  • 輪詢方式 線程不斷輪詢是否發(fā)生按鈕點擊事件攒射,如果發(fā)生醋旦,調(diào)用處理邏輯
  • 事件驅(qū)動方式 發(fā)生點擊事件把事件放入事件隊列,在另外線程消費的事件列表中的事件会放,根據(jù)事件類型調(diào)用相關(guān)事件處理邏輯

這里借用O’Reilly 大神關(guān)于事件驅(qū)動模型解釋圖

主要包括4個基本組件:

  • 事件隊列(event queue):接收事件的入口饲齐,存儲待處理事件
  • 分發(fā)器(event mediator):將不同的事件分發(fā)到不同的業(yè)務(wù)邏輯單元
  • 事件通道(event channel):分發(fā)器與處理器之間的聯(lián)系渠道
  • 事件處理器(event processor):實現(xiàn)業(yè)務(wù)邏輯,處理完成后會發(fā)出事件咧最,觸發(fā)下一步操作

可以看出捂人,相對傳統(tǒng)輪詢模式,事件驅(qū)動有如下優(yōu)點:

  • 可擴展性好矢沿,分布式的異步架構(gòu)滥搭,事件處理器之間高度解耦,可以方便擴展事件處理邏輯
  • 高性能捣鲸,基于隊列暫存事件瑟匆,能方便并行異步處理事件

Reactor線程模型

Reactor是反應(yīng)堆的意思,Reactor模型栽惶,是指通過一個或多個輸入同時傳遞給服務(wù)處理器的服務(wù)請求的事件驅(qū)動處理模式愁溜。 服務(wù)端程序處理傳入多路請求,并將它們同步分派給請求對應(yīng)的處理線程外厂,Reactor模式也叫Dispatcher模式冕象,即I/O多了復(fù)用統(tǒng)一監(jiān)聽事件,收到事件后分發(fā)(Dispatch給某進程)酣衷,是編寫高性能網(wǎng)絡(luò)服務(wù)器的必備技術(shù)之一交惯。

Reactor模型中有2個關(guān)鍵組成:

  • Reactor Reactor在一個單獨的線程中運行,負責監(jiān)聽和分發(fā)事件,分發(fā)給適當?shù)奶幚沓绦騺韺O事件做出反應(yīng)席爽。 它就像公司的電話接線員意荤,它接聽來自客戶的電話并將線路轉(zhuǎn)移到適當?shù)穆?lián)系人
  • Handlers 處理程序執(zhí)行I/O事件要完成的實際事件,類似于客戶想要與之交談的公司中的實際官員只锻。Reactor通過調(diào)度適當?shù)奶幚沓绦騺眄憫?yīng)I/O事件玖像,處理程序執(zhí)行非阻塞操作

取決于Reactor的數(shù)量和Hanndler線程數(shù)量的不同,Reactor模型有3個變種

  • 單Reactor單線程
  • 單Reactor多線程
  • 主從Reactor多線程

可以這樣理解齐饮,Reactor就是一個執(zhí)行while (true) { selector.select(); …}循環(huán)的線程捐寥,會源源不斷的產(chǎn)生新的事件,稱作反應(yīng)堆很貼切祖驱。

篇幅關(guān)系握恳,這里不再具體展開Reactor特性、優(yōu)缺點比較捺僻,有興趣的讀者可以參考我之前另外一篇文章:《理解高性能網(wǎng)絡(luò)模型》

Netty線程模型

Netty主要基于主從Reactors多線程模型(如下圖)做了一定的修改乡洼,其中主從Reactor多線程模型有多個Reactor:MainReactor和SubReactor:

  • MainReactor負責客戶端的連接請求掂器,并將請求轉(zhuǎn)交給SubReactor
  • SubReactor負責相應(yīng)通道的IO讀寫請求
  • 非IO請求(具體邏輯處理)的任務(wù)則會直接寫入隊列彤敛,等待worker threads進行處理

這里引用Doug Lee大神的Reactor介紹:Scalable IO in Java里面關(guān)于主從Reactor多線程模型的圖

特別說明的是: 雖然Netty的線程模型基于主從Reactor多線程,借用了MainReactor和SubReactor的結(jié)構(gòu)聘鳞,但是實際實現(xiàn)上葛峻,SubReactor和Worker線程在同一個線程池中:

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)

123456

上面代碼中的bossGroup 和workerGroup是Bootstrap構(gòu)造方法中傳入的兩個對象锹雏,這兩個group均是線程池

  • bossGroup線程池則只是在bind某個端口后,獲得其中一個線程作為MainReactor术奖,專門處理端口的accept事件礁遵,每個端口對應(yīng)一個boss線程
  • workerGroup線程池會被各個SubReactor和worker線程充分利用

異步處理

異步的概念和同步相對。當一個異步過程調(diào)用發(fā)出后采记,調(diào)用者不能立刻得到結(jié)果榛丢。實際處理這個調(diào)用的部件在完成后,通過狀態(tài)挺庞、通知和回調(diào)來通知調(diào)用者。

Netty中的I/O操作是異步的稼病,包括bind选侨、write、connect等操作會簡單的返回一個ChannelFuture然走,調(diào)用者并不能立刻獲得結(jié)果援制,通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結(jié)果芍瑞。

當future對象剛剛創(chuàng)建時晨仑,處于非完成狀態(tài),調(diào)用者可以通過返回的ChannelFuture來獲取操作執(zhí)行的狀態(tài),注冊監(jiān)聽函數(shù)來執(zhí)行完成后的操洪己,常見有如下操作:

  • 通過isDone方法來判斷當前操作是否完成
  • 通過isSuccess方法來判斷已完成的當前操作是否成功
  • 通過getCause方法來獲取已完成的當前操作失敗的原因
  • 通過isCancelled方法來判斷已完成的當前操作是否被取消
  • 通過addListener方法來注冊監(jiān)聽器妥凳,當操作已完成(isDone方法返回完成),將會通知指定的監(jiān)聽器答捕;如果future對象已完成逝钥,則理解通知指定的監(jiān)聽器

例如下面的的代碼中綁定端口是異步操作,當綁定操作處理完拱镐,將會調(diào)用相應(yīng)的監(jiān)聽器處理邏輯

    serverBootstrap.bind(port).addListener(future -> {
        if (future.isSuccess()) {
            System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");
        } else {
            System.err.println("端口[" + port + "]綁定失敗!");
        }
    });

12345678

相比傳統(tǒng)阻塞I/O艘款,執(zhí)行I/O操作后線程會被阻塞住, 直到操作完成;異步處理的好處是不會造成線程阻塞沃琅,線程在I/O操作期間可以執(zhí)行別的程序哗咆,在高并發(fā)情形下會更穩(wěn)定和更高的吞吐量。

Netty架構(gòu)設(shè)計

前面介紹完Netty相關(guān)一些理論介紹益眉,下面從功能特性晌柬、模塊組件、運作過程來介紹Netty的架構(gòu)設(shè)計

功能特性

  • 傳輸服務(wù) 支持BIO和NIO
  • 容器集成 支持OSGI呜叫、JBossMC空繁、Spring、Guice容器
  • 協(xié)議支持 HTTP朱庆、Protobuf盛泡、二進制、文本娱颊、WebSocket等一系列常見協(xié)議都支持傲诵。 還支持通過實行編碼解碼邏輯來實現(xiàn)自定義協(xié)議
  • Core核心 可擴展事件模型、通用通信API箱硕、支持零拷貝的ByteBuf緩沖對象

模塊組件

Bootstrap拴竹、ServerBootstrap

Bootstrap意思是引導(dǎo),一個Netty應(yīng)用通常由一個Bootstrap開始剧罩,主要作用是配置整個Netty程序栓拜,串聯(lián)各個組件,Netty中Bootstrap類是客戶端程序的啟動引導(dǎo)類惠昔,ServerBootstrap是服務(wù)端啟動引導(dǎo)類幕与。

Future、ChannelFuture

正如前面介紹镇防,在Netty中所有的IO操作都是異步的啦鸣,不能立刻得知消息是否被正確處理,但是可以過一會等它執(zhí)行完成或者直接注冊一個監(jiān)聽来氧,具體的實現(xiàn)就是通過Future和ChannelFutures诫给,他們可以注冊一個監(jiān)聽香拉,當操作執(zhí)行成功或失敗時監(jiān)聽會自動觸發(fā)注冊的監(jiān)聽事件。

Channel

Netty網(wǎng)絡(luò)通信的組件中狂,能夠用于執(zhí)行網(wǎng)絡(luò)I/O操作凫碌。 Channel為用戶提供:

  • 當前網(wǎng)絡(luò)連接的通道的狀態(tài)(例如是否打開?是否已連接吃型?)
  • 網(wǎng)絡(luò)連接的配置參數(shù) (例如接收緩沖區(qū)大兄づ浮)
  • 提供異步的網(wǎng)絡(luò)I/O操作(如建立連接,讀寫勤晚,綁定端口)枉层,異步調(diào)用意味著任何I / O調(diào)用都將立即返回,并且不保證在調(diào)用結(jié)束時所請求的I / O操作已完成赐写。調(diào)用立即返回一個ChannelFuture實例鸟蜡,通過注冊監(jiān)聽器到ChannelFuture上,可以I / O操作成功挺邀、失敗或取消時回調(diào)通知調(diào)用方揉忘。
  • 支持關(guān)聯(lián)I/O操作與對應(yīng)的處理程序

不同協(xié)議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應(yīng)端铛,下面是一些常用的 Channel 類型

  • NioSocketChannel泣矛,異步的客戶端 TCP Socket 連接
  • NioServerSocketChannel,異步的服務(wù)器端 TCP Socket 連接
  • NioDatagramChannel禾蚕,異步的 UDP 連接
  • NioSctpChannel您朽,異步的客戶端 Sctp 連接
  • NioSctpServerChannel,異步的 Sctp 服務(wù)器端連接 這些通道涵蓋了 UDP 和 TCP網(wǎng)絡(luò) IO以及文件 IO.

Selector

Netty基于Selector對象實現(xiàn)I/O多路復(fù)用换淆,通過 Selector, 一個線程可以監(jiān)聽多個連接的Channel事件, 當向一個Selector中注冊Channel 后哗总,Selector 內(nèi)部的機制就可以自動不斷地查詢(select) 這些注冊的Channel是否有已就緒的I/O事件(例如可讀, 可寫, 網(wǎng)絡(luò)連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel 倍试。

NioEventLoop

NioEventLoop中維護了一個線程和任務(wù)隊列讯屈,支持異步提交執(zhí)行任務(wù),線程啟動時會調(diào)用NioEventLoop的run方法县习,執(zhí)行I/O任務(wù)和非I/O任務(wù):

  • I/O任務(wù) 即selectionKey中ready的事件涮母,如accept、connect躁愿、read哈蝇、write等,由processSelectedKeys方法觸發(fā)攘已。
  • 非IO任務(wù) 添加到taskQueue中的任務(wù),如register0怜跑、bind0等任務(wù)样勃,由runAllTasks方法觸發(fā)吠勘。

兩種任務(wù)的執(zhí)行時間比由變量ioRatio控制,默認為50峡眶,則表示允許非IO任務(wù)執(zhí)行的時間與IO任務(wù)的執(zhí)行時間相等剧防。

NioEventLoopGroup

NioEventLoopGroup,主要管理eventLoop的生命周期辫樱,可以理解為一個線程池峭拘,內(nèi)部維護了一組線程,每個線程(NioEventLoop)負責處理多個Channel上的事件狮暑,而一個Channel只對應(yīng)于一個線程鸡挠。

ChannelHandler

ChannelHandler是一個接口,處理I / O事件或攔截I / O操作搬男,并將其轉(zhuǎn)發(fā)到其ChannelPipeline(業(yè)務(wù)處理鏈)中的下一個處理程序拣展。

ChannelHandler本身并沒有提供很多方法,因為這個接口有許多的方法需要實現(xiàn)缔逛,方便使用期間备埃,可以繼承它的子類:

  • ChannelInboundHandler用于處理入站I / O事件
  • ChannelOutboundHandler用于處理出站I / O操作

或者使用以下適配器類:

  • ChannelInboundHandlerAdapter用于處理入站I / O事件
  • ChannelOutboundHandlerAdapter用于處理出站I / O操作
  • ChannelDuplexHandler用于處理入站和出站事件

ChannelHandlerContext

保存Channel相關(guān)的所有上下文信息,同時關(guān)聯(lián)一個ChannelHandler對象

ChannelPipline

保存ChannelHandler的List褐奴,用于處理或攔截Channel的入站事件和出站操作按脚。 ChannelPipeline實現(xiàn)了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式敦冬,以及Channel中各個的ChannelHandler如何相互交互辅搬。

下圖引用Netty的Javadoc4.1中ChannelPipline的說明,描述了ChannelPipeline中ChannelHandler通常如何處理I/O事件匪补。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler處理伞辛,并通過調(diào)用ChannelHandlerContext中定義的事件傳播方法(例如ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object))轉(zhuǎn)發(fā)到其最近的處理程序。

                                                 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)            |
  +-------------------------------------------------------------------+

123456789101112131415161718192021222324252627282930313233343536373839

入站事件由自下而上方向的入站處理程序處理夯缺,如圖左側(cè)所示蚤氏。 入站Handler處理程序通常處理由圖底部的I / O線程生成的入站數(shù)據(jù)。 通常通過實際輸入操作(例如SocketChannel.read(ByteBuffer))從遠程讀取入站數(shù)據(jù)踊兜。

出站事件由上下方向處理竿滨,如圖右側(cè)所示。 出站Handler處理程序通常會生成或轉(zhuǎn)換出站傳輸捏境,例如write請求于游。 I/O線程通常執(zhí)行實際的輸出操作,例如SocketChannel.write(ByteBuffer)垫言。

在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應(yīng), 它們的組成關(guān)系如下:

一個 Channel 包含了一個 ChannelPipeline, 而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向鏈表, 并且每個 ChannelHandlerContext 中又關(guān)聯(lián)著一個 ChannelHandler贰剥。入站事件和出站事件在一個雙向鏈表中,入站事件會從鏈表head往后傳遞到最后一個入站的handler筷频,出站事件會從鏈表tail往前傳遞到最前一個出站的handler蚌成,兩種類型的handler互不干擾前痘。

工作原理架構(gòu)

初始化并啟動Netty服務(wù)端過程如下:

    public static void main(String[] args) {
        // 創(chuàng)建mainReactor
        NioEventLoopGroup boosGroup = new NioEventLoopGroup();
        // 創(chuàng)建工作線程組
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        final ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap 
                 // 組裝NioEventLoopGroup 
                .group(boosGroup, workerGroup)
                 // 設(shè)置channel類型為NIO類型
                .channel(NioServerSocketChannel.class)
                // 設(shè)置連接配置參數(shù)
                .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 + "]綁定失敗!");
            }
        });
}
  • 基本過程如下:
  • 1 初始化創(chuàng)建2個NioEventLoopGroup芹缔,其中boosGroup用于Accetpt連接建立事件并分發(fā)請求, workerGroup用于處理I/O讀寫事件和業(yè)務(wù)邏輯
  • 2 基于ServerBootstrap(服務(wù)端啟動引導(dǎo)類)瓶盛,配置EventLoopGroup最欠、Channel類型,連接參數(shù)惩猫、配置入站芝硬、出站事件handler
  • 3 綁定端口,開始工作

結(jié)合上面的介紹的Netty Reactor模型帆锋,介紹服務(wù)端Netty的工作架構(gòu)圖:

server端包含1個Boss NioEventLoopGroup和1個Worker NioEventLoopGroup吵取,NioEventLoopGroup相當于1個事件循環(huán)組,這個組里包含多個事件循環(huán)NioEventLoop锯厢,每個NioEventLoop包含1個selector和1個事件循環(huán)線程皮官。

每個Boss NioEventLoop循環(huán)執(zhí)行的任務(wù)包含3步:

  • 1 輪詢accept事件
  • 2 處理accept I/O事件,與Client建立連接实辑,生成NioSocketChannel捺氢,并將NioSocketChannel注冊到某個Worker NioEventLoop的Selector上 *3 處理任務(wù)隊列中的任務(wù),runAllTasks剪撬。任務(wù)隊列中的任務(wù)包括用戶調(diào)用eventloop.execute或schedule執(zhí)行的任務(wù)摄乒,或者其它線程提交到該eventloop的任務(wù)。

每個Worker NioEventLoop循環(huán)執(zhí)行的任務(wù)包含3步:

  • 1 輪詢read残黑、write事件馍佑;
  • 2 處I/O事件,即read梨水、write事件拭荤,在NioSocketChannel可讀、可寫事件發(fā)生時進行處理
  • 3 處理任務(wù)隊列中的任務(wù)疫诽,runAllTasks舅世。

其中任務(wù)隊列中的task有3種典型使用場景

  • 1 用戶程序自定義的普通任務(wù)
ctx.channel().eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        //...
    }
});

1234567
  • 2 非當前reactor線程調(diào)用channel的各種方法 例如在推送系統(tǒng)的業(yè)務(wù)線程里面,根據(jù)用戶的標識奇徒,找到對應(yīng)的channel引用雏亚,然后調(diào)用write類方法向該用戶推送消息,就會進入到這種場景摩钙。最終的write會提交到任務(wù)隊列中后被異步消費罢低。
  • 3 用戶自定義定時任務(wù)
ctx.channel().eventLoop().schedule(new Runnable() {
    @Override
    public void run() {

    }
}, 60, TimeUnit.SECONDS);

1234567

總結(jié)

現(xiàn)在穩(wěn)定推薦使用的主流版本還是Netty4,Netty5 中使用了 ForkJoinPool胖笛,增加了代碼的復(fù)雜度奕短,但是對性能的改善卻不明顯宜肉,所以這個版本不推薦使用,官網(wǎng)也沒有提供下載鏈接翎碑。

Netty 入門門檻相對較高,其實是因為這方面的資料較少之斯,并不是因為他有多難日杈,大家其實都可以像搞透 Spring 一樣搞透 Netty。在學習之前佑刷,建議先理解透整個框架原理結(jié)構(gòu)莉擒,運行過程,可以少走很多彎路瘫絮。

作者:thinkwon
來源:https://thinkwon.blog.csdn.net/article/details/104391081
本文首發(fā)于公眾號:Java版web項目涨冀,歡迎關(guān)注獲取更多精彩內(nèi)容

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市麦萤,隨后出現(xiàn)的幾起案子鹿鳖,更是在濱河造成了極大的恐慌,老刑警劉巖壮莹,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翅帜,死亡現(xiàn)場離奇詭異,居然都是意外死亡命满,警方通過查閱死者的電腦和手機涝滴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胶台,“玉大人歼疮,你說我怎么就攤上這事≌┗#” “怎么了韩脏?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長讯榕。 經(jīng)常有香客問我骤素,道長,這世上最難降的妖魔是什么愚屁? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任济竹,我火速辦了婚禮,結(jié)果婚禮上霎槐,老公的妹妹穿的比我還像新娘送浊。我一直安慰自己,他們只是感情好丘跌,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布袭景。 她就那樣靜靜地躺著唁桩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪耸棒。 梳的紋絲不亂的頭發(fā)上荒澡,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天,我揣著相機與錄音与殃,去河邊找鬼单山。 笑死,一個胖子當著我的面吹牛幅疼,可吹牛的內(nèi)容都是我干的米奸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼爽篷,長吁一口氣:“原來是場噩夢啊……” “哼悴晰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起逐工,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤铡溪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后钻弄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體佃却,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年窘俺,在試婚紗的時候發(fā)現(xiàn)自己被綠了饲帅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡瘤泪,死狀恐怖灶泵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情对途,我是刑警寧澤赦邻,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站实檀,受9級特大地震影響惶洲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜膳犹,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一恬吕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧须床,春花似錦铐料、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柒凉。三九已至,卻和暖如春篓跛,著一層夾襖步出監(jiān)牢的瞬間膝捞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工愧沟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绑警,地道東北人。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓央渣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親渴频。 傳聞我的和親對象是個殘疾皇子芽丹,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內(nèi)容