Netty實戰四之傳輸
流經網路的數據總是具有相同的類型:位元組(網路傳輸——一個幫助我們抽象底層數據傳輸機制的概念)
Netty為它所有的傳輸實現提供了一個通用的API,即我們可以將時間花在其他更有成效的事情上。
我們將通過一個案例來對傳輸進行學習,應用程序只簡單地接收連接,向客戶端寫 「Hi!」 ,然後關閉連接。
1、不通過Netty使用OIO和NIO
先介紹JDK API的應用程序的阻塞(OIO)版本和非同步(NIO)版本。
這段代碼完全可以處理中等數量的並發客戶端,但是隨著應用程序變得流行起來,你會發現它並不能很好地伸縮到支撐成千上萬的並發連入連接。你決定改用非同步網路編程,但是很快就發現非同步API是完全不同的,以至於現在你不得不重寫你的應用程序。
雖然這段代碼所做的事情與之前的版本完全相同,但是代碼卻截然不同,如果為了用於非阻塞I/O而重新實現這個簡單的應用程序,都需要一次完全重寫的話,那麼不難想像,移植真正複雜的應用程序需要付出什麼樣的努力!
2、通過Netty使用OIO和NIO
3、非阻塞的Netty版本
因為Netty為每種傳輸的實現都暴露了相同的API,所以無論選用哪一種傳輸的實現,你的代碼都仍然幾乎不受影響,在所有的情況下,傳輸的實現都依賴於interface Channel、ChannelPipeline和ChannelHandler。
4、傳輸API
傳輸API的核心是interface Channel ,它被用於所有的I/O操作。Channel類的層次結構如圖4-1(Channel介面的層次結構)所示。
如圖所示,每個Channel都將會將分配一個ChannelPipeline和ChannelConfig。ChannelConfig包含了該Channel的所有配置設置,並且支持熱更新。
由於特定的傳輸可能具有獨特的設置,所以它可能會實現一個ChannelConfig的子類型。
由於Channel是獨一無二的,所以為了保證順序將Channel聲明為java.lang.Comparable的子介面。因此,如果兩個不同的Channel實例都返回相同的散列碼,那麼AbstractChannel中的compareTo()方法的實現將會拋出一個Error。
ChannelPiepeline持有所有將應用於入站和出站數據以及事件的ChannelHandler實例,這些ChannelHandler實現了應用程序用於處理狀態變化以及數據處理的邏輯。
ChannelHandler的典型用途包括:
-將數據從一種格式轉換為另一種格式
-提供異常的通知
-提供Channel變為活動的或者非活動的通知
-提供當Channel註冊到EventLoop或者從EventLoop註銷時的通知
-提供有關用戶自定義事件的通知
攔截過濾器 ChannelPipeline實現了一種常見的設計模式——攔截過濾器(InterceptingFilter)。UNIX管道是另外一個熟悉的例子:多個命令被鏈接在一起,其中一個命令的輸出端將連接到命令行中下一個命令的輸入端。
你也可以根據需要通過添加或者移除ChannelHandler實例來修改ChannelPipeline。通過利用Netty的這項能力可以構建出高度靈活的應用程序。例如,每當STARTTLS協議被請求時,你可以簡單地通過向ChannelPipeline添加一個適當的ChannelHandler(SslHandler)來按需地支持STARTTLS協議。
考慮一下寫數據並將其沖刷到遠程節點這樣的常規任務,代碼清單4-5演示了使用Channel.writeAndFlush()來實現這一目的。
Netty的Channel實現是線程安全的,因此你可以存儲一個到Channel的引用,並且每當你需要向遠程節點寫數據時,都可以使用它,即使當時許多線程都在使用它。代碼清單4-6展示了一個多線程寫數據的簡單例子,需要注意的是,消息將會被保證按順序發送的。
5、內置的傳輸
Netty內置了一些可開箱即用的傳輸,因為並不是它們所有的傳輸都支持每一種協議,所以你必須選擇一個和你的應用程序所使用的協議相容的傳輸。 下表顯示了所有Netty提供的傳輸
6、NIO——非阻塞I/O
NIO提供了一個所有I/O操作的全非同步的實現。它利用了自NIO子系統被引入JDK1.4時便可用的基於選擇器的API。
選擇器背後的基本概念是充當一個註冊表,在那裡你將可以請求在Channel的狀態發生變化時得到通知。
-新的Channel已被接受並且就緒
-Channel連接已經完成
-Channel有已經就緒的可供讀取的數據
-Channel可用於寫數據
選擇器運行在一個檢查狀態變化並對其做出相應響應的線程上,在應用程序對狀態的改變做出響應之後,選擇器將會被重置,並將重複這個過程。
零拷貝(zero-copy)是一種目前只有在使用NIO和Epoll傳輸時才可使用的特性。它使你可以快速高效地將數據從文件系統移動到網路介面,而不需要將其從內核空間複製到用戶空間,其在像FTP或者HTTP這樣的協議中可以顯著地提升性能。但是,並不是所有的操作系統都支持這一特性。特別地,它對於實現了數據加密或者壓縮的文件系統是不可用的——只能傳輸文件的原始內容。反過來說,傳輸已被加密的文件則不是問題。
7、Epoll——用於Linux的本地非阻塞傳輸
Linux作為高性能網路編程的平台,其重要性與日俱增,這催生了大量先進特性的開發,其中包括Epoll——一個高度可擴展的I/O事件通知特性,這個API自Linux內核版本2.5.44被引入,提供了比舊的POSIX select和poll系統調用更好的性能,同時現在也是Linux上非阻塞網路編程的事實標準。Linux JDK NIO API使用了這些epoll調用。
Netty為Linux提供了一組NIO API,其以一種和它本身的設計更加一致的方式使用epoll,並且以一種更加輕量的方式使用中斷,如果你的應用程序旨在運行於Linux系統,那麼請考慮利用這個版本的傳輸,你將發現在高負載下它的性能要優於JDK的NIO實現。
8、OIO——舊的阻塞I/O
Netty的OIO傳輸實現代表了一種折中:它可以通過常規的傳輸API使用,但是由於它是建立在java.net包的阻塞實現上的,所以他不是非同步的。
例如,你可能需要移植使用了一些進行阻塞調用的庫(如JDBC)的遺留代碼,而將邏輯轉換為非阻塞的可能也是不切實際。相反,你可以在短期內使用Netty的OIO傳輸,然後再將你的代碼移植到純粹的非同步傳輸上。
在 java.net API中,你通常會有一個用來接受到達正在監聽的ServerSocket的新連接的線程。會創建一個新的和遠程節點進行交互的套接字,並且會分配一個新的用於處理響應通信流量的線程。這是必需的,因為某個指定套接字上的任何I/O操作在任意的時間點上都可能會阻塞。使用單個線程來處理多個套接字,很容易導致一個套接字上的阻塞操作也捆綁了所有其他的套接字。
Netty是如何能夠使用和用於非同步傳輸相同的API支持OIO的呢?Netty利用了SO_TIMEOUT這個Socket標誌,它指定了等待一個I/O操作完成的最大毫秒數。如果操作在指定的時間間隔內沒有完成,則將會拋出一個SocketTimeout Exception。Netty將捕獲這個異常並繼續處理循環。在EventLoop下一次運行時,它將再次嘗試,這也是Netty這樣的非同步框架能夠支持OIO的唯一方式。
9、用於JVM內部通信的Local傳輸
Netty提供了一個Local傳輸,用於在同一個JVM中運行的客戶端和伺服器程序之間的非同步通信,且也支持對於所有Netty傳輸實現都共同的API。
在這個傳輸中,和伺服器Channel相關聯的SocketAddress並沒有綁定物理網路地址;相反,只要伺服器還在運行,它就會被存儲在註冊表裡,並在Channel關閉時註銷。因為這個傳輸並不接受真正的網路流量,所以它並不能夠和其他傳輸實現進行互操作。因此,客戶端希望連接到(在同一個JVM中)使用了這個傳輸的伺服器端時也必須使用它。除了這個限制,它的使用方式和其他傳輸一模一樣。
10、Embedded傳輸
Netty提供了一種額外的傳輸,使得你可以將一組ChannelHandler作為幫助器類嵌入到其他的ChannelHandler內部。通過這種方式,你將可以擴展一個CHannelHandler的功能,而又不需要修改其內部代碼。
11、傳輸的用例
在Linux上啟用SCTP
SCTP需要內核的支持,並且需要安裝用戶庫
例如,對於Ubuntn,可以使用下面的命令
sudo apt-get install libsctpl
對於Fedora,可以使用yum
sudo yum install kernel-modules-extra.x86_64 lksctp-tools.x86_64
有關如何啟用SCTP的詳細信息,請參考你的Linux發行版的文檔。
雖然只有SCTP傳輸有這些特殊要求,但是其他傳輸可能也有它們自己的配置選項需要考慮。此外,如果只是為了支持更高的並發連接數,伺服器平台可能需要配置得和客戶端不一樣。
——非阻塞代碼庫:如果你的代碼庫中沒有阻塞調用(或者你能夠限制它們的範圍),那麼在Linux上使用NIO或者epoll始終是個好主意。雖然NIO/Epoll旨在處理大量的並發連接,但是在處理較小數目的並發連接時,它也能很好地工作,尤其是考慮到它在連接之間共享線程的方式。
——阻塞代碼庫:如果你的代碼庫嚴重地依賴於阻塞I/O,而且你的應用程序也有一個相應的設計,那麼在你嘗試將其直接轉換為Netty的NIO傳輸時,你將可能會遇到和阻塞操作相關的問題。不要為此而重寫你的代碼,可以考慮分階段遷移:先從OIO開始,等你的代碼修改好之後,在遷移到NIO(或者EPoll,如果你在使用Linux)
——在同一個JVM內部的通信:同一個JVM內部的通信,不需要通過網路暴露服務,是Local傳輸的完美用例。這將消除所有真實網路操作的開銷,同時仍然使用你的Netty代碼庫。如果隨後需要通過網路暴露服務,那麼你將只需要把傳輸改為NIO或者OIO即可。
——測試你的ChannelHandler實現:如果你想要為自己的ChannelHandler實現編寫單元測試,那麼請考慮使用Embedded傳輸。這既便於測試你的代碼,而又不需要創建大量的模擬對象。你的類將仍然符合常規API事件流,保證該Channelhandler在和真實的傳輸一起使用時能夠正確地工作。


TAG:Java貓說 |