04 傳輸

點(diǎn)擊查看 《Netty in Action》筆記目錄。

本文是對(duì)《Netty in Action》第4章內(nèi)容的筆記和翻譯,主要內(nèi)容包括:

  • OIO:阻塞傳輸
  • NIO:異步傳輸
  • 本地傳輸:和 JVM 異步交互
  • 測(cè)試你的 ChannelHandler

案例學(xué)習(xí):傳輸遷移

不通過(guò) Netty 使用 OIO 和 NIO

我們將要展示基于 JDK API 的阻塞(OIO)和異步(NIO)應(yīng)用版本。下面展示了阻塞實(shí)現(xiàn)版本。


下面展示了非阻塞版本。


通過(guò) Netty 使用 OIO 和 NIO

非阻塞 Netty 版本

因?yàn)?Netty 對(duì)每一種傳輸實(shí)現(xiàn)都暴露了相同的 API,所以無(wú)論你采用哪一種傳輸,你的代碼幾乎不用修改。

傳輸 API

傳輸 API 的核心是 Channel 接口,所有的 I/O 操作都會(huì)使用這個(gè)接口。Channel 類(lèi)的繼承關(guān)系如圖4.1所示。

Channel 中組合了 ChannelPipelineChannelConfigChannelConfig 保存了 Channel 所有的配置,并且支持熱更新。

由于 Channel 是獨(dú)一無(wú)二的,所以 Channel 繼承了 java.lang.Comparable 接口,從而保證對(duì)象的有序。因此,如果兩個(gè) Channel 實(shí)例返回相同的 hash 值,那 AbstractChannelcompareTo() 的實(shí)現(xiàn)將會(huì)拋出 Error。

圖4.1 Channel接口繼承關(guān)系

ChannelHandler 常用于:

  • 轉(zhuǎn)換數(shù)據(jù)格式。
  • 提供異常通知。
  • 提供 Channel 被激活或者關(guān)閉的通知。
  • 提供 ChannelEventLoop 中注冊(cè)和取消注冊(cè)的通知。
  • 提供用戶定義事件的通知。

攔截過(guò)濾器
ChannelPipeline 實(shí)現(xiàn)了一個(gè)常用設(shè)計(jì)模式:攔截過(guò)濾器。UNIX 管道是這個(gè)模式的另一個(gè)典型例子:命令被鏈?zhǔn)骄幣?,每個(gè)命令的輸出會(huì)作為接下來(lái)一個(gè)命令的輸入。

你同樣可以在運(yùn)行時(shí)按照你的需求為 ChannelPipeline 添加或刪除 ChannelHandler。

除了可以使用 ChannelPipelineChannelConfig 之外,你還可以使用 Channel 的方法,下表展示了一些常用的方法。

方法名稱 描述
eventLoop 返回與 Channel 綁定的 EventLoop。
pipeline 返回與 Channel 綁定的 ChannelPipeline。
isActive 如果 Channel 是激活(active)的,返回 true。 Active 的含義取決于下層的傳輸協(xié)議。例如, Socket 中的 active 是指與遠(yuǎn)端建立連接;Datagram 中的 active 是指 Datagram 打開(kāi)。
localAddress 返回本地的 SocketAddress.
remoteAddress 返回遠(yuǎn)端的 SocketAddress.
write 將數(shù)據(jù)寫(xiě)到遠(yuǎn)端。數(shù)據(jù)會(huì)經(jīng)過(guò) ChannelPipeline,并將會(huì)進(jìn)行排隊(duì),直到刷新時(shí)才會(huì)發(fā)送。
flush 將之前寫(xiě)的數(shù)據(jù)刷新到底層的傳輸協(xié)議中,例如:Socket
writeAndFlush 先調(diào)用 write() ,然后調(diào)用 flush()。

下圖展示了如何利用 Channel.writeAndFlush() 實(shí)現(xiàn)最常見(jiàn)的發(fā)送數(shù)據(jù)到遠(yuǎn)端的任務(wù)。

Netty 的 Channel 實(shí)現(xiàn)是線程安全的,所以你可以存儲(chǔ) Channel 的一個(gè)引用,并且在你需要發(fā)送數(shù)據(jù)的時(shí)候使用它,即使在多線程環(huán)境中也沒(méi)有關(guān)系。

包含的傳輸

下表展示了 Netty 中提供的傳輸。

