點(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 中組合了 ChannelPipeline 和 ChannelConfig。ChannelConfig 保存了 Channel 所有的配置,并且支持熱更新。
由于 Channel 是獨(dú)一無(wú)二的,所以 Channel 繼承了 java.lang.Comparable 接口,從而保證對(duì)象的有序。因此,如果兩個(gè) Channel 實(shí)例返回相同的 hash 值,那 AbstractChannel 中 compareTo() 的實(shí)現(xiàn)將會(huì)拋出 Error。

ChannelHandler 常用于:
- 轉(zhuǎn)換數(shù)據(jù)格式。
- 提供異常通知。
- 提供
Channel被激活或者關(guān)閉的通知。 - 提供
Channel從EventLoop中注冊(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。
除了可以使用 ChannelPipeline 和 ChannelConfig 之外,你還可以使用 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ù)流。

零拷貝
零拷貝這個(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

你可能會(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 |