Netty實戰(一)

2023-05-23 18:00:34

第一章 Java網路程式設計

最早期的 Java API(java.net)只支援由本地系統通訊端庫提供的所謂的阻塞函數,像下面的那樣

        //建立一個新的 ServerSocket,用以監聽指定埠上的連線請求
        ServerSocket serverSocket = new ServerSocket(portNumber);
        //對 accept()方法的呼叫將被阻塞,直到一個連線建立.隨後返回一個新的 Socket 用於使用者端和伺服器之間的通訊。該 ServerSocket 將繼續監聽傳入的連線。
        Socket clientSocket = serverSocket.accept();
        //這些流物件都派生於該通訊端的流物件
        BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));//從一個字元輸入流中讀取文字
        PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);//列印物件的格式化的表示到文字輸出流
        String request, response;
        //處理迴圈開始
        while ((request = in.readLine()) != null) { //readLine()方法將會阻塞,直到一個由換行符或者回車符結尾的字串被讀取。
            if ("Done".equals(request)) { //如果使用者端傳送了「Done」,則退出處理迴圈
                break;
            }
            //請求被傳遞給伺服器的處理方法
            response = processRequest(request);//使用者端的請求已經被處理
            out.println(response);//伺服器的響應被傳送給了使用者端
            //繼續執行處理迴圈
        }

這樣有幾個不足之處:

1、這段程式碼一次只能處理一個連線(如下圖),當有新的連線時就需要為新的連線新增一個執行緒。但每個執行緒都不可能時時刻刻在工作,所以這樣就造成了大量的資源浪費。

2、分配執行緒是需要佔用記憶體的,每個執行緒佔用64KB還是1MB取決於作業系統。

3、即使使用者有足夠的資源來支撐這種方案,但當連線數達到10000以上的時候上下文的切換還是非常麻煩的。

1.1 Java NIO

由於阻塞IO的不便,我們想到了非阻塞的通訊端呼叫——NIO,其為網路資源的利用率提供了相當多的控制:

  • 可以使用 setsockopt()方法設定通訊端,以便讀/寫呼叫在沒有資料的時候立即返回

  • 可以使用作業系統的事件通知 API註冊一組非阻塞通訊端,以確定它們中是否有任何的通訊端已經有資料可供讀寫。

Java 對於非阻塞 I/O 的支援是在 2002 年引入的,位於 JDK 1.4 的 java.nio 包中。NIO 最開始是新的輸入/輸出(New Input/Output)的英文縮寫,但是,該Java API 已經出現足夠長的時間了,不再是「新的」了,因此,如今大多數的使用者認為NIO 代表非阻塞 I/O(Non-blocking I/O),而阻塞I/O(blocking I/O)是舊的輸入/輸出(old input/output,OIO)。你也可能遇到它被稱為普通I/O(plain I/O)的時候。

1.2 選擇器

class java.nio.channels.Selector 是Java 的非阻塞 I/O 實現的關鍵,它使用了事件通知 API以確定在一組非阻塞通訊端中有哪些已經就緒能夠進行 I/O 相關的操作。因為可以在任何的時間檢查任意的讀操作或者寫操作的完成狀態,所以一個單一的執行緒便可以處理多個並行的連線。

這種設計帶來更好的資源管理:

  • 使用較少的執行緒便可以處理許多連線,因此也減少了記憶體管理和上下文切換所帶來開銷。
  • 當沒有 I/O 操作需要處理的時候,執行緒也可以被用於其他任務。

儘管已經有許多直接使用 Java NIO API 的應用程式被構建了,但是要做到如此正確和安全並
不容易。特別是,在高負載下可靠和高效地處理和排程 I/O 操作是一項繁瑣而且容易出錯的任務,這些Netty可以更好的幫我們來處理。

第二章 Netty是什麼

2.1 Netty簡介

Netty是由JBOSS提供的一個java開源框架,它提供非同步的、事件驅動的網路應用程式框架和工具。Netty相當簡化和流線化了網路應用的程式設計開發過程,例如,TCP和UDP的socket服務開發。

2.2 Netty的特性

2.2.1 設計

  • 統一的 API,支援多種傳輸型別,阻塞的和非阻塞的。
  • 簡單而強大的執行緒模型。
  • 真正的無連線資料包通訊端支援。
  • 連結邏輯元件以支援複用。

2.2.2 易於使用

  • 詳實的Javadoc和大量的範例集。
  • 不需要超過JDK 1.6+的依賴。(一些可選的特性可能需要Java 1.7+和/或額外的依賴)。

2.2.3 效能

  • 擁有比 Java 的核心 API 更高的吞吐量以及更低的延遲。
  • 得益於池化和複用,擁有更低的資源消耗。
  • 最少的記憶體複製。

