Netty實戰讀書筆記一:Netty的組件和設計以及它的傳輸

工作中用到了GRPC, 而它又用到了Netty, 所以最近在學習Netty的相關內容。

第三章 Netty的組件和設計

Channel 接口

基本的 I/O 操作(bind()、connect()、read()和 write())依賴於底層網絡傳輸所提 供的原語。在基於 Java 的網絡編程中,其基本的構造是 class Socket。Netty 的 Channel 接 口所提供的 API,大大地降低了直接使用 Socket 類的複雜性。此外,Channel 也是擁有許多 預定義的、專門化實現的廣泛類層次結構的根,下面是一個簡短的部分清單:

 EmbeddedChannel;
 LocalServerChannel;
 NioDatagramChannel;
 NioSocketChannel。

EventLoop 接口

EventLoop 定義了 Netty 的核心抽象,用於處理連接的生命週期中所發生的事件。我們將 在第 7 章中結合 Netty 的線程處理模型的上下文對 EventLoop 進行詳細的討論。目前,圖 3-1 在高層次上說明了 Channel、EventLoop、Thread 以及 EventLoopGroup 之間的關係。

這些關係是:
 一個 EventLoopGroup 包含一個或者多個 EventLoop;
 一個 EventLoop 在它的生命週期內只和一個 Thread 綁定;
 所有由 EventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理;
 一個 Channel 在它的生命週期內只註冊於一個 EventLoop;
 一個 EventLoop 可能會被分配給一個或多個 Channel。 注意,在這種設計中,一個給定 Channel 的 I/O 操作都是由相同的 Thread 執行的,實際
上消除了對於同步的需要。

ChannelFuture 接口

正如我們已經解釋過的那樣,Netty 中所有的 I/O 操作都是異步的。因爲一個操作可能不會 立即返回,所以我們需要一種用於在之後的某個時間點確定其結果的方法。爲此,Netty 提供了 ChannelFuture接口,其addListener()方法註冊了一個ChannelFutureListener,以 便在某個操作完成時(無論是否成功)得到通知。
我們將在第 7 章中深入地討論 EventLoop 和 EventLoopGroup。

ChannelHandler 接口

從應用程序開發人員的角度來看,Netty 的主要組件是 ChannelHandler,它充當了所有 處理入站和出站數據的應用程序邏輯的容器。這是可行的,因爲 ChannelHandler 的方法是 由網絡事件(其中術語“事件”的使用非常廣泛)觸發的。事實上,ChannelHandler 可專 門用於幾乎任何類型的動作,例如將數據從一種格式轉換爲另外一種格式,或者處理轉換過程 中所拋出的異常。

舉例來說,ChannelInboundHandler 是一個你將會經常實現的子接口。這種類型的 ChannelHandler 接收入站事件和數據,這些數據隨後將會被你的應用程序的業務邏輯所處 理。當你要給連接的客戶端發送響應時,也可以從 ChannelInboundHandler 沖刷數據。你 的應用程序的業務邏輯通常駐留在一個或者多個 ChannelInboundHandler 中。

ChannelPipeline 接口

ChannelPipeline 提供了 ChannelHandler 鏈的容器,並定義了用於在該鏈上傳播入站 和出站事件流的 API。當 Channel 被創建時,它會被分配一個ChannelPipeline。

ChannelHandler 安裝到 ChannelPipeline 中的過程如下所示:
 一個ChannelInitializer的實現被註冊到了ServerBootstrap中 ;
 當 ChannelInitializer.initChannel()方法被調用時,ChannelInitializer
將在 ChannelPipeline 中安裝一組自定義的 ChannelHandler;  ChannelInitializer 將它自己從 ChannelPipeline 中移除。


鑑於出站操作和入站操作是不同的,你可能會想知道如果將兩個類別的 ChannelHandler 都混合添加到同一個 ChannelPipeline 中會發生什麼。雖然 ChannelInboundHandle 和 ChannelOutboundHandle 都擴展自 ChannelHandler,但是 Netty 能區分 ChannelIn- boundHandler 實現和 ChannelOutboundHandler 實現,並確保數據只會在具有相同定 向類型的兩個 ChannelHandler 之間傳遞。

當 ChannelHandler 被添加到 ChannelPipeline 時,它將會被分配一個 ChannelHandler- Context,其代表了 ChannelHandler 和 ChannelPipeline 之間的綁定。雖然這個對象可 以被用於獲取底層的 Channel,但是它主要還是被用於寫出站數據。

在Netty中,有兩種發送消息的方式。你可以直接寫到Channel中,也可以 寫到和Channel- Handler 相關聯的 ChannelHandlerContext 對象中。前一種方式將會導致消息從 Channel- Pipeline 的尾端開始流動,而後者將導致消息從 ChannelPipeline 中的下一個 Channel- Handler 開始流動。

Bootstrap

有兩種類型的引導:一種用於客戶端(簡單地稱爲 Bootstrap),而另一種 (ServerBootstrap)用於服務器。ServerBootstrap 將綁定到一個 端口,因爲服務器必須要監聽連接,而 Bootstrap 則是由想要連接到遠程節點的客戶端應用程 序所使用的。第二個區別可能更加明顯。引導一個客戶端只需要一個 EventLoopGroup,但是一個 ServerBootstrap 則需要兩個(也可以是同一個實例)。爲什麼呢?

