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