本文是Netty文集中“Netty in action”系列的文章。主要是對Norman Maurer and Marvin Allen Wolfthal 的 《Netty in action》一書簡要翻譯吏祸,同時對重要點加上一些自己補(bǔ)充和擴(kuò)展对蒲。
概要
- OIO —— 阻塞傳輸
- NIO —— 異步傳輸
- Local transport —— JVM內(nèi)部的異步通訊
- Embedded transport —— 測試你的ChannelHandlers
數(shù)據(jù)流經(jīng)一個網(wǎng)絡(luò)時總是有一樣的類型:字節(jié)。
使用JAVA提供OIO API 和 NIO API 有著很大的不同。
Netty使用了一個公共的API層蹈矮,該API涵蓋了所以的傳輸實現(xiàn)
在Netty中使用OIO 和 NIO
傳輸協(xié)議API
傳輸API的關(guān)鍵是 Channel 接口砰逻,Channel接口被用于所有的I/O操作。
一個Channel會被分配有一個ChannelPipeline和一個ChannelConfig泛鸟。
ChannelConfig持有所有設(shè)置Channel的配置并支持熱修改蝠咆。因為一個指定的傳輸可能有它獨(dú)特的設(shè)置,它可以實現(xiàn)一個ChannelConfig的子類北滥。
因為Channel都是獨(dú)一無二的刚操,所以聲明Channel為java.lang.Comparable的子類用意是為了保證排序。因此再芋,AbstractChannel對compareTo方法實現(xiàn):當(dāng)兩個不同的channel實例返回了相同的hashCode將拋出一個Error異常菊霜。
ChannelPipeline持有所以的ChannelHandler實例,這些ChannelHandler實例將被應(yīng)用到入站和出站數(shù)據(jù)和事件上济赎。這些ChannelHandlers實現(xiàn)了用于處理狀態(tài)改變和數(shù)據(jù)處理的應(yīng)用邏輯占卧。
典型的ChannelHandlers的使用包括:
- 轉(zhuǎn)換數(shù)據(jù)格式從一種到另外一種
- 提供異常的通知
- 提供一個Channel活躍( active )或不活躍( inactive )的通知
- 提供當(dāng)一個Channel注冊( registered )到EventLoop或從EventLoop注銷( deregistered )的通知
- 提供關(guān)于用戶定義事件的通知
Intercepting Filter :ChannelPipeline實現(xiàn)了一個常見的設(shè)計模式,攔截過濾器联喘。UNIX 的管道是另一個常見的例子:指令被鏈接到一起,通過一個指令的輸出連接到下一個行的輸入辙纬。( 也就是將當(dāng)前指令的輸出作為下一條指令的輸入內(nèi)容豁遭,以此方式將指令給鏈接到一起 )
你可以通過需要添加或刪除ChannelHandler來即時修改ChannelPipeline。Netty的這個能力能被利用與構(gòu)建一個高靈活性的應(yīng)用贺拣。
Netty的Channel實現(xiàn)是線程安全的蓖谢,所以你能夠存有一個Channel的引用,并在你需要的任何時候使用它去寫數(shù)據(jù)到遠(yuǎn)端譬涡,甚至可以多個線程同時使用這個引用闪幽。
包含的傳輸協(xié)議
NIO —— 非阻塞 I/O
NIO提供所有I/O操作的完全異步實現(xiàn)涡匀。它使用了基于selector的API盯腌。
selector的一個基本概念是作為一個注冊表,你請求收到一個通知當(dāng)Channel的狀態(tài)改變時陨瘩。
可能的狀態(tài)改變有:
- OP_ACCEPT :個新Channel被接收并準(zhǔn)備好 ( 服務(wù)端 )
- OP_CONNECT :一個Channel連接已經(jīng)完成 ( 客戶端 )
- OP_READ :一個Channel的數(shù)據(jù)已經(jīng)準(zhǔn)備好被讀取
- OP_WRITE :一個Channel的寫數(shù)據(jù)有效腕够。
OP_WRITE需要特別注意。該事件表示的是:請求收到通知舌劳,當(dāng)Channel能夠?qū)懭敫嗟臄?shù)據(jù)時帚湘。這是當(dāng)socket緩存已經(jīng)完全滿的處理情況( 即,當(dāng)socket緩存已經(jīng)滿了甚淡,但還有數(shù)據(jù)未寫完時大诸,需要注冊該事件為希望得到通知的事件 ),這經(jīng)常發(fā)生在當(dāng)數(shù)據(jù)的傳輸速度遠(yuǎn)快于遠(yuǎn)端處理數(shù)據(jù)的速度時。
在應(yīng)用對狀態(tài)的改變作出反應(yīng)后资柔,selector將被重置焙贷,并且重復(fù)該過程。
這些模式被合并到一個指定的集合中建邓,應(yīng)用請求得到一個通知當(dāng)該集合中包含的狀態(tài)改變時盈厘。
這些NIO的內(nèi)部實現(xiàn)被用戶級API所隱藏,該API是Netty所有傳輸?shù)墓餐瑢崿F(xiàn)官边。
零拷貝是目前僅適用于NIO和Epoll傳輸?shù)墓δ芊惺帧K试S你 快速且高效的移動數(shù)據(jù)從一個文件系統(tǒng)到網(wǎng)絡(luò),而無需從內(nèi)核空間拷貝數(shù)據(jù)到用戶空間注簿,這能夠顯著提升如FTP 或 HTTP協(xié)議的性能契吉。零拷貝功能并不是所有的操作系統(tǒng)都支持的。需要指明的零拷貝不能用于實現(xiàn)文件系統(tǒng)的數(shù)據(jù)加密或壓縮诡渴,它只能夠傳輸未加工的文件內(nèi)容捐晶。相反的,傳輸一個已經(jīng)被加密過的文件不是問題妄辩。
也就是說惑灵,有些文件系統(tǒng)不是單純的操作一個數(shù)據(jù)的傳輸,還要對文件進(jìn)行一些加密和壓縮的操作眼耀,而這些需要將數(shù)據(jù)拷貝到用戶空間并對數(shù)據(jù)進(jìn)行修改操作英支。所以像這樣的文件操作是不支持零拷貝的。
Epoll —— Linux的本地非阻塞傳輸
正如我們前面說展示的哮伟,Netty的NIO傳輸是基于java提供的異步/非阻塞網(wǎng)絡(luò)的通用抽象干花。盡管這確保了Netty的NIO能在任何平臺上使用;但它也有限制楞黄,因為JDK必須妥協(xié)才能讓所有的系統(tǒng)都具有相同的功能池凄。
Linux作為日漸重要的高性能網(wǎng)絡(luò)平臺,這導(dǎo)致了許多先進(jìn)功能的開發(fā)鬼廓,包括epoll肿仑,一個高可擴(kuò)展的I/O事件通知功能。
Netty為Linux提供了一個使用epoll的NIO API碎税,通過該方式與你的設(shè)計更加一致并且使中斷的使用成本更低柏副。在大負(fù)載的性能上,Linux NIO 實現(xiàn)優(yōu)于JDK NIO 的實現(xiàn)蚣录。
OIO —— 老的阻塞 I/O
Netty OIO傳輸實現(xiàn)代表著一種妥協(xié):它通過通用的傳輸API來訪問割择,但因為他構(gòu)建在java.net的阻塞實現(xiàn)上,它是非異步的萎河。它非常適用于某些情況荔泳。鑒于此蕉饼,你可能擔(dān)心Netty如何提供一個NIO通過一樣的API用于異步的傳輸。這個答案是Netty使用 SO_TIMEOUT Socket 標(biāo)志玛歌,該標(biāo)志指定了等待I/O操作完成的最大毫秒數(shù)昧港。如果一個操作在指定期間內(nèi)沒有完成,那么將拋出一個SocketTimeoutException異常支子。Netty捕獲這個異常并繼續(xù)處理循環(huán)创肥。在下一次EventLoop運(yùn)行時,將再嘗試一次前面的邏輯值朋。這是一個像Netty的異步框架能夠支持OIO的唯一方式叹侄。
我們通過OioSocketChannel的讀操作來了解下關(guān)于上面描述的源碼實現(xiàn):
??這個讀操操作如果拋出超時異常,則會返回讀到的字節(jié)數(shù)為0昨登。這里大家可以關(guān)注另外一點趾代,在當(dāng)socket關(guān)閉是,返回時可讀字節(jié)數(shù)為-1丰辣。這個是和NIO的模式相一致的撒强,在NIO中如果read返回的可讀字節(jié)數(shù)為-1時,也就表示當(dāng)遠(yuǎn)端連接已經(jīng)關(guān)閉了笙什。
用于JVM內(nèi)部通訊的本地傳輸
Netty提供了一個本地傳輸用于客戶端和服務(wù)端在相同JVM的異步通訊飘哨。
在該傳輸中,一個同服務(wù)端Channel關(guān)聯(lián)的SocketAddress不會綁定到一個物理網(wǎng)絡(luò)地址琐凭;當(dāng)然芽隆,它會被保存到一個注冊表在服務(wù)端運(yùn)行的期間,并在Channel ( 這里指服務(wù)端的channel )關(guān)閉時被注銷淘正。所以傳輸沒有通過真實的網(wǎng)絡(luò)傳輸,所以它不能通過其他傳輸?shù)膶崿F(xiàn)來進(jìn)行交互 ( 也就是不能同其他傳輸臼闻,如NIO transport 進(jìn)行數(shù)據(jù)的傳輸交互 )鸿吆。所以客戶端希望連接一個在同一JVM的使用了該傳輸方式的服務(wù)端,那么客戶端也需要使用該傳輸方式述呐。除了這個限制惩淳,它與其他傳輸方式并無不同。
內(nèi)嵌的傳輸協(xié)議
Netty提供了一個附加的傳輸方式乓搬,該傳輸方式允許你一個ChannelHandler作為輔助類嵌入到其他ChannelHandler中思犁。照這樣,你能在不修改內(nèi)部代碼的情況下夠擴(kuò)展一個ChannelHandler的功能进肯。
傳輸協(xié)議使用場景
并不是所有的傳輸方式都支持所有的傳輸協(xié)議。這里是你可能會遇到的使用場景:
- 非阻塞代碼庫 —— 如果你不要一個阻塞調(diào)用在你的代碼庫中江掩,或者你能夠限制它們学辱,在Linux上使用NIO或epoll經(jīng)常是個好主意乘瓤。當(dāng)NIO/epoll 用于處理許多并發(fā)的連接,它也能通過更少的線程來更好的工作策泣,尤其是在連接間共享線程的方式衙傀。
- 阻塞代碼庫 —— 正如我們已經(jīng)說到的,如果你的代碼庫嚴(yán)重依賴于阻塞I/O萨咕,并且你的應(yīng)用有對應(yīng)于此的設(shè)計统抬。如果你直接轉(zhuǎn)為Netty的NIO傳輸方式,你可能會遇到阻塞操作問題危队。對比與重寫你的代碼去完成這些聪建,考慮一個階段性的遷移:從OIO開始,然后轉(zhuǎn)移到NIO(或epoll如果你在Linux上)當(dāng)你改進(jìn)你的代碼后交掏。
- 相同JVM的內(nèi)部通訊 —— 在相同JVM的內(nèi)部通訊不需要暴露一個服務(wù)在網(wǎng)絡(luò)表現(xiàn)層妆偏,在相同JVM的內(nèi)部通訊為本地傳輸?shù)耐昝朗褂们闆r。這將消除真實網(wǎng)絡(luò)操作的所有開銷盅弛,同時仍然使用你的Netty代碼庫钱骂。如果需要暴露一個服務(wù)在網(wǎng)絡(luò)上,你只需要簡單的修改傳輸方式為NIO或OIO挪鹏。
- 測試你的ChannelHandler的實現(xiàn) —— 如果你想要寫單元測試用于你的ChannelHandler實現(xiàn)见秽,考慮使用內(nèi)嵌的傳輸方式。這將使測試你的代碼變得簡單讨盒,而不需創(chuàng)建許多的mock對象解取。你的類將仍然遵循通用API的事件流,保證ChannelHandler將在真實傳輸中正確工作返顺。
后記
本文主要對Netty的支持的傳輸協(xié)議進(jìn)行了介紹禀苦。即便是不同的傳輸協(xié)議,Netty也為我們提供了一致的API接口遂鹊,它將大量復(fù)雜的處理邏輯封裝在了源碼實現(xiàn)中振乏,為用戶提供了簡易且方便的API接口,這也是Netty設(shè)計一致性的例子之一秉扑。
若文章有任何錯誤慧邮,望大家不吝指教:)