閱讀221 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《Netty實戰》Netty In Action中文版 第1章——Netty——異步和事件驅動(二)

1.2 Netty簡介

不久以前,我們在本章一開始所呈現的場景——支持成千上萬的並發客戶端——還被認定為是不可能的。然而今天,作為係統用戶,我們將這種能力視為理所當然;同時作為開發人員,我們期望將水平線提得更高[3]。因為我們知道,總會有更高的吞吐量和可擴展性的要求——在更低的成本的基礎上進行交付。

不要低估了這最後一點的重要性。我們已經從漫長的痛苦經曆中學到:直接使用底層的API暴露了複雜性,並且引入了對往往供不應求的技能的關鍵性依賴[4]。這也就是,麵向對象的基本概念:用較簡單的抽象隱藏底層實現的複雜性。

這一原則也催生了大量框架的開發,它們為常見的編程任務封裝了解決方案,其中的許多都和分布式係統的開發密切相關。我們可以確定地說:所有專業的Java開發人員都至少對它們熟知一二。[5]對於我們許多人來說,它們已經變得不可或缺,因為它們既能滿足我們的技術需求,又能滿足我們的時間表。

在網絡編程領域,Netty是Java的卓越框架。[6]它駕馭了Java高級API的能力,並將其隱藏在一個易於使用的API之後。Netty使你可以專注於自己真正感興趣的——你的應用程序的獨一無二的價值。

在我們開始首次深入地了解Netty之前,請仔細審視表1-1中所總結的關鍵特性。有些是技術性的,而其他的更多的則是關於架構或設計哲學的。在本書的學習過程中,我們將不止一次地重新審視它們。

表1-1 Netty的特性總結

分  類

Netty的特性

設計

統一的API,支持多種傳輸類型,阻塞的和非阻塞的簡單而強大的線程模型真正的無連接數據報套接字支持鏈接邏輯組件以支持複用

易於使用

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

性能

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

健壯性

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

安全性

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

社區驅動

發布快速而且頻繁

1.2.1 誰在使用Netty

Netty擁有一個充滿活力並且不斷壯大的用戶社區,其中不乏大型公司,如Apple、Twitter、Facebook、Google、Square和Instagram,還有流行的開源項目,如Infinispan、HornetQ、Vert.x、Apache Cassandra和Elasticsearch[8],它們所有的核心代碼都利用了Netty強大的網絡抽象[9]。在初創企業中,Firebase和Urban Airship也在使用Netty,前者用來做HTTP長連接,而後者用來支持各種各樣的推送通知。

每當你使用Twitter,你便是在使用Finagle[10],它們基於Netty的係統間通信框架。Facebook在Nifty中使用了Netty,它們的Apache Thrift服務。可伸縮性和性能對這兩家公司來說至關重要,他們也經常為Netty貢獻代碼[11]

反過來,Netty也已從這些項目中受益,通過實現FTP、SMTP、HTTP和WebSocket以及其他的基於二進製和基於文本的協議,Netty擴展了它的應用範圍及靈活性。

1.2.2 異步和事件驅動

因為我們要大量地使用“異步”這個詞,所以現在是一個澄清上下文的好時機。異步(也就是非同步)事件肯定大家都熟悉。考慮一下電子郵件:你可能會也可能不會收到你已經發出去的電子郵件對應的回複,或者你也可能會在正在發送一封電子郵件的時候收到一個意外的消息。異步事件也可以具有某種有序的關係。通常,你隻有在已經問了一個問題之後才會得到一個和它對應的答案,而在你等待它的同時你也可以做點別的事情。

在日常的生活中,異步自然而然地就發生了,所以你可能沒有對它考慮過多少。但是讓一個計算機程序以相同的方式工作就會產生一些非常特殊的問題。本質上,一個既是異步的又是事件驅動的係統會表現出一種特殊的、對我們來說極具價值的行為:它可以以任意的順序響應在任意的時間點產生的事件。

這種能力對於實現最高級別的可伸縮性至關重要,定義為:“一種係統、網絡或者進程在需要處理的工作不斷增長時,可以通過某種可行的方式或者擴大它的處理能力來適應這種增長的能力。”[12]

異步和可伸縮性之間的聯係又是什麼呢?

  • 非阻塞網絡調用使得我們可以不必等待一個操作的完成。完全異步的I/O正是基於這個特性構建的,並且更進一步:異步方法會立即返回,並且在它完成時,會直接或者在稍後的某個時間點通知用戶。
  • 選擇器使得我們能夠通過較少的線程便可監視許多連接上的事件。

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

在1.3節中,我們將首先看一看Netty的核心組件。現在,隻需要將它們看作是域對象,而不是具體的Java類。隨著時間的推移,我們將看到它們是如何協作,來為在網絡上發生的事件提供通知,並使得它們可以被處理的。

1.3 Netty的核心組件

在本節中我將要討論Netty的主要構件塊:

  • Channel
  • 回調;
  • Future
  • 事件和ChannelHandler

這些構建塊代表了不同類型的構造:資源、邏輯以及通知。你的應用程序將使用它們來訪問網絡以及流經網絡的數據。