因爲服務器需要兩組不同的 Channel。第一組將只包含一個 ServerChannel,代表服務 器自身的已綁定到某個本地端口的正在監聽的套接字。而第二組將包含所有已創建的用來處理傳 入客戶端連接(對於每個服務器已經接受的連接都有一個)的 Channel。圖 3-4 說明了這個模 型,並且展示了爲何需要兩個不同的 EventLoopGroup。

與 ServerChannel 相關聯的 EventLoopGroup 將分配一個負責爲傳入連接請求創建 Channel 的 EventLoop。一旦連接被接受,第二個 EventLoopGroup 就會給它的 Channel 分配一個 EventLoop。

第四章 傳輸

傳輸API

傳輸 API 的核心是 interface Channel,它被用於所有的 I/O 操作。Channel 類的層次結構如圖 4-1 所示。

如圖所示,每個 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 和 ChannelConfig 之外,也可以利用 Channel 的其他方法,其中最重要的列舉在表 4-1 中。

Netty 的 Channel 實現是線程安全的,因此你可以存儲一個到 Channel 的引用,並且每當 你需要向遠程節點寫數據時,都可以使用它,即使當時許多線程都在使用它。需要注意的是,消息將會被保證按順序發送。

內置的傳輸


NIO——非阻塞 I/O

NIO 提供了一個所有 I/O 操作的全異步的實現。它利用了自 NIO 子系統被引入 JDK 1.4 時便 可用的基於選擇器的 API。
選擇器背後的基本概念是充當一個註冊表,在那裏你將可以請求在 Channel 的狀態發生變 化時得到通知。可能的狀態變化有:

 新的 Channel 已被接受並且就緒;
 Channel 連接已經完成;
 Channel 有已經就緒的可供讀取的數據;
 Channel 可用於寫數據。

選擇器運行在一個檢查狀態變化並對其做出相應響應的線程上,在應用程序對狀態的改變做出響應之後,選擇器將會被重置,並將重複這個過程。

對於所有 Netty 的傳輸實現都共有的用戶級別 API 完全地隱藏了這些 NIO 的內部細節。 圖 4-2 展示了該處理流程。

Epoll— 用於 Linux 的本地非阻塞傳輸

正如我們之前所說的,Netty 的 NIO 傳輸基於 Java 提供的異步/非阻塞網絡編程的通用抽象。 雖然這保證了 Netty 的非阻塞 API 可以在任何平臺上使用,但它也包含了相應的限制,因爲 JDK 爲了在所有系統上提供相同的功能,必須做出妥協。
Linux作爲高性能網絡編程的平臺,其重要性與日俱增,這催生了大量先進特性的開發,其 中包括epoll——一個高度可擴展的I/O事件通知特性。這個API自Linux內核版本 2.5.44(2002)被 引入,提供了比舊的POSIX select和poll系統調用更好的性能,同時現在也是Linux上非阻 塞網絡編程的事實標準。Linux JDK NIO API使用了這些epoll調用。

Netty爲Linux提供了一組NIO API,其以一種和它本身的設計更加一致的方式使用epoll,並 且以一種更加輕量的方式使用中斷。1如果你的應用程序旨在運行於Linux系統,那麼請考慮利用 這個版本的傳輸;你將發現在高負載下它的性能要優於JDK的NIO實現。
這個傳輸的語義與在圖 4-2 所示的完全相同,而且它的用法也是簡單直接的。相關示例參照 代碼清單 4-4。如果要在那個代碼清單中使用 epoll 替代 NIO,只需要將 NioEventLoopGroup 替換爲 EpollEventLoopGroup,並且將 NioServerSocketChannel.class 替換爲 EpollServerSocketChannel.class 即可。

OIO— 舊的阻塞 I/O

Netty 的 OIO 傳輸實現代表了一種折中:它可以通過常規的傳輸 API 使用,但是由於它 是建立在 java.net 包的阻塞實現之上的,所以它不是異步的。但是,它仍然非常適合於某 些用途。
例如,你可能需要移植使用了一些進行阻塞調用的庫(如JDBC2)的遺留代碼,而將邏輯轉 換爲非阻塞的可能也是不切實際的。相反,你可以在短期內使用Netty的OIO傳輸,然後再將你的 代碼移植到純粹的異步傳輸上。讓我們來看一看怎麼做。
在 java.net API 中,你通常會有一個用來接受到達正在監聽的 ServerSocket 的新連 接的線程。會創建一個新的和遠程節點進行交互的套接字,並且會分配一個新的用於處理相應通 信流量的線程。這是必需的,因爲某個指定套接字上的任何 I/O 操作在任意的時間點上都可能會 阻塞。使用單個線程來處理多個套接字,很容易導致一個套接字上的阻塞操作也捆綁了所有其他 的套接字。
有了這個背景,你可能會想,Netty是如何能夠使用和用於異步傳輸相同的API來支持OIO的呢。 答案就是,Netty利用了SO_TIMEOUT這個Socket標誌,它指定了等待一個I/O操作完成的最大毫秒 數。如果操作在指定的時間間隔內沒有完成,則將會拋出一個SocketTimeout Exception。Netty 將捕獲這個異常並繼續處理循環。在EventLoop下一次運行時,它將再次嘗試。這實際上也是 類似於Netty這樣的異步框架能夠支持OIO的唯一方式 3。圖 4-3 說明了這個邏輯。

發佈了123 篇原創文章 · 獲贊 334 · 訪問量 52萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章