Netty實戰學習筆記(一)——Netty的概念及體系結構
JAVA早期的Socket
JAVA早期只支援由本地系統套接字型檔提供的所謂的阻塞函式。
socket通訊的基本過程如圖:
在服務端,註冊服務對某個埠進行監聽,然後使用阻塞的accept()函式,來取出請求佇列中的socket,或者一直等待,直到收到客戶端的請求。連線建立後,服務端接受輸入流進行處理,返回結果給客戶端。最後通訊結束,雙方斷開連線。
這樣的過程每次只能處理一個連線,如果要多個併發客戶端,則需要為每個socket都建立一個Thread。如圖:
但是缺點有很多:
- 在任何時候都可能有大量的執行緒處於休眠狀態,只是等待輸入或者輸出資料就緒,這可能算是一種資源浪費。
- 需要為每個執行緒的呼叫棧都分配記憶體,其預設值大小區間為 64 KB 到 1 MB, 具體取決於作業系統。
- 即使 Java 虛擬機器(JVM) 在物理上可以支援非常大數量的執行緒, 但是遠在到達該極限之前, 上下文切換所帶來的開銷就會帶來麻煩, 例如, 在達到 10000 個連線的時候。
雖然這種併發方案對於支撐中小數量的客戶端來說還算可以接受,但是為了支撐 100000 或者更多的併發連線所需要的資源使得它很不理想。
JAVA NIO
非阻塞的設計如圖:
java.nio.channels.Selector 是 Java 的非阻塞 I/O 實現的關鍵。它使用了事件通知 API 以確定在一組非阻塞套接字中有哪些已經就緒能夠進行 I/O 相關的操作。因為可以在任何的時間檢查任意的讀操作或者寫操作的完成狀態,所以一個單一的執行緒便可以處理多個併發的連線。
總體來看,與阻塞 I/O 模型相比,這種模型提供了更好的資源管理:
- 使用較少的執行緒便可以處理許多連線,因此也減少了記憶體管理和上下文切換所帶來開銷;
- 當沒有 I/O 操作需要處理的時候,執行緒也可以被用於其他任務。
儘管已經有許多直接使用 Java NIO API 的應用程式被構建了,但是要做到如此正確和安全並不容易。特別是,在高負載下可靠和高效地處理和排程 I/O 操作是一項繁瑣而且容易出錯的任務,Netty很好的解決了這個問題。
Netty特性
分類 | Netty的特性 |
---|---|
設計 | 統一的 API,支援多種傳輸型別,阻塞的和非阻塞的 簡單而強大的執行緒模型 真正的無連線資料報套接字支援 連結邏輯元件以支援複用 |
易於使用 | 詳實的Javadoc和大量的示例集 不需要超過JDK 1.6+③的依賴。(一些可選的特性可能需要Java 1.7+和/或額外的依賴) |
效能 | 擁有比 Java 的核心 API 更高的吞吐量以及更低的延遲 得益於池化和複用, 擁有更低的資源消耗 最少的記憶體複製 |
健壯性 | 不會因為慢速、快速或者超載的連線而導致 OutOfMemoryError 消除在高速網路中 NIO 應用程式常見的不公平讀/寫比率 |
安全性 | 完整的 SSL/TLS 以及 StartTLS 支援 可用於受限環境下,如 Applet 和 OSGI |
社群驅動 | 釋出快速而且頻繁 |
非同步的能力對於實現最高級別的可伸縮性至關重要,定義為:“ 一種系統、 網路或者程序在需要處理的工作不斷增長時, 可以通過某種可行的方式或者擴大它的處理能力來適應這種增長的能力。”
非同步和可伸縮性之間的聯絡又是什麼呢?
- 非阻塞網路呼叫使得我們可以不必等待一個操作的完成。完全非同步的 I/O 正是基於這個特性構建的,並且更進一步:非同步方法會立即返回,並且在它完成時,會直接或者在稍後的某個時間點通知使用者。
- 選擇器使得我們能夠通過較少的執行緒便可監視許多連線上的事件。將這些元素結合在一起,與使用阻塞 I/O 來處理大量事件相比, 使用非阻塞 I/O 來處理更快速、更經濟。從網路程式設計的角度來看,這是構建我們理想系統的關鍵,而且你會看到,這也是Netty 的設計底蘊的關鍵。
Netty核心元件
Channel
Channel 是 Java NIO 的一個基本構造。它代表一個到實體(如一個硬體裝置、一個檔案、一個網路套接字或者一個能夠執
行一個或者多個不同的I/O操作的程式元件) 的開放連線,如讀操作和寫操作。目前,可以把 Channel 看作是傳入(入站)或者傳出(出站)資料的載體。因此,它可以被開啟或者被關閉,連線或者斷開連線。
回撥
Netty 在內部使用了回撥來處理事件;當一個回撥被觸發時,相關的事件可以被一個 interfaceChannelHandler 的實現處理。
如下當一個新的連線已經被建立時,ChannelHandler 的 channelActive()回撥方法將會被呼叫,並將打印出一條資訊。
public class ConnectHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Client " + ctx.channel().remoteAddress() + " connected");
}
}
Future
Future 提供了另一種在操作完成時通知應用程式的方式。這個物件可以看作是一個非同步操作的結果的佔位符;它將在未來的某個時刻完成,並提供對其結果的訪問。
JDK 預置了 interface java.util.concurrent.Future,但是其所提供的實現,只允許手動檢查對應的操作是否已經完成,或者一直阻塞直到它完成。這是非常繁瑣的,所以 Netty 提供了它自己的實現——ChannelFuture,用於在執行非同步操作的時候使用。
public class ConnectExample {
private static final Channel CHANNEL_FROM_SOMEWHERE = new NioSocketChannel();
public static void connect() {
Channel channel = CHANNEL_FROM_SOMEWHERE; //reference form somewhere
// Does not block
ChannelFuture future = channel.connect(new InetSocketAddress("192.168.0.1", 25));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (future.isSuccess()) {
ByteBuf buffer = Unpooled.copiedBuffer("Hello", Charset.defaultCharset());
ChannelFuture wf = future.channel().writeAndFlush(buffer);
// ...
} else {
Throwable cause = future.cause();
cause.printStackTrace();
}
}
});
}
}
這裡,connect()方法將會直接返回, 而不會阻塞,該呼叫將會在後臺完成。這究竟什麼時候會發生則取決於若干的因素,但這個關注點已經從程式碼中抽象出來了。因為執行緒不用阻塞以等待對應的操作完成, 所以它可以同時做其他的工作,從而更加有效地利用資源。
首先, 要連線到遠端節點上。然後, 要註冊一個新的 ChannelFutureListener 到對 connect()方法的呼叫所返回的 ChannelFuture 上。當該監聽器被通知連線已經建立的時候, 要檢查對應的狀態 。如果該操作是成功的, 那麼將資料寫到該 Channel。否則, 要從 ChannelFuture 中檢索對應的 Throwable。
事件和 ChannelHandler
Netty 使用不同的事件來通知我們狀態的改變或者是操作的狀態。這使得我們能夠基於已經發生的事件來觸發適當的動作。這些動作可能是:
- 記錄日誌
- 資料轉換
- 流控制
- 應用程式邏輯
Netty 是一個網路程式設計框架,所以事件是按照它們與入站或出站資料流的相關性進行分類的。可能由入站資料或者相關的狀態更改而觸發的事件包括:
- 連線已被啟用或者連線失活
- 資料讀取
- 使用者事件
- 錯誤事件
出站事件是未來將會觸發的某個動作的操作結果,這些動作包括:
- 開啟或者關閉到遠端節點的連線
- 將資料寫到或者沖刷到套接字
Netty 的 ChannelHandler 為處理器提供了基本的抽象, 目前可以認為每個 ChannelHandler 的例項都類似於一種為了響應特定事件而被執行的回撥。
Netty 提供了大量預定義的可以開箱即用的 ChannelHandler 實現,包括用於各種協議(如 HTTP 和 SSL/TLS)的 ChannelHandler。在內部, ChannelHandler 自己也使用了事件和 Future,使得它們也成為了你的應用程式將使用的相同抽象的消費者。