名稱 所在的包 描述
NIO io.netty.channel.socket.nio java.nio.channels 包為基礎(chǔ),是一個(gè)基于 selector 的實(shí)現(xiàn)方案。
Epoll io.netty.channel.epoll 通過(guò) JNI 使用 epoll() 進(jìn)入非阻塞 IO。這個(gè)傳輸?shù)奶匦灾贿m用于 Linux,如:SO_REUSEPORT。它比 NIO 傳輸要快,是完全非阻塞。
OIO io.netty.channel.socket.oio 使用 java.net 包作為基礎(chǔ),使用阻塞流。
Local io.netty.channel.local 一個(gè)本地傳輸,可以被用來(lái)通過(guò)管道與 VM 交互。
Embedded io.netty.channel.embedded 一個(gè)嵌入式傳輸協(xié)議。通過(guò)它,你可以在虛擬的傳輸協(xié)議上使用 ChannelHandler。這在測(cè)試 ChannelHandler 實(shí)現(xiàn)時(shí)很有用。

NIO — 非阻塞 I/O

Selector 背后基本的原理是:提供一個(gè)注冊(cè)器,通過(guò)注冊(cè)感興趣的事件,當(dāng) Channel 狀態(tài)改變的時(shí)候你可以得到通知??赡艿臓顟B(tài)改變包括:

  • 新的 Channel 被接收到并且已經(jīng)準(zhǔn)備好。
  • Channel 連接已經(jīng)完成。
  • Channel 已經(jīng)準(zhǔn)備好為讀操作提供數(shù)據(jù)。
  • Channel 已經(jīng)準(zhǔn)備好,可以寫(xiě)入數(shù)據(jù)了。

下表展示了 java.nio.channels.SelectionKey 類(lèi)定義的一些模式。

方法名稱 描述
OP_ACCEPT 當(dāng)一個(gè)新的請(qǐng)求被接受并且 Channel 被創(chuàng)建的時(shí)候,會(huì)進(jìn)行通知。
OP_CONNECT 當(dāng)連接建立的時(shí)候,會(huì)進(jìn)行通知。
OP_READ 當(dāng)可以從 Channel 中讀取數(shù)據(jù)的時(shí)候,會(huì)進(jìn)行通知。
OP_WRITE 當(dāng)可以往 Channel 中寫(xiě)入更多數(shù)據(jù)的時(shí)候,會(huì)進(jìn)行通知。這個(gè)事件在 socket 緩存被填滿的時(shí)候發(fā)生,通常意味著數(shù)據(jù)發(fā)送的速率操過(guò)了遠(yuǎn)端的處理能力。

NIO 內(nèi)部實(shí)現(xiàn)的細(xì)節(jié),在所有的 Netty 用戶級(jí)別的 API 中都被隱藏了。圖4.2 展示了處理的數(shù)據(jù)流。

圖4.2 選擇和處理狀態(tài)改變

零拷貝
零拷貝這個(gè)特性目前只在 NIO 和 Epoll 傳輸中被支持。通過(guò)它可以使你快速并高效地從一個(gè)文件系統(tǒng)轉(zhuǎn)移數(shù)據(jù)到網(wǎng)絡(luò)中,不需要從內(nèi)核空間拷貝到用戶空間,這大大提高了 FTP、HTTP 等協(xié)議的傳輸效率。這個(gè)特性并不是被所有的操作系統(tǒng)支持。確切地說(shuō),它并不適用于采用加密或壓縮的文件系統(tǒng),只有文件的原始數(shù)據(jù)可以被轉(zhuǎn)移。但對(duì)于已經(jīng)加密好的文件是可以被傳輸?shù)摹?/p>

Epoll — 針對(duì) Linux 的原生非阻塞傳輸

Netty 為 Linux 系統(tǒng)提供了一個(gè)使用 epoll 的 NIO API。在某種程度上,這與 Netty 本身的設(shè)計(jì)更加一致,并且比中斷的實(shí)現(xiàn)方式開(kāi)銷(xiāo)更少。如果你的應(yīng)用準(zhǔn)備在 Linux 上使用,那么可以考慮這個(gè)版本,你會(huì)發(fā)現(xiàn)在高負(fù)載的情況下,它的性能表現(xiàn)會(huì)好于 JDK 本生的 NIO 實(shí)現(xiàn)。

這個(gè)傳輸和圖4.2中展示的傳輸語(yǔ)義相同,并且它的使用是很簡(jiǎn)單的,可以參考 list 4.4。為了使用 epoll 版的 NIO,可以將 NioEventLoopGroup 替換為 EpollEventLoopGroup,將 NioServerSocketChannel.class 替換為 EpollServerSocketChannel.class。

OIO — 阻塞型 I/O

圖4.3 OIO 處理邏輯