2.2.4 健壯性

  • 不會因為慢速、快速或者超載的連線而導致 OutOfMemoryError。
  • 消除在高速網路中 NIO 應用程式常見的不公平讀/寫比率。

2.2.5 安全性

  • 完整的 SSL/TLS 以及 StartTLS 支援。
  • 可用於受限環境下,如 Applet 和 OSGI。

2.2.6 社群驅動

  • 釋出快速而且頻繁。

2.3 Netty的使用者

Netty擁有一個充滿活力並且不斷壯大的使用者社群,其中不乏大型公司,如Apple、Twitter、Facebook、Google、Square和Instagram,還有流行的開源專案,如Infinispan、HornetQ、Vert.x、Apache Cassandra和Elasticsearch,它們所有的核心程式碼都利用了Netty強大的網路抽象。

每當你使用Twitter,你便是在使用Finagle,它們基於Netty的系統間通訊框架。Facebook在Nifty中使用了Netty,它們的Apache Thrift服務。可伸縮性和效能對這兩家公司來說至關重要,他們也經常為Netty貢獻程式碼 。反過來,Netty 也已從這些專案中受益,通過實現 FTP、SMTP、HTTP 和 WebSocket 以及其他的基於二進位制和基於文字的協定,Netty 擴充套件了它的應用範圍及靈活性。

2.4 非同步和事件驅動

2.4.1 非同步

生活中我們可能遇到過很多非同步的場景。比如:燒水的過程中你可以乾點別的,等待水燒開。本質上我們可以認為:它可以以任意的順序響應在任意的時間點產生的事件

非同步在計算機程式中可以這樣這樣定義它:一種系統、網路或者程序在需要處理的工作不斷增長時,可以通過某種可行的方式或者擴大它的處理能力來適應這種增長的能力。

2.4.2 非同步和伸縮性

非同步和可伸縮性之間的聯絡又是什麼呢?

  • 非阻塞網路呼叫使得我們可以不必等待一個操作的完成。完全非同步的 I/O 正是基於這個特性構建的,並且更進一步:非同步方法會立即返回,並且在它完成時,會直接或者在稍後的某個時間點通知使用者。

  • 選擇器使得我們能夠通過較少的執行緒便可監視許多連線上的事件。

將這些元素結合在一起,與使用阻塞 I/O 來處理大量事件相比,使用非阻塞 I/O 來處理更快速、更經濟。從網路程式設計的角度來看,這是構建我們理想系統的關鍵,這也是Netty 的設計底蘊的關鍵。

第三章 Netty核心元件

3.1 Channel

Channel 是 Java NIO 的一個基本構造。它代表一個到實體(如一個硬體裝置、一個檔案、一個網路通訊端或者一個能夠執行一個或者多個不同的I/O操作的程式元件)的開放連線,如讀操作和寫操作。目前,可以把 Channel 看作是傳入(入站)或者傳出(出站)資料的載體。因此,它可以被開啟或者被關閉,連線或者斷開連線。

3.2 回撥

一個回撥其實就是一個方法,一個指向已經被提供給另外一個方法的方法的參照。這使得後者可以在適當的時候呼叫前者。回撥在廣泛的程式設計場景中都有應用,而且也是在操作完成後通知相關方最常見的方式之一。

Netty 在內部使用了回撥來處理事件;當一個回撥被觸發時,相關的事件可以被一個interfaceChannelHandler 的實現處理。如下:

public class ConnectHandler extends ChannelInboundHandlerAdapter {

    //當一個新的連線已經被建立時,channelActive(ChannelHandler Context)將會被呼叫
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Client " + ctx.channel().remoteAddress() + " connected");
    }
}

當一個新的連線已經被建立時,ChannelHandler 的 channelActive()回撥方法將會被呼叫,並將列印出一條資訊。

3.3 Future

Future 提供了另一種在操作完成時通知應用程式的方式。這個物件可以看作是一個非同步操作的結果的預留位置;它將在未來的某個時刻完成,並提供對其結果的存取。

Java中也提供了Future的實現,但比較繁瑣。為此,Netty提供了它自己的實現——ChannelFuture,用於在執行非同步操作的時候使用。

ChannelFuture提供了幾種額外的方法,這些方法使得我們能夠註冊一個或者多個ChannelFutureListener範例。

監聽器的回撥方法operationComplete(),將會在對應的操作完成時被呼叫 。然後監聽器可以判斷該操作是成功地完成了還是出錯了。如果是後者,我們可以檢索產生的Throwable。

每個 Netty 的出站 I/O 操作都將返回一個 ChannelFuture,它們都不會阻塞。

Channel channel = ...;
//非同步地連線到遠端節點
ChannelFuture future = channel.connect(
new InetSocketAddress("192.168.0.1", 25));

