1. netty入門(一)
1.1. 傳統socket編程
- 在任何時候都可能有大量的線程處於休眠狀態,只是等待輸入或者輸出數據就緒,這可能算是一種資源浪費。
- 需要為每個線程的調用棧都分配內存,其默認值大小區間為 64 KB 到 1 MB,具體取決於操作系統。
- 即使 Java 虛擬機(JVM)在物理上可以支持非常大數量的線程,但是遠在到達該極限之前,上下文切換所帶來的開銷就會帶來麻煩
1.2. NIO
- class java.nio.channels.Selector 是Java 的非阻塞 I/O 實現的關鍵。它使用了事件通知 API以確定在一組非阻塞套接字中有哪些已經就緒能夠進行 I/O 相關的操作。因為可以在任何的時間檢查任意的讀操作或者寫操作的完成狀態,所以如圖 1-2 所示,一個單一的線程便可以處理多個並發的連接。
1.3. Netty核心組件
1.3.1. Channel
它代表一個到實體(如一個硬件設備、一個文件、一個網絡套接字或者一個能夠執
行一個或者多個不同的I/O操作的程序組件)的開放連接,如讀操作和寫操作
- 目前,可以把 Channel 看作是傳入(入站)或者傳出(出站)數據的載體。因此,它可以
被打開或者被關閉,連接或者斷開連接。
1.3.2. 回調
- 一個回調其實就是一個方法,一個指向已經被提供給另外一個方法的方法的引用。這使得后者可以在適當的時候調用前者。回調在廣泛的編程場景中都有應用,而且也是在操作完成后通知相關方最常見的方式之一
- Netty 在內部使用了回調來處理事件;當一個回調被觸發時,相關的事件可以被一個 interfaceChannelHandler 的實現處理。代碼清單 1-2 展示了一個例子:當一個新的連接已經被建立時,
ChannelHandler 的 channelActive()回調方法將會被調用,並將打印出一條信息
1.3.3. Future
- Future 提供了另一種在操作完成時通知應用程序的方式。這個對象可以看作是一個異步操作的結果的占位符;它將在未來的某個時刻完成,並提供對其結果的訪問
- JDK 預置了 interface java.util.concurrent.Future,但是其所提供的實現,只允許手動檢查對應的操作是否已經完成,或者一直阻塞直到它完成。這是非常繁瑣的,所以 Netty提供了它自己的實現——ChannelFuture,用於在執行異步操作的時候使用。
- ChannelFuture提供了幾種額外的方法,這些方法使得我們能夠注冊一個或者多個ChannelFutureListener實例。監聽器的回調方法operationComplete(),將會在對應的操作完成時被調用
- 簡而 言之 ,由ChannelFutureListener提供的通知機制消除了手動檢查對應的操作是否完成的必要
- 每個 Netty 的出站 I/O 操作都將返回一個 ChannelFuture;也就是說,它們都不會阻塞。正如我們前面所提到過的一樣,Netty 完全是異步和事件驅動的。
1.3.4. 事件和 ChannelHandler
-
Netty 使用不同的事件來通知我們狀態的改變或者是操作的狀態。這使得我們能夠基於已經
發生的事件來觸發適當的動作。這些動作可能是:- 記錄日志;
- 數據轉換;
- 流控制;
- 應用程序邏輯
-
每個事件都可以被分發給 ChannelHandler 類中的某個用戶實現的方法。這是一個很好的將事件驅動范式直接轉換為應用程序構件塊的例子。
-
Netty 的 ChannelHandler 為處理器提供了基本的抽象,如圖 1-3 所示的那些
1.4. 服務端核心流程
-
EchoServerHandler 實現了業務邏輯;
-
main()方法引導了服務器;
引導過程中所需要的步驟如下:
- 創建一個 ServerBootstrap 的實例以引導和綁定服務器;
- 創建並分配一個 NioEventLoopGroup 實例以進行事件的處理,如接受新連接以及讀/
寫數據; - 指定服務器綁定的本地的 InetSocketAddress;
- 使用一個 EchoServerHandler 的實例初始化每一個新的 Channel;
- 調用 ServerBootstrap.bind()方法以綁定服務器
1.5. 客戶端核心流程
- Echo 客戶端將會:
- 連接到服務器;
- 發送一個或者多個消息;
- 對於每個消息,等待並接收從服務器發回的相同的消息;
- 關閉連接。
- 流程
- 為初始化客戶端,創建了一個 Bootstrap 實例;
- 為進行事件處理分配了一個 NioEventLoopGroup 實例,其中事件處理包括創建新的連接以及處理入站和出站數據;
- 為服務器連接創建了一個 InetSocketAddress 實例;
- 當連接被建立時,一個 EchoClientHandler 實例會被安裝到(該 Channel 的)ChannelPipeline 中;
- 在一切都設置完成后,調用 Bootstrap.connect()方法連接到遠程節點;
1.6. Netty 的組件和設計
1.6.1. Channel、EventLoop 和 ChannelFuture
- Channel—Socket;
- EventLoop—控制流、多線程處理、並發;
- ChannelFuture—異步通知
1.6.1.1. Channel 接口
- 基本的 I/O 操作(bind()、connect()、read()和 write())依賴於底層網絡傳輸所提供的原語。
- Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 類的復雜性。
1.6.1.2. EventLoop 接口
-
EventLoop 定義了 Netty 的核心抽象,用於處理連接的生命周期中所發生的事件。
-
圖 3-1在高層次上說明了 Channel、EventLoop、Thread 以及 EventLoopGroup 之間的關系。
-
這些關系是:
- 一個 EventLoopGroup 包含一個或者多個 EventLoop;
- 一個 EventLoop 在它的生命周期內只和一個 Thread 綁定;
- 所有由 EventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理;
- 一個 Channel 在它的生命周期內只注冊於一個 EventLoop;
- 一個 EventLoop 可能會被分配給一個或多個 Channel
1.6.1.3. ChannelFuture 接口
- Netty 中所有的 I/O 操作都是異步的。因為一個操作可能不會立即返回,所以我們需要一種用於在之后的某個時間點確定其結果的方法。為此,Netty 提供了ChannelFuture 接口,其 addListener()方法注冊了一個 ChannelFutureListener,以便在某個操作完成時(無論是否成功)得到通知。
1.6.2. ChannelHandler 和 ChannelPipeline
1.6.2.1. ChannelHandler 接口
- 從應用程序開發人員的角度來看,Netty 的主要組件是 ChannelHandler,它充當了所有處理入站和出站數據的應用程序邏輯的容器
1.6.2.2. ChannelPipeline 接口
- ChannelPipeline 提供了 ChannelHandler 鏈的容器,並定義了用於在該鏈上傳播入站和出站事件流的 API。當 Channel 被創建時,它會被自動地分配到它專屬的 ChannelPipeline。
- ChannelHandler 安裝到 ChannelPipeline 中的過程如下所示:
- 一個ChannelInitializer的實現被注冊到了ServerBootstrap中
- 當 ChannelInitializer.initChannel()方法被調用時,ChannelInitializer將在 ChannelPipeline 中安裝一組自定義的 ChannelHandler;
- ChannelInitializer 將它自己從 ChannelPipeline 中移除。
- 圖 3-3 說明了一個 Netty 應用程序中入站和出站數據流之間的區別。從一個客戶端應用程序的角度來看,如果事件的運動方向是從客戶端到服務器端,那么我們稱這些事件為出站的,反之則稱為入站的。
1.6.2.3. 編碼器和解碼器
- 當你通過 Netty 發送或者接收一個消息的時候,就將會發生一次數據轉換。入站消息會被解碼;也就是說,從字節轉換為另一種格式,通常是一個 Java 對象。
- 如果是出站消息,則會發生相反方向的轉換:它將從它的當前格式被編碼為字節。這兩種方向的轉換的原因很簡單:網絡數據總是一系列的字節。
1.6.2.4. 抽象類 SimpleChannelInboundHandler
- 最常見的情況是,你的應用程序會利用一個 ChannelHandler 來接收解碼消息,並對該數據應用業務邏輯。
- 要創建一個這樣的 ChannelHandler,你只需要擴展基類 SimpleChannelInboundHandler
,其中 T 是你要處理的消息的 Java 類型 。
1.7. 傳輸
- 流經網絡的數據總是具有相同的類型:字節。這些字節是如何流動的主要取決於我們所說的網絡傳輸—一個幫助我們抽象底層數據傳輸機制的概念。
- ChannelHandler 的典型用途包括:
- 將數據從一種格式轉換為另一種格式;
- 提供異常的通知;
- 提供 Channel 變為活動的或者非活動的通知;
- 提供當 Channel 注冊到 EventLoop 或者從 EventLoop 注銷時的通知;
- 提供有關用戶自定義事件的通知
1.8. ByteBuf
- 網絡數據的基本單位總是字節。Java NIO 提供了 ByteBuffer 作為它的字節容器,但是這個類使用起來過於復雜,而且也有些繁瑣
- Netty 的 ByteBuffer 替代品是 ByteBuf,一個強大的實現,既解決了 JDK API 的局限性,又為網絡應用程序的開發者提供了更好的 API。
- 下面是一些 ByteBuf API 的優點:
- 它可以被用戶自定義的緩沖區類型擴展;
- 通過內置的復合緩沖區類型實現了透明的零拷貝;
- 容量可以按需增長(類似於 JDK 的 StringBuilder);
- 在讀和寫這兩種模式之間切換不需要調用 ByteBuffer 的 flip()方法;
- 讀和寫使用了不同的索引;
- 支持方法的鏈式調用;
- 支持引用計數;
- 支持池化
1.8.1. 如何工作
- ByteBuf 維護了兩個不同的索引:一個用於讀取,一個用於寫入。當你從 ByteBuf 讀取時,它的readerIndex 將會被遞增已經被讀取的字節數。同樣地,當你寫入 ByteBuf 時,它的writerIndex 也會被遞增。圖 5-1 展示了一個空 ByteBuf 的布局結構和狀態。
1.8.2. ByteBuf 的使用模式
1.8.2.1. 堆緩沖區
- 最常用的 ByteBuf 模式是將數據存儲在 JVM 的堆空間中。這種模式被稱為支撐數組(backing array),它能在沒有使用池化的情況下提供快速的分配和釋放。這種方式,如代碼清單5-1 所示,非常適合於有遺留的數據需要處理的情況。
1.8.2.2. 直接緩沖區
- 直接緩沖區是另外一種 ByteBuf 模式。我們期望用於對象創建的內存分配永遠都來自於堆中,但這並不是必須的——NIO 在 JDK 1.4 中引入的 ByteBuffer 類允許 JVM 實現通過本地調用來分配內存。這主要是為了避免在每次調用本地 I/O 操作之前(或者之后)將緩沖區的內容復制到一個中間緩沖區(或者從中間緩沖區把內容復制到緩沖區)。
- 直接緩沖區的主要缺點是,相對於基於堆的緩沖區,它們的分配和釋放都較為昂貴。如果你正在處理遺留代碼,你也可能會遇到另外一個缺點:因為數據不是在堆上,所以你不得不進行一次復制,如代碼清單 5-2 所示。
1.8.2.3. 復合緩沖區
- 第三種也是最后一種模式使用的是復合緩沖區,它為多個 ByteBuf 提供一個聚合視圖。在這里你可以根據需要添加或者刪除 ByteBuf 實例,這是一個 JDK 的 ByteBuffer 實現完全缺失的特性。
- Netty 通過一個 ByteBuf 子類——CompositeByteBuf——實現了這個模式,它提供了一個將多個緩沖區表示為單個合並緩沖區的虛擬表示
- 代碼清單 5-3 展示了如何通過使用 JDK 的 ByteBuffer 來實現這一需求。創建了一個包含兩個 ByteBuffer 的數組用來保存這些消息組件,同時創建了第三個 ByteBuffer 用來保存所有這些數據的副本。
- 分配和復制操作,以及伴隨着對數組管理的需要,使得這個版本的實現效率低下而且笨拙。代碼清單 5-4 展示了一個使用了 CompositeByteBuf 的版本
- CompositeByteBuf 可能不支持訪問其支撐數組,因此訪問 CompositeByteBuf 中的數據類似於(訪問)直接緩沖區的模式,如代碼清單 5-5 所示。
1.8.3. Unpooled 緩沖區
- 可能某些情況下,你未能獲取一個到 ByteBufAllocator 的引用。對於這種情況,Netty 提供了一個簡單的稱為 Unpooled 的工具類,它提供了靜態的輔助方法來創建未池化的 ByteBuf實例。表 5-8 列舉了這些中最重要的方法。
1.9. ChannelHandler和ChannelPipeline
1.9.1. ChannelHandler 家族
1.9.1.1. Channel 的生命周期
- Interface Channel 定義了一組和 ChannelInboundHandler API 密切相關的簡單但功能強大的狀態模型
- ChannelUnregistered Channel 已經被創建,但還未注冊到 EventLoop
- ChannelRegistered Channel 已經被注冊到了 EventLoop
- ChannelActive Channel 處於活動狀態(已經連接到它的遠程節點)。它現在可以接收和發送數據了
- ChannelInactive Channel 沒有連接到遠程節點
- Channel 的正常生命周期如圖 6-1 所示。當這些狀態發生改變時,將會生成對應的事件。這些事件將會被轉發給 ChannelPipeline 中的 ChannelHandler,其可以隨后對它們做出響應
1.9.1.2. ChannelHandler 的生命周期
- 表 6-2 中列出了 interface ChannelHandler 定義的生命周期操作,在 ChannelHandler被添加到 ChannelPipeline 中或者被從 ChannelPipeline 中移除時會調用這些操作。這些方法中的每一個都接受一個 ChannelHandlerContext 參數。
- Netty 定義了下面兩個重要的 ChannelHandler 子接口:
- ChannelInboundHandler——處理入站數據以及各種狀態變化;
- ChannelOutboundHandler——處理出站數據並且允許攔截所有的操作。
1.9.1.3. ChannelInboundHandler 接口
- 當某個 ChannelInboundHandler 的實現重寫 channelRead()方法時,它將負責顯式地釋放與池化的 ByteBuf 實例相關的內存。Netty 為此提供了一個實用方法 ReferenceCountUtil.release(),如代碼清單 6-1 所示。
- Netty 將使用 WARN 級別的日志消息記錄未釋放的資源,使得可以非常簡單地在代碼中發現違規的實例。但是以這種方式管理資源可能很繁瑣。一個更加簡單的方式是使用SimpleChannelInboundHandler。代碼清單 6-2 是代碼清單 6-1 的一個變體,說明了這一點
1.9.1.4. ChannelOutboundHandler 接口
- 出站操作和數據將由 ChannelOutboundHandler 處理。它的方法將被 Channel、ChannelPipeline 以及 ChannelHandlerContext 調用
- ChannelOutboundHandler 的一個強大的功能是可以按需推遲操作或者事件,這使得可以通過一些復雜的方法來處理請求。例如,如果到遠程節點的寫入被暫停了,那么你可以推遲沖刷操作並在稍后繼續
- 表6-4顯示了所有由ChannelOutboundHandler本身所定義的方法(忽略了那些從ChannelHandler 繼承的方法)。
1.9.1.5. ChannelHandler 適配器
-
你可以使用 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter類作為自己的 ChannelHandler 的起始點。這兩個適配器分別提供了 ChannelInboundHandler和ChannelOutboundHandler 的基本實現。
-
ChannelHandlerAdapter 還提供了實用方法 isSharable()。如果其對應的實現被標注為 Sharable,那么這個方法將返回 true,表示它可以被添加到多個 ChannelPipeline中
1.9.2. ChannelPipeline 接口
- ChannelHandler 可以通過添加、刪除或者替換其他的 ChannelHandler 來實時地修改
ChannelPipeline 的布局。