你可能會(huì)對(duì)此感到好奇:Netty 是如何通過(guò)相同的 API 來(lái)支持異步傳輸?shù)?NIO?答案是 Netty 使用了 SO_TIMEOUT Socket 標(biāo)志。 這個(gè)標(biāo)志表明等待 I/O 操作完成的最大毫秒數(shù)。如果操作在指定的時(shí)間間隔內(nèi)沒(méi)有完成,那么將會(huì)拋出 SocketTimeoutException。Netty 會(huì)捕捉這個(gè)異常,并繼續(xù)循環(huán)處理。

Local — 通過(guò)本地傳輸與 JVM 交互

Netty 為同一個(gè) JVM 上的客戶端和服務(wù)端的異步交互提供了一個(gè)本地傳輸。

因?yàn)楸镜貍鬏敳](méi)有真正的網(wǎng)絡(luò)傳輸,所以它不能和其它傳輸協(xié)議一起協(xié)作。因此,一個(gè)客戶端如果想通過(guò)這個(gè)傳輸來(lái)連接服務(wù)端的話(在同一個(gè) JVM 上),服務(wù)端也必須使用這個(gè)傳輸。除了這個(gè)限制,它的使用和其它傳輸協(xié)議是一樣的。

嵌入傳輸

Netty 還提供了一個(gè)傳輸,可以允許你嵌入在 ChannelHandler 中嵌入一個(gè)幫助類(lèi) ChannelHandler。在這種模式下,你可以擴(kuò)展 ChannelHandler 的功能,而不用內(nèi)部的代碼。

傳輸使用例子

下表展示了當(dāng)前版本中傳輸支持的協(xié)議。

Transport TCP UDP SCTP UDT
NIO 支持 支持 支持 支持
Linux 上的 Epoll 支持 支持 - -
OIO 支持 支持 支持

在 Linux 上開(kāi)啟 SCTP
SCTP 需要內(nèi)核支持并且需要安裝一些用戶庫(kù)。例如,在 Ubuntu 上可以使用下面的命令:

# sudo apt-get install libsctp1

在 Fedora 上,你需要使用 yum:

# sudo yum install kernel-modules-extra.x86_64 lksctp-tools.x86_64

請(qǐng)閱讀你的 Linux 發(fā)布版本的手冊(cè),了解如何開(kāi)啟 SCTP。

下面是你可能會(huì)遇到的一些用例。

  • 基于非阻塞的代碼:如果你在你的代碼中不阻塞調(diào)用,或者你要限制阻塞的調(diào)用,推薦使用 NIO(在 Linux 上使用 epoll)。NIO/epoll 主要是用于處理很多并發(fā)的連接,在數(shù)量較少的時(shí)候變現(xiàn)的也不錯(cuò),尤其是在多個(gè)連接共享線程的情況下。
  • 基于阻塞的代碼:正如我們之前提過(guò),如果你的代碼很依賴阻塞 I/O,并且你應(yīng)用具有相應(yīng)的設(shè)計(jì),那么你直接從阻塞 I/O 切換到 Netty 的 NIO 傳輸,可能會(huì)遇到麻煩。最好不要直接重寫(xiě)代碼來(lái)適應(yīng)新的 I/O, 可以考慮一個(gè)階段性的遷移:從 OIO 開(kāi)始,遷移到 NIO(或者你使用 Linux 的話就是 epoll)。
  • 在同一個(gè) JVM 上進(jìn)行交互:如果要在同一個(gè) JVM 上進(jìn)行交互不需要使用網(wǎng)絡(luò),使用本地傳輸再恰當(dāng)不過(guò)了。這可以在使用 Netty 代碼的同時(shí)消除真實(shí)網(wǎng)絡(luò)的開(kāi)銷(xiāo)。如果你需要在真實(shí)網(wǎng)絡(luò)上使用該服務(wù),你只需將傳輸替換為 NIO 或者 OIO。
  • 測(cè)試你的 ChannelHandler 實(shí)現(xiàn):如果你想為你的 ChannelHandler 實(shí)現(xiàn)寫(xiě)單元測(cè)試,可以考慮使用嵌入傳輸。這可以使得你在測(cè)試實(shí)現(xiàn)的時(shí)候不用創(chuàng)建很多的模擬對(duì)象。你寫(xiě)的類(lèi)還是會(huì)和通用的 API 事件流保持一致,保證 ChannelHandler 在真實(shí)傳輸上表現(xiàn)正確。

下表總結(jié)了我們提到過(guò)的用例。

應(yīng)用需求 推薦使用的協(xié)議
非阻塞或者普通的嘗試 NIO(或者在 Linux 上使用 epoll)
阻塞 OIO
在同一個(gè) JVM 上交互 Local
測(cè)試你的 ChannelHandler 實(shí)現(xiàn) Embedded
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容