像這樣connect()方法將會直接返回,而不會阻塞,該呼叫將會在後臺完成。這究竟什麼時候會發生
則取決於若干的因素,但這個關注點已經從程式碼中抽象出來了。因為執行緒不用阻塞以等待對應的操作完成,所以它可以同時做其他的工作,從而更加有效地利用資源。

ps:如果在 ChannelFutureListener 新增到 ChannelFuture 的時候,ChannelFuture 已經完成,那麼該 ChannelFutureListener 將會被直接地通知。

3.3.1 如何使用ChannelFutureListener

下面的程式碼演示瞭如何使用ChannelFutureListener 。首先,要連線到遠端節點上。然後,要註冊一個新的 ChannelFutureListener 到對 connect()方法的呼叫所返回的 ChannelFuture 上。當該監聽器被通知連線已經建立的時候,要檢查對應的狀態 。如果該操作是成功的,那麼將資料寫到該 Channel。否則,要從ChannelFuture 中檢索對應的 Throwable。

Channel channel = ...;
//非同步連線到遠端節點
ChannelFuture future = channel.connect(new InetSocketAddress("192.168.0.1", 25));
//註冊一個 ChannelFutureListener,以便在操作完成時獲得通知
future.addListener(new ChannelFutureListener() {
//檢查操作的狀態
@Override
public void operationComplete(ChannelFuture future) {
//如果操作是成功的,則建立一個 ByteBuf 以持有資料
if (future.isSuccess()){
ByteBuf buffer = Unpooled.copiedBuffer("Hello",Charset.defaultCharset());
//將資料非同步地傳送到遠端節點。返回一個 ChannelFuture
ChannelFuture wf = future.channel().writeAndFlush(buffer);
....
} else {
//如果發生錯誤,則存取描述原因的 Throwable。接下來的處理可以根據具體業務來處理
Throwable cause = future.cause();
cause.printStackTrace();
}
}
});

我們可以把ChannelFutureListener 看作是回撥的一個更加精細的版本。

3.4 事件和ChannelHandler

Netty使用以下事件來通知我們狀態改變或者操作狀態。

  • 記錄紀錄檔;
  • 資料轉換;
  • 流控制;
  • 應用程式邏輯。

Netty 是一個網路程式設計框架,所以事件是按照它們與入站或出站資料流的相關性進行分類的。

可能由入站資料或者相關的狀態更改而觸發的事件包括:

  • 連線已被啟用或者連線失活。
  • 資料讀取。
  • 使用者事件。
  • 錯誤事件。

出站事件是未來將會觸發的某個動作的操作結果,這些動作包括:

  • 開啟或者關閉到遠端節點的連線。
  • 將資料寫到或者沖刷到通訊端。

每個事件都可以被分發給 ChannelHandler 類中的某個使用者實現的方法。這是一個很好的將事件驅動正規化直接轉換為應用程式構件塊的例子。下圖展示了一個事件是如何被一個這樣的ChannelHandler 鏈處理的。

目前暫時可以認為每個 ChannelHandler 的範例都類似於一種為了響應特定事件而被執行的回撥。

Netty 提供了大量預定義的可以開箱即用的 ChannelHandler 實現,包括用於各種協定(如 HTTP 和 SSL/TLS)的 ChannelHandler。在內部,ChannelHandler 自己也使用了事件和 Future,使得它們也成為了你的應用程式將使用的相同抽象的消費者。

3.5 Future、回撥和 ChannelHandler

Netty的非同步程式設計模型是建立在Future和回撥的概念之上的,而將事件派發到ChannelHandler的方法則發生在更深的層次上。結合在一起,這些元素就提供了一個處理環境,使你的應用程式邏輯可以獨立於任何網路操作相關的顧慮而獨立地演變。這也是 Netty 的設計方式的一個關鍵目標。攔截操作以及高速地轉換入站資料和出站資料,都只需要你提供回撥或者利用操作所返回的Future。這使得連結操作變得既簡單又高效,並且促進了可重用的通用程式碼的編寫。

3.6 選擇器、事件和 EventLoop

Netty 通過觸發事件將 Selector 從應用程式中抽象出來,消除了所有本來將需要手動編寫的派發程式碼。在內部,將會為每個 Channel 分配一個 EventLoop,用以處理所有事件,包括:

  • 註冊感興趣的事件。
  • 將事件派發給 ChannelHandler。
  • 安排進一步的動作。

EventLoop 本身只由一個執行緒驅動,其處理了一個 Channel 的所有 I/O 事件,並且在該EventLoop 的整個生命週期內都不會改變。這個簡單而強大的設計消除了你可能有的在ChannelHandler 實現中需要進行同步的任何顧慮,因此,你可以專注於提供正確的邏輯,用來在有感興趣的資料要處理的時候執行。如同我們在詳細探討 Netty 的執行緒模型時將會看到的,該 API 是簡單而緊湊的。