對於每個組件來說,我們都將提供一個基本的定義,並且在適當的情況下,還會提供一個簡單的示例代碼來說明它的用法。

1.3.1 Channel

Channel是Java NIO的一個基本構造。

它代表一個到實體(如一個硬件設備、一個文件、一個網絡套接字或者一個能夠執行一個或者多個不同的I/O操作的程序組件)的開放連接,如讀操作和寫操作[13]

目前,可以把Channel看作是傳入(入站)或者傳出(出站)數據的載體。因此,它可以被打開或者被關閉,連接或者斷開連接。

1.3.2 回調

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

Netty在內部使用了回調來處理事件;當一個回調被觸發時,相關的事件可以被一個interface-ChannelHandler的實現處理。代碼清單1-2展示了一個例子:當一個新的連接已經被建立時,ChannelHandlerchannelActive()回調方法將會被調用,並將打印出一條信息。

代碼清單1-2 被回調觸發的ChannelHandler

public class ConnectHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx)
        throws Exception {     --  當一個新的連接已經被建立時,channelActive(ChannelHandlerContext)將會被調用
        System.out.println(
            "Client " + ctx.channel().remoteAddress() + " connected");
    }
}

1.3.3 Future

Future提供了另一種在操作完成時通知應用程序的方式。這個對象可以看作是一個異步操作的結果的占位符;它將在未來的某個時刻完成,並提供對其結果的訪問。

JDK預置了interface java.util.concurrent.Future,但是其所提供的實現,隻允許手動檢查對應的操作是否已經完成,或者一直阻塞直到它完成。這是非常繁瑣的,所以Netty提供了它自己的實現——ChannelFuture,用於在執行異步操作的時候使用。

ChannelFuture提供了幾種額外的方法,這些方法使得我們能夠注冊一個或者多個ChannelFutureListener實例。監聽器的回調方法operationComplete(),將會在對應的操作完成時被調用[15]。然後監聽器可以判斷該操作是成功地完成了還是出錯了。如果是後者,我們可以檢索產生的Throwable。簡而言之,由ChannelFutureListener提供的通知機製消除了手動檢查對應的操作是否完成的必要。

每個Netty的出站I/O操作都將返回一個ChannelFuture;也就是說,它們都不會阻塞。正如我們前麵所提到過的一樣,Netty完全是異步和事件驅動的。

代碼清單1-3展示了一個ChannelFuture作為一個I/O操作的一部分返回的例子。這裏,connect()方法將會直接返回,而不會阻塞,該調用將會在後台完成。這究竟什麼時候會發生則取決於若幹的因素,但這個關注點已經從代碼中抽象出來了。因為線程不用阻塞以等待對應的操作完成,所以它可以同時做其他的工作,從而更加有效地利用資源。

代碼清單1-3 異步地建立連接

Channel channel = ...;
![](/api/storage/getbykey/screenshow?key=1704e6378455b23f17f7)![](/api/storage/getbykey/screenshow?key=1704725cc4b464f4793a)![](/api/storage/getbykey/screenshow?key=17042eacf3010fc856d6)// Does not block
ChannelFuture future = channel.connect(      --  異步地連接到遠程節點
    new InetSocketAddress("192.168.0.1", 25));

代碼清單1-4顯示了如何利用ChannelFutureListener。首先,要連接到遠程節點上。然後,要注冊一個新的ChannelFutureListener到對connect()方法的調用所返回的ChannelFuture上。當該監聽器被通知連接已經建立的時候,要檢查對應的狀態。如果該操作是成功的,那麼將數據寫到該Channel。否則,要從ChannelFuture中檢索對應的Throwable

代碼清單1-4 回調實戰

Channel channel = ...;
// Does not block
ChannelFuture future = channel.connect(   -- 異步地連接到遠程節點
    new InetSocketAddress("192.168.0.1", 25));
future.addListener(new ChannelFutureListener() {    --  注冊一個ChannelFutureListener,以便在操作完成時獲得通知
    @Override
    public void operationComplete(ChannelFuture future) {  --   檢查操作
的狀態
       if (future.isSuccess()){ 
            ByteBuf buffer = Unpooled.copiedBuffer(   -- 如果操作是成功的,則創建一個ByteBuf以持有數據
               "Hello",Charset.defaultCharset());
           ChannelFuture wf = future.channel()
                .writeAndFlush(buffer);    -- 將數據異步地發送到遠程節點。
返回一個ChannelFuture
            ....
        } else {
            Throwable cause = future.cause();  ⇽ -- 如果發生錯誤,則訪問描述原因的Throwable
            cause.printStackTrace();
        }
    }
});

需要注意的是,對錯誤的處理完全取決於你、目標,當然也包括目前任何對於特定類型的錯誤加以的限製。例如,如果連接失敗,你可以嚐試重新連接或者建立一個到另一個遠程節點的連接。

如果你把ChannelFutureListener看作是回調的一個更加精細的版本,那麼你是對的。事實上,回調和Future是相互補充的機製;它們相互結合,構成了Netty本身的關鍵構件塊之一。

