本文是Netty文集中“Netty in action”系列的文章。主要是對Norman Maurer and Marvin Allen Wolfthal 的 《Netty in action》一書簡要翻譯,同時對重要點加上一些自己補充和擴展。
概要
- OIO —— 阻塞傳輸
- NIO —— 異步傳輸
- Local transport —— JVM內部的異步通訊
- Embedded transport —— 測試你的ChannelHandlers
數據流經一個網絡時總是有一樣的類型:字節。
使用JAVA提供OIO API 和 NIO API 有著很大的不同。
Netty使用了一個公共的API層,該API涵蓋了所以的傳輸實現
在Netty中使用OIO 和 NIO
傳輸協議API
傳輸API的關鍵是 Channel 接口,Channel接口被用于所有的I/O操作。
一個Channel會被分配有一個ChannelPipeline和一個ChannelConfig。
ChannelConfig持有所有設置Channel的配置并支持熱修改。因為一個指定的傳輸可能有它獨特的設置,它可以實現一個ChannelConfig的子類。
因為Channel都是獨一無二的,所以聲明Channel為java.lang.Comparable的子類用意是為了保證排序。因此,AbstractChannel對compareTo方法實現:當兩個不同的channel實例返回了相同的hashCode將拋出一個Error異常。
ChannelPipeline持有所以的ChannelHandler實例,這些ChannelHandler實例將被應用到入站和出站數據和事件上。這些ChannelHandlers實現了用于處理狀態改變和數據處理的應用邏輯。
典型的ChannelHandlers的使用包括:
- 轉換數據格式從一種到另外一種
- 提供異常的通知
- 提供一個Channel活躍( active )或不活躍( inactive )的通知
- 提供當一個Channel注冊( registered )到EventLoop或從EventLoop注銷( deregistered )的通知
- 提供關于用戶定義事件的通知
Intercepting Filter :ChannelPipeline實現了一個常見的設計模式,攔截過濾器。UNIX 的管道是另一個常見的例子:指令被鏈接到一起,通過一個指令的輸出連接到下一個行的輸入。( 也就是將當前指令的輸出作為下一條指令的輸入內容,以此方式將指令給鏈接到一起 )
你可以通過需要添加或刪除ChannelHandler來即時修改ChannelPipeline。Netty的這個能力能被利用與構建一個高靈活性的應用。
Netty的Channel實現是線程安全的,所以你能夠存有一個Channel的引用,并在你需要的任何時候使用它去寫數據到遠端,甚至可以多個線程同時使用這個引用。
包含的傳輸協議
NIO —— 非阻塞 I/O
NIO提供所有I/O操作的完全異步實現。它使用了基于selector的API。
selector的一個基本概念是作為一個注冊表,你請求收到一個通知當Channel的狀態改變時。
可能的狀態改變有:
- OP_ACCEPT :個新Channel被接收并準備好 ( 服務端 )
- OP_CONNECT :一個Channel連接已經完成 ( 客戶端 )
- OP_READ :一個Channel的數據已經準備好被讀取
- OP_WRITE :一個Channel的寫數據有效。
OP_WRITE需要特別注意。該事件表示的是:請求收到通知,當Channel能夠寫入更多的數據時。這是當socket緩存已經完全滿的處理情況( 即,當socket緩存已經滿了,但還有數據未寫完時,需要注冊該事件為希望得到通知的事件 ),這經常發生在當數據的傳輸速度遠快于遠端處理數據的速度時。
在應用對狀態的改變作出反應后,selector將被重置,并且重復該過程。
這些模式被合并到一個指定的集合中,應用請求得到一個通知當該集合中包含的狀態改變時。
這些NIO的內部實現被用戶級API所隱藏,該API是Netty所有傳輸的共同實現。
零拷貝是目前僅適用于NIO和Epoll傳輸的功能。它允許你 快速且高效的移動數據從一個文件系統到網絡,而無需從內核空間拷貝數據到用戶空間,這能夠顯著提升如FTP 或 HTTP協議的性能。零拷貝功能并不是所有的操作系統都支持的。需要指明的零拷貝不能用于實現文件系統的數據加密或壓縮,它只能夠傳輸未加工的文件內容。相反的,傳輸一個已經被加密過的文件不是問題。
也就是說,有些文件系統不是單純的操作一個數據的傳輸,還要對文件進行一些加密和壓縮的操作,而這些需要將數據拷貝到用戶空間并對數據進行修改操作。所以像這樣的文件操作是不支持零拷貝的。
Epoll —— Linux的本地非阻塞傳輸
正如我們前面說展示的,Netty的NIO傳輸是基于java提供的異步/非阻塞網絡的通用抽象。盡管這確保了Netty的NIO能在任何平臺上使用;但它也有限制,因為JDK必須妥協才能讓所有的系統都具有相同的功能。
Linux作為日漸重要的高性能網絡平臺,這導致了許多先進功能的開發,包括epoll,一個高可擴展的I/O事件通知功能。
Netty為Linux提供了一個使用epoll的NIO API,通過該方式與你的設計更加一致并且使中斷的使用成本更低。在大負載的性能上,Linux NIO 實現優于JDK NIO 的實現。
OIO —— 老的阻塞 I/O
Netty OIO傳輸實現代表著一種妥協:它通過通用的傳輸API來訪問,但因為他構建在java.net的阻塞實現上,它是非異步的。它非常適用于某些情況。鑒于此,你可能擔心Netty如何提供一個NIO通過一樣的API用于異步的傳輸。這個答案是Netty使用 SO_TIMEOUT Socket 標志,該標志指定了等待I/O操作完成的最大毫秒數。如果一個操作在指定期間內沒有完成,那么將拋出一個SocketTimeoutException異常。Netty捕獲這個異常并繼續處理循環。在下一次EventLoop運行時,將再嘗試一次前面的邏輯。這是一個像Netty的異步框架能夠支持OIO的唯一方式。
我們通過OioSocketChannel的讀操作來了解下關于上面描述的源碼實現:
??這個讀操操作如果拋出超時異常,則會返回讀到的字節數為0。這里大家可以關注另外一點,在當socket關閉是,返回時可讀字節數為-1。這個是和NIO的模式相一致的,在NIO中如果read返回的可讀字節數為-1時,也就表示當遠端連接已經關閉了。
用于JVM內部通訊的本地傳輸
Netty提供了一個本地傳輸用于客戶端和服務端在相同JVM的異步通訊。
在該傳輸中,一個同服務端Channel關聯的SocketAddress不會綁定到一個物理網絡地址;當然,它會被保存到一個注冊表在服務端運行的期間,并在Channel ( 這里指服務端的channel )關閉時被注銷。所以傳輸沒有通過真實的網絡傳輸,所以它不能通過其他傳輸的實現來進行交互 ( 也就是不能同其他傳輸,如NIO transport 進行數據的傳輸交互 )。所以客戶端希望連接一個在同一JVM的使用了該傳輸方式的服務端,那么客戶端也需要使用該傳輸方式。除了這個限制,它與其他傳輸方式并無不同。
內嵌的傳輸協議
Netty提供了一個附加的傳輸方式,該傳輸方式允許你一個ChannelHandler作為輔助類嵌入到其他ChannelHandler中。照這樣,你能在不修改內部代碼的情況下夠擴展一個ChannelHandler的功能。
傳輸協議使用場景
并不是所有的傳輸方式都支持所有的傳輸協議。這里是你可能會遇到的使用場景:
- 非阻塞代碼庫 —— 如果你不要一個阻塞調用在你的代碼庫中,或者你能夠限制它們,在Linux上使用NIO或epoll經常是個好主意。當NIO/epoll 用于處理許多并發的連接,它也能通過更少的線程來更好的工作,尤其是在連接間共享線程的方式。
- 阻塞代碼庫 —— 正如我們已經說到的,如果你的代碼庫嚴重依賴于阻塞I/O,并且你的應用有對應于此的設計。如果你直接轉為Netty的NIO傳輸方式,你可能會遇到阻塞操作問題。對比與重寫你的代碼去完成這些,考慮一個階段性的遷移:從OIO開始,然后轉移到NIO(或epoll如果你在Linux上)當你改進你的代碼后。
- 相同JVM的內部通訊 —— 在相同JVM的內部通訊不需要暴露一個服務在網絡表現層,在相同JVM的內部通訊為本地傳輸的完美使用情況。這將消除真實網絡操作的所有開銷,同時仍然使用你的Netty代碼庫。如果需要暴露一個服務在網絡上,你只需要簡單的修改傳輸方式為NIO或OIO。
- 測試你的ChannelHandler的實現 —— 如果你想要寫單元測試用于你的ChannelHandler實現,考慮使用內嵌的傳輸方式。這將使測試你的代碼變得簡單,而不需創建許多的mock對象。你的類將仍然遵循通用API的事件流,保證ChannelHandler將在真實傳輸中正確工作。
后記
本文主要對Netty的支持的傳輸協議進行了介紹。即便是不同的傳輸協議,Netty也為我們提供了一致的API接口,它將大量復雜的處理邏輯封裝在了源碼實現中,為用戶提供了簡易且方便的API接口,這也是Netty設計一致性的例子之一。
若文章有任何錯誤,望大家不吝指教:)