本章主要內容
- OIO——阻塞傳輸
- NIO——異步傳輸
- Local——JVM內部的異步通信
- Embedded——測試你的
ChannelHandler
流經網絡的數據總是具有相同的類型:字節。這些字節是如何流動的主要取決於我們所說的網絡傳輸——一個幫助我們抽像底層數據傳輸機制的概念。用戶並不關心這些細節;他們只想確保他們的字節被可靠地發送和接收。
如果你有Java網絡編程的經驗,那麼你可能已經發現,在某些時候,你需要支撐比預期多很多的並發連接。如果你隨後嘗試從阻塞傳輸切換到非阻塞傳輸,那麼你可能會因為這兩種網絡API的截然不同而遇到問題。
然而,Netty為它所有的傳輸實現提供了一個通用API,這使得這種轉換比你直接使用JDK所能夠達到的簡單得多。所產生的代碼不會被實現的細節所污染,而你也不需要在你的整個代碼庫上進行廣泛的重構。簡而言之,你可以將時間花在其他更有成效的事情上。
在本章中,我們將學習這個通用API,並通過和JDK的對比來證明它極其簡單易用。我們將闡述Netty自帶的不同傳輸實現,以及它們各自適用的場景。有了這些信息,你會發現選擇最適合於你的應用程序的選項將是直截了當的。
本章的唯一前提是Java編程語言的相關知識。有網絡框架或者網絡編程相關的經驗更好,但不是必需的。
我們先來看一看傳輸在現實世界中是如何工作的。
4.1 案例研究:傳輸遷移
我們將從一個應用程序開始我們對傳輸的學習,這個應用程序只簡單地接受連接,向客戶端寫「Hi!」,然後關閉連接。
4.1.1 不通過Netty使用OIO和NIO
我們將介紹僅使用了JDK API的應用程序的阻塞(OIO)版本和異步(NIO)版本。代碼清單4-1展示了其阻塞版本的實現。如果你曾享受過使用JDK進行網絡編程的樂趣,那麼這段代碼將喚起你美好的回憶。
代碼清單4-1 未使用Netty的阻塞網絡編程
public class PlainOioServer {
public void serve(int port) throws IOException {
final ServerSocket socket = new ServerSocket(port); ← -- 將服務器綁定到指定端口
try {
for (;;) {
final Socket clientSocket = socket.accept; ← -- 接受連接
System.out.println(
"Accepted connection from " + clientSocket);
new Thread(new Runnable { ← -- 創建一個新的線程來處理該連接
@Override
public void run {
OutputStream out;
try {
out = clientSocket.getOutputStream;
out.write("Hi!\r\n".getBytes( ← -- 將消息寫給已連接的客戶端
Charset.forName("UTF-8")));
out.flush;
clientSocket.close; ← -- 關閉連接
}
catch (IOException e) {
e.printStackTrace;
}
finally {
try {
clientSocket.close;
}
catch (IOException ex) {
// ignore on close
}
}
}
}).start; ← -- 啟動線程
}
}
catch (IOException e) {
e.printStackTrace;
}
}
}
這段代碼完全可以處理中等數量的並發客戶端。但是隨著應用程序變得流行起來,你會發現它並不能很好地伸縮到支撐成千上萬的並發連入連接。你決定改用異步網絡編程,但是很快就發現異步API是完全不同的,以至於現在你不得不重寫你的應用程序。
其非阻塞版本如代碼清單4-2所示。
代碼清單4-2 未使用Netty的異步網絡編程
public class PlainNioServer {
public void serve(int port) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open;
serverChannel.configureBlocking(false);
ServerSocket ssocket = serverChannel.socket;
InetSocketAddress address = new InetSocketAddress(port);
ssocket.bind(address); ← -- 將服務器綁定到選定的端口
Selector selector = Selector.open; ← -- 打開Selector來處理Channel
serverChannel.register(selector, SelectionKey.OP_ACCEPT); ← -- 將ServerSocket註冊到Selector 以接受連接
final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes);
for (;;) {
try {
selector.select; ← -- 等待需要處理的新事件;阻塞將一直持續到下一個傳入事件
} catch (IOException ex) {
ex.printStackTrace;
// handle exception
break;
}
Set<SelectionKey> readyKeys = selector.selectedKeys; ← -- 獲取所有接收事件的Selection-Key 實例
Iterator<SelectionKey> iterator = readyKeys.iterator;
while (iterator.hasNext) {
SelectionKey key = iterator.next;
iterator.remove;
try {
if (key.isAcceptable) { ← -- 檢查事件是否是一個新的已經就緒可以被接受的連接
ServerSocketChannel server =
(ServerSocketChannel)key.channel;
SocketChannel client = server.accept;
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_WRITE | ← -- 接受客戶端,並將它註冊到選擇器
SelectionKey.OP_READ, msg.duplicate);
System.out.println(
"Accepted connection from " + client);
}
if (key.isWritable) { ← -- 檢查套接字是否已經準備好寫數據
SocketChannel client =
(SocketChannel)key.channel;
ByteBuffer buffer =
(ByteBuffer)key.attachment;
while (buffer.hasRemaining) {
if (client.write(buffer) == 0) { ← -- 將數據寫到已連接的客戶端
break;
}
}
client.close; ← -- 關閉連接
}
} catch (IOException ex) {
key.cancel;
try {
key.channel.close;
} catch (IOException cex) {
// ignore on close
}
}
}
}
}
}
如同你所看到的,雖然這段代碼所做的事情與之前的版本完全相同,但是代碼卻截然不同。如果為了用於非阻塞I/O而重新實現這個簡單的應用程序,都需要一次完全的重寫的話,那麼不難想像,移植真正複雜的應用程序需要付出什麼樣的努力。
鑒於此,讓我們來看看使用Netty實現該應用程序將會是什麼樣子吧。
4.1.2 通過Netty使用OIO和NIO
我們將先編寫這個應用程序的另一個阻塞版本,這次我們將使用Netty框架,如代碼清單4-3所示。
代碼清單4-3 使用Netty的阻塞網絡處理
public class NettyOioServer {
public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
EventLoopGroup group = new OioEventLoopGroup;
try {
ServerBootstrap b = new ServerBootstrap; ← -- 創建Server-Bootstrap
b.group(group)
.channel(OioServerSocketChannel.class) ← -- 使用OioEventLoopGroup以允許阻塞模式(舊的I/O)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel> { ← -- 指定Channel-Initializer,對於每個已接受的連接都調用它
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline.addLast(
new ChannelInboundHandlerAdapter { ← -- 添加一個Channel-InboundHandler-Adapter 以攔截和處理事件
@Override
public void channelActive(
ChannelHandlerContext ctx)
throws Exception {
ctx.writeAndFlush(buf.duplicate)
.addListener(
ChannelFutureListener.CLOSE); ← -- 將消息寫到客戶端,並添加ChannelFutureListener,以便消息一被寫完就關閉連接
}
});
}
});
ChannelFuture f = b.bind.sync; ← -- 綁定服務器以接受連接
f.channel.closeFuture.sync;
} finally {
group.shutdownGracefully.sync; ← -- 釋放所有的資源
}
}
}
接下來,我們使用Netty和非阻塞I/O來實現同樣的邏輯。
4.1.3 非阻塞的Netty版本
代碼清單4-4和代碼清單4-3幾乎一模一樣,除了高亮顯示的那兩行。這就是從阻塞(OIO)傳輸切換到非阻塞(NIO)傳輸需要做的所有變更。
代碼清單4-4 使用Netty的異步網絡處理
public class NettyNioServer {
public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.copiedBuffer("Hi!\r\n",
Charset.forName("UTF-8"));
EventLoopGroup group = new NioEventLoopGroup; ← -- 為非阻塞模式使用NioEventLoopGroup
try {
ServerBootstrap b = new ServerBootstrap; ← -- 創建ServerBootstrap
b.group(group).channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer { ← -- 指定Channel-Initializer,對於每個已接受的連接都調用它
@Override
public void initChannel(SocketChannel ch)
throws Exception{
ch.pipeline.addLast(
new ChannelInboundHandlerAdapter { ← -- 添加ChannelInbound-HandlerAdapter 以接收和處理事件
@Override
public void channelActive(
ChannelHandlerContext ctx) throws Exception { ← -- 將消息寫到客戶端,並添加ChannelFutureListener,以便消息一被寫完就關閉連接
ctx.writeAndFlush(buf.duplicate)
.addListener(
ChannelFutureListener.CLOSE);
}
});
}
});
ChannelFuture f = b.bind.sync; ← -- 綁定服務器以接受連接
f.channel.closeFuture.sync;
} finally {
group.shutdownGracefully.sync; ← -- 釋放所有的資源
}
}
}
因為Netty為每種傳輸的實現都暴露了相同的API,所以無論選用哪一種傳輸的實現,你的代碼都仍然幾乎不受影響。在所有的情況下,傳輸的實現都依賴於interface Channel
、ChannelPipeline
和ChannelHandler
。
在看過一些使用基於Netty的傳輸的這些優點之後,讓我們仔細看看傳輸API本身。
4.2 傳輸API
傳輸API的核心是interface
Channel
,它被用於所有的I/O操作。Channel
類的層次結構如圖4-1所示。
圖4-1 Channel
接口的層次結構
如圖所示,每個Channel
都將會被分配一個ChannelPipeline
和ChannelConfig
。ChannelConfig
包含了該Channel
的所有配置設置,並且支持熱更新。由於特定的傳輸可能具有獨特的設置,所以它可能會實現一個ChannelConfig
的子類型。(請參考ChannelConfig
實現對應的Javadoc。)
由於Channel
是獨一無二的,所以為了保證順序將Channel
聲明為java.lang.Comparable
的一個子接口。因此,如果兩個不同的Channel
實例都返回了相同的散列碼,那麼AbstractChannel
中的compareTo
方法的實現將會拋出一個Error
。
ChannelPipeline
持有所有將應用於入站和出站數據以及事件的ChannelHandler
實例,這些ChannelHandler
實現了應用程序用於處理狀態變化以及數據處理的邏輯。
ChannelHandler
的典型用途包括:
- 將數據從一種格式轉換為另一種格式;
- 提供異常的通知;
- 提供
Channel
變為活動的或者非活動的通知; - 提供當
Channel
註冊到EventLoop
或者從EventLoop
註銷時的通知; - 提供有關用戶自定義事件的通知。
攔截過濾器
ChannelPipeline
實現了一種常見的設計模式——攔截過濾器(Intercepting Filter)。UNIX管道是另外一個熟悉的例子:多個命令被鏈接在一起,其中一個命令的輸出端將連接到命令行中下一個命令的輸入端。
你也可以根據需要通過添加或者移除ChannelHandler
實例來修改ChannelPipeline
。通過利用Netty的這項能力可以構建出高度靈活的應用程序。例如,每當STARTTLS[1]協議被請求時,你可以簡單地通過向ChannelPipeline添加
一個適當的ChannelHandler
(SslHandler
)來按需地支持STARTTLS協議。
除了訪問所分配的ChannelPipeline
和ChannelConfig
之外,也可以利用Channel
的其他方法,其中最重要的列舉在表4-1中。
表4-1 Channel
的方法
方 法 名
描 述
eventLoop
返回分配給Channel
的EventLoop
pipeline
返回分配給Channel
的ChannelPipeline
isActive
如果Channel
是活動的,則返回true
。活動的意義可能依賴於底層的傳輸。例如,一個Socket
傳輸一旦連接到了遠程節點便是活動的,而一個Datagram
傳輸一旦被打開便是活動的
localAddress
返回本地的SokcetAddress
remoteAddress
返回遠程的SocketAddress
write
將數據寫到遠程節點。這個數據將被傳遞給ChannelPipeline
,並且排隊直到它被沖刷
flush
將之前已寫的數據沖刷到底層傳輸,如一個Socket
writeAndFlush
一個簡便的方法,等同於調用write
並接著調用flush
稍後我們將進一步深入地討論所有這些特性的應用。目前,請記住,Netty所提供的廣泛功能只依賴於少量的接口。這意味著,你可以對你的應用程序邏輯進行重大的修改,而又無需大規模地重構你的代碼庫。
考慮一下寫數據並將其沖刷到遠程節點這樣的常規任務。代碼清單4-5演示了使用Channel.writeAndFlush
來實現這一目的。
代碼清單4-5 寫出到Channel
Channel channel = ...
ByteBuf buf = Unpooled.copiedBuffer("your data", CharsetUtil.UTF_8); ← -- 創建持有要寫數據的ByteBuf
ChannelFuture cf = channel.writeAndFlush(buf); ← -- 寫數據並沖刷它
cf.addListener(new ChannelFutureListener { ← -- 添加ChannelFutureListener 以便在寫操作完成後接收通知
@Override
public void operationComplete(ChannelFuture future) {
if (future.isSuccess) { ← -- 寫操作完成,並且沒有錯誤發生
System.out.println("Write successful");
} else {
System.err.println("Write error"); ← -- 記錄錯誤
future.cause.printStackTrace;
}
}
});
Netty的Channel
實現是線程安全的,因此你可以存儲一個到Channel
的引用,並且每當你需要向遠程節點寫數據時,都可以使用它,即使當時許多線程都在使用它。代碼清單4-6展示了一個多線程寫數據的簡單例子。需要注意的是,消息將會被保證按順序發送。
代碼清單4-6 從多個線程使用同一個Channel
final Channel channel = ...
final ByteBuf buf = Unpooled.copiedBuffer("your data",
CharsetUtil.UTF_8).retain; ← -- 創建持有要寫數據的ByteBuf
Runnable writer = new Runnable { ← -- 創建將數據寫到Channel 的Runnable
@Override
public void run {
channel.writeAndFlush(buf.duplicate);
}
};
Executor executor = Executors.newCachedThreadPool; ← -- 獲取到線程池Executor 的引用
// write in one thread
executor.execute(writer); ← -- 遞交寫任務給線程池以便在某個線程中執行
// write in another thread
executor.execute(writer); ← -- 遞交另一個寫任務以便在另一個線程中執行
...
4.3 內置的傳輸
Netty內置了一些可開箱即用的傳輸。因為並不是它們所有的傳輸都支持每一種協議,所以你必須選擇一個和你的應用程序所使用的協議相容的傳輸。在本節中我們將討論這些關係。
表4-2顯示了所有Netty提供的傳輸。
表4-2 Netty所提供的傳輸
名 稱
包
描 述
NIO
io.netty.channel.socket.nio
使用java.nio.channels
包作為基礎——基於選擇器的方式
Epoll[2]
io.netty.channel.epoll
由JNI驅動的epoll
和非阻塞IO。這個傳輸支持只有在Linux上可用的多種特性,如SO_REUSEPORT
,比NIO傳輸更快,而且是完全非阻塞的
OIO
io.netty.channel.socket.oio
使用java.net
包作為基礎——使用阻塞流
Local
io.netty.channel.local
可以在VM內部通過管道進行通信的本地傳輸
Embedded
io.netty.channel.embedded
Embedded傳輸,允許使用ChannelHandler
而又不需要一個真正的基於網絡的傳輸。這在測試你的ChannelHandler
實現時非常有用
我們將在接下來的幾節中詳細討論這些傳輸。
4.3.1 NIO——非阻塞I/O
NIO提供了一個所有I/O操作的全異步的實現。它利用了自NIO子系統被引入JDK 1.4時便可用的基於選擇器的API。
選擇器背後的基本概念是充當一個註冊表,在那裡你將可以請求在Channel
的狀態發生變化時得到通知。可能的狀態變化有:
- 新的
Channel
已被接受並且就緒; Channel
連接已經完成;Channel
有已經就緒的可供讀取的數據;Channel
可用於寫數據。
選擇器運行在一個檢查狀態變化並對其做出相應響應的線程上,在應用程序對狀態的改變做出響應之後,選擇器將會被重置,並將重複這個過程。
表4-3中的常量值代表了由class java.nio.channels.SelectionKey
定義的位模式。這些位模式可以組合起來定義一組應用程序正在請求通知的狀態變化集。
表4-3 選擇操作的位模式
名 稱
描 述
OP_ACCEPT
請求在接受新連接並創建Channel
時獲得通知
OP_CONNECT
請求在建立一個連接時獲得通知
OP_READ
請求當數據已經就緒,可以從Channel
中讀取時獲得通知
OP_WRITE
請求當可以向Channel
中寫更多的數據時獲得通知。這處理了套接字緩衝區被完全填滿時的情況,這種情況通常發生在數據的發送速度比遠程節點可處理的速度更快的時候
對於所有Netty的傳輸實現都共有的用戶級別API完全地隱藏了這些NIO的內部細節。圖4-2展示了該處理流程。
圖4-2 選擇並處理狀態的變化
零拷貝
零拷貝(zero-copy)是一種目前只有在使用NIO和Epoll傳輸時才可使用的特性。它使你可以快速高效地將數據從文件系統移動到網絡接口,而不需要將其從內核空間複製到用戶空間,其在像FTP或者HTTP這樣的協議中可以顯著地提升性能。但是,並不是所有的操作系統都支持這一特性。特別地,它對於實現了數據加密或者壓縮的文件系統是不可用的——只能傳輸文件的原始內容。反過來說,傳輸已被加密的文件則不是問題。
4.3.2 Epoll——用於Linux的本地非阻塞傳輸
正如我們之前所說的,Netty的NIO傳輸基於Java提供的異步/非阻塞網絡編程的通用抽像。雖然這保證了Netty的非阻塞API可以在任何平台上使用,但它也包含了相應的限制,因為JDK為了在所有系統上提供相同的功能,必須做出妥協。
Linux作為高性能網絡編程的平台,其重要性與日俱增,這催生了大量先進特性的開發,其中包括epoll——一個高度可擴展的I/O事件通知特性。這個API自Linux內核版本2.5.44(2002)被引入,提供了比舊的POSIX select
和poll
系統調用[3]更好的性能,同時現在也是Linux上非阻塞網絡編程的事實標準。Linux JDK NIO API使用了這些epoll調用。
Netty為Linux提供了一組NIO API,其以一種和它本身的設計更加一致的方式使用epoll,並且以一種更加輕量的方式使用中斷。[4]如果你的應用程序旨在運行於Linux系統,那麼請考慮利用這個版本的傳輸;你將發現在高負載下它的性能要優於JDK的NIO實現。
這個傳輸的語義與在圖4-2所示的完全相同,而且它的用法也是簡單直接的。相關示例參照代碼清單4-4。如果要在那個代碼清單中使用epoll替代NIO,只需要將NioEventLoopGroup
替換為EpollEventLoopGroup
,並且將NioServerSocketChannel.class
替換為EpollServerSocketChannel.class
即可。
4.3.3 OIO——舊的阻塞I/O
Netty的OIO傳輸實現代表了一種折中:它可以通過常規的傳輸API使用,但是由於它是建立在java.net
包的阻塞實現之上的,所以它不是異步的。但是,它仍然非常適合於某些用途。
例如,你可能需要移植使用了一些進行阻塞調用的庫(如JDBC[5])的遺留代碼,而將邏輯轉換為非阻塞的可能也是不切實際的。相反,你可以在短期內使用Netty的OIO傳輸,然後再將你的代碼移植到純粹的異步傳輸上。讓我們來看一看怎麼做。
在java.net
API中,你通常會有一個用來接受到達正在監聽的ServerSocket
的新連接的線程。會創建一個新的和遠程節點進行交互的套接字,並且會分配一個新的用於處理相應通信流量的線程。這是必需的,因為某個指定套接字上的任何I/O操作在任意的時間點上都可能會阻塞。使用單個線程來處理多個套接字,很容易導致一個套接字上的阻塞操作也捆綁了所有其他的套接字。
有了這個背景,你可能會想,Netty是如何能夠使用和用於異步傳輸相同的API來支持OIO的呢。答案就是,Netty利用了SO_TIMEOUT
這個Socket
標誌,它指定了等待一個I/O操作完成的最大毫秒數。如果操作在指定的時間間隔內沒有完成,則將會拋出一個SocketTimeout Exception
。Netty將捕獲這個異常並繼續處理循環。在EventLoop
下一次運行時,它將再次嘗試。這實際上也是類似於Netty這樣的異步框架能夠支持OIO的唯一方式[6]。圖4-3說明了這個邏輯。
4.3.4 用於JVM內部通信的Local傳輸
Netty提供了一個Local傳輸,用於在同一個JVM中運行的客戶端和服務器程序之間的異步通信。同樣,這個傳輸也支持對於所有Netty傳輸實現都共同的API。
在這個傳輸中,和服務器Channel
相關聯的SocketAddress
並沒有綁定物理網絡地址;相反,只要服務器還在運行,它就會被存儲在註冊表裡,並在Channel
關閉時註銷。因為這個傳輸並不接受真正的網絡流量,所以它並不能夠和其他傳輸實現進行互操作。因此,客戶端希望連接到(在同一個JVM中)使用了這個傳輸的服務器端時也必須使用它。除了這個限制,它的使用方式和其他的傳輸一模一樣。
圖4-3 OIO的處理邏輯
4.3.5 Embedded傳輸
Netty提供了一種額外的傳輸,使得你可以將一組ChannelHandler
作為幫助器類嵌入到其他的ChannelHandler
內部。通過這種方式,你將可以擴展一個ChannelHandler
的功能,而又不需要修改其內部代碼。
不足為奇的是,Embedded傳輸的關鍵是一個被稱為EmbeddedChannel
的具體的Channel
實現。在第9章中,我們將詳細地討論如何使用這個類來為ChannelHandler
的實現創建單元測試用例。
4.4 傳輸的用例
既然我們已經詳細地瞭解了所有的傳輸,那麼讓我們考慮一下選用一個適用於特定用途的協議的因素吧。正如前面所提到的,並不是所有的傳輸都支持所有的核心協議,其可能會限制你的選擇。表4-4展示了截止出版時的傳輸和其所支持的協議。
表4-4 支持的傳輸和網絡協議
傳 輸
TCP
UDP
SCTP*
UDT[7]
NIO
×
×
×
×
Epoll(僅Linux)
×
×
—
—
OIO
×
×
×
×
* 參見RFC 2960中有關流控制傳輸協議(SCTP)的解釋:www.ietf.org/rfc/rfc2960.txt。表中X表示支持,—表示不支持。
在Linux上啟用SCTP
SCTP需要內核的支持,並且需要安裝用戶庫。
例如,對於Ubuntu,可以使用下面的命令:
# sudo apt-get install libsctp1
對於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傳輸。這既便於測試你的代碼,而又不需要創建大量的模擬(mock)對象。你的類將仍然符合常規的API事件流,保證該ChannelHandler
在和真實的傳輸一起使用時能夠正確地工作。你將在第9章中發現關於測試ChannelHandler
的更多信息。
表4-5總結了我們探討過的用例。
表4-5 應用程序的最佳傳輸
應用程序的需求
推薦的傳輸
非阻塞代碼庫或者一個常規的起點
NIO(或者在Linux上使用epoll)
阻塞代碼庫
OIO
在同一個JVM內部的通信
Local
測試ChannelHandler
的實現
Embedded
4.5 小結
在本章中,我們研究了傳輸、它們的實現和使用,以及Netty是如何將它們呈現給開發者的。
我們深入探討了Netty預置的傳輸,並且解釋了它們的行為。因為不是所有的傳輸都可以在相同的Java版本下工作,並且其中一些可能只在特定的操作系統下可用,所以我們也描述了它們的最低需求。最後,我們討論了你可以如何匹配不同的傳輸和特定用例的需求。
在下一章中,我們將關注於ByteBuf
和ByteBufHolder
——Netty的數據容器。我們將展示如何使用它們以及如何通過它們獲得最佳性能。
[1] 參見STARTTLS:http://en.wikipedia.org/wiki/STARTTLS。
[2] 這個是Netty特有的實現,更加適配Netty現有的線程模型,具有更高的性能以及更低的垃圾回收壓力,詳見https://github.com/netty/netty/wiki/Native-transports。——譯者注
[3] 參見Linux手冊頁中的epoll(4):http://linux.die.net/man/4/epoll。
[4] JDK的實現是水平觸發,而Netty的(默認的)是邊沿觸發。有關的詳細信息參見epoll在維基百科上的解釋:http://en.wikipedia.org/wiki/Epoll - Triggering_modes。
[5] JDBC的文檔可以在www.oracle.com/technetwork/java/javase/jdbc/index.html獲取。
[6] 這種方式的一個問題是,當一個SocketTimeoutException
被拋出時填充棧跟蹤所需要的時間,其對於性能來說代價很大。
[7] UDT協議實現了基於UDP協議的可靠傳輸,詳見https://zh.wikipedia.org/zh-cn/UDT。——譯者注