1.3.4 事件和ChannelHandler

Netty使用不同的事件來通知我們狀態的改變或者是操作的狀態。這使得我們能夠基於已經發生的事件來觸發適當的動作。這些動作可能是:

  • 記錄日誌;
  • 數據轉換;
  • 流控製;
  • 應用程序邏輯。

Netty是一個網絡編程框架,所以事件是按照它們與入站或出站數據流的相關性進行分類的。可能由入站數據或者相關的狀態更改而觸發的事件包括:

  • 連接已被激活或者連接失活;
  • 數據讀取;
  • 用戶事件;
  • 錯誤事件。

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

  • 打開或者關閉到遠程節點的連接;
  • 將數據寫到或者衝刷到套接字。

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

圖1-3 流經ChannelHandler鏈的入站事件和出站事件

Netty的ChannelHandler為處理器提供了基本的抽象,如圖1-3所示的那些。我們會在適當的時候對ChannelHandler進行更多的說明,但是目前你可以認為每個Channel-Handler的實例都類似於一種為了響應特定事件而被執行的回調。

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

1.3.5 把它們放在一起

在本章中,我們介紹了Netty實現高性能網絡編程的方式,以及它的實現中的一些主要的組件。讓我們大體回顧一下我們討論過的內容吧。

1.Future、回調和ChannelHandler

Netty的異步編程模型是建立在Future和回調的概念之上的, 而將事件派發到ChannelHandler的方法則發生在更深的層次上。結合在一起,這些元素就提供了一個處理環境,使你的應用程序邏輯可以獨立於任何網絡操作相關的顧慮而獨立地演變。這也是Netty的設計方式的一個關鍵目標。

攔截操作以及高速地轉換入站數據和出站數據,都隻需要你提供回調或者利用操作所返回的Future。這使得鏈接操作變得既簡單又高效,並且促進了可重用的通用代碼的編寫。

2.選擇器、事件和EventLoop

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

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

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

1.4 小結

在這一章中,我們介紹了Netty框架的背景知識,包括Java網絡編程API的演變過程,阻塞和非阻塞網絡操作之間的區別,以及異步I/O在高容量、高性能的網絡編程中的優勢。

然後,我們概述了Netty的特性、設計和優點,其中包括Netty異步模型的底層機製,包括回調、Future以及它們的結合使用。我們還談到了事件是如何產生的以及如何攔截和處理它們。

在本書接下來的部分,我們將更加深入地探討如何利用這些豐富的工具集來滿足自己的應用程序的特定需求。

在下一章中,我們將要深入地探討Netty的API以及編程模型的基礎知識,而你則將編寫你的第一款客戶端和服務器應用程序。


[1] W. Richard Stevens的Advanced Programming in the UNIX Environment (Addison-Wesley, 1992)第364頁“4.3BSD returned EWOULDBLOCK if an operation on a non-blocking descriptor could not complete without blocking”。

[2] 也稱為I/O多路複用,該接口從最初的select()poll()調用到更加高性能的實現,已經演變了很多年。參見Sangjin Han的文章《Scalable Event Multiplexing: epoll vs. kqueue》(www.eecs.berkeley.edu/~ sangjin/2012/12/21/epoll-vs-kqueue.html)。

[3] 這裏指支撐更多的並發的客戶端。——譯者注

[4] 這裏指熟悉這些底層的API的人員少。——譯者注

[5] Spring框架大概是最出名的,並且實際上是一個完整的應用程序框架的生態係統,處理了對象的創建、批量處理、數據庫編程等。

[6] Netty在2011年榮獲了Duke’s Choice Award的殊榮,參見www.java.net/dukeschoice/2011。

[7] 最新的版本編譯需要JDK 1.8+,參見https://github.com/netty/netty/pull/6392。——譯者注

[8] 還包括炙手可熱的大數據處理引擎Spark。——譯者注

[9] 完整的已知采用者列表參見https://netty.io/wiki/adopters.html。

[10] 關於Finagle的更多信息參見https://twitter.github.io/finagle/。

[11] 第15章和第16章的案例研究描述了這裏提到的公司中的一些是如何使用Netty來解決現實世界的問題的。

[12] André B. Bondi的Proceedings of the second international workshop on Software and performance— WOSP’00 (2000)第195頁,“Characteristics of scalability and their impact on performance”。

[13] Java平台,標準版第8版API規範,java.nio.channels,Channel:https://docs.oracle.com/javase/8/docs/ api/java/nio/channels/package-summary.html。

[14] 指接受回調的方法。——譯者注

[15] 如果在ChannelFutureListener添加到ChannelFuture的時候,ChannelFuture已經完成,那麼該ChannelFutureListener將會被直接地通知。——譯者注

轉載自 並發編程網 - ifeve.com

最後更新:2017-05-18 20:36:42

  上一篇:go  《Flink官方文檔》Batch Examples(一)
  下一篇:go  《Netty實戰》Netty In Action中文版 第1章——Netty——異步和事件驅動(一)