1. 程式人生 > >《Netty實戰》Netty In Action中文版 第1章——Netty——非同步和事件驅動

《Netty實戰》Netty In Action中文版 第1章——Netty——非同步和事件驅動

  • Java網路程式設計
  • Netty簡介
  • Netty的核心元件

假設你正在為一個重要的大型公司開發一款全新的任務關鍵型的應用程式。在第一次會議上,你得知該系統必須要能夠擴充套件到支撐150 000名併發使用者,並且不能有任何的效能損失,這時所有的目光都投向了你。你會怎麼說呢?

如果你可以自信地說:“當然,沒問題。”那麼大家都會向你脫帽致敬。但是,我們大多數人可能會採取一個更加謹慎的立場,例如:“聽上去是可行的。”然後,一回到計算機旁,我們便開始搜尋“high performance Java networking”(高效能Java網路程式設計)。

如果你現在搜尋它,在第一頁結果中,你將會看到下面的內容:

Netty: Home

netty.io/

Netty是一款非同步的事件驅動的網路應用程式框架,支援快速地開發可維護的高效能的面向協議的伺服器和客戶端。

如果你和大多數人一樣,通過這樣的方式發現了Netty,那麼你的下一步多半是:瀏覽該網站,下載原始碼,仔細閱讀Javadoc和一些相關的部落格,然後寫點兒程式碼試試。如果你已經有了紮實的網路程式設計經驗,那麼可能進展還不錯,不然則可能是一頭霧水。

這是為什麼呢?因為像我們例子中那樣的高效能系統不僅要求超一流的程式設計技巧,還需要幾個複雜領域(網路程式設計、多執行緒處理和併發)的專業知識。Netty優雅地處理了這些領域的知識,使得即使是網路程式設計新手也能使用。但到目前為止,由於還缺乏一本全面的指南,使得對它的學習過程比實際需要的艱澀得多——因此便有了這本書。

我們編寫這本書的主要目的是:使得Netty能夠儘可能多地被更加廣泛的開發者採用。這也包括那些擁有創新的內容或者服務,卻沒有時間或者興趣成為網路程式設計專家的人。如果這適用於你,我們相信你將會非常驚訝自己這麼快便可以開始建立你的第一款基於Netty的應用程式了。當然在另一個層面上講,我們也需要支援那些正在尋找工具來建立他們自己的網路協議的高階從業人員。

Netty確實提供了極為豐富的網路程式設計工具集,我們將花大部分的時間來探究它的能力。但是,Netty終究是一個框架,它的架構方法和設計原則是:每個小點都和它的技術性內容一樣重要,窮其精妙。因此,我們也將探討很多其他方面的內容,例如:

  • 關注點分離——業務和網路邏輯解耦;
  • 模組化和可複用性;
  • 可測試性作為首要的要求。

在這第1章中,我們將從一些與高效能網路程式設計相關的背景知識開始鋪陳,特別是它在Java開發工具包(JDK)中的實現。有了這些背景知識後,我們將介紹Netty,它的核心概念以及構建塊。在本章結束之後,你就能夠編寫你的第一款基於Netty的客戶端和伺服器應用程式了。

1.1 Java網路程式設計

早期的網路程式設計開發人員,需要花費大量的時間去學習複雜的C語言套接字型檔,去處理它們在不同的作業系統上出現的古怪問題。雖然最早的Java(1995—2002)引入了足夠多的面向物件façade(門面)來隱藏一些棘手的細節問題,但是建立一個複雜的客戶端/伺服器協議仍然需要大量的樣板程式碼(以及相當多的底層研究才能使它整個流暢地執行起來)。

那些最早期的Java API(java.net)只支援由本地系統套接字型檔提供的所謂的阻塞函式。程式碼清單1-1展示了一個使用了這些函式呼叫的伺服器程式碼的普通示例。

程式碼清單1-1 阻塞I/O示例

ServerSocket serverSocket = new ServerSocket(portNumber);      --  建立一個新的ServerSocket,用以監聽指定埠上的連線請求
Socket clientSocket = serverSocket.accept();     --    accept()方法的呼叫將被阻塞,直到一個連線建立
BufferedReader in = new BufferedReader(   
    new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out =
    new PrintWriter(clientSocket.getOutputStream(), true);      --    這些流物件都派生於該套接字的流物件
String request, response;
while ((request = in.readLine()) != null) {     --    處理迴圈開始
    if ("Done".equals(request)) {
        break;      --  如果客戶端傳送了“Done”,則退出處理迴圈
    }
    response = processRequest(request);      --    請求被傳遞給服
務器的處理方法
    out.println(response);      --  伺服器的響應被髮送給了客戶端
}     --  繼續執行處理迴圈

44程式碼清單1-1實現了Socket API的基本模式之一。以下是最重要的幾點。

  • ServerSocket上的accept()方法將會一直阻塞到一個連線建立❶,隨後返回一個新的Socket用於客戶端和伺服器之間的通訊。該ServerSocket將繼續監聽傳入的連線。
  • BufferedReaderPrintWriter都衍生自Socket的輸入輸出流❷。前者從一個字元輸入流中讀取文字,後者列印物件的格式化的表示到文字輸出流。
  • readLine()方法將會阻塞,直到在❸處一個由換行符或者回車符結尾的字串被讀取。
  • 客戶端的請求已經被處理❹。

這段程式碼片段將只能同時處理一個連線,要管理多個併發客戶端,需要為每個新的客戶端Socket建立一個新的Thread,如圖1-1所示。

圖1-1 使用阻塞I/O處理多個連線

讓我們考慮一下這種方案的影響。第一,在任何時候都可能有大量的執行緒處於休眠狀態,只是等待輸入或者輸出資料就緒,這可能算是一種資源浪費。第二,需要為每個執行緒的呼叫棧都分配記憶體,其預設值大小區間為64 KB到1 MB,具體取決於作業系統。第三,即使Java虛擬機器(JVM)在物理上可以支援非常大數量的執行緒,但是遠在到達該極限之前,上下文切換所帶來的開銷就會帶來麻煩,例如,在達到10 000個連線的時候。

雖然這種併發方案對於支撐中小數量的客戶端來說還算可以接受,但是為了支撐100 000或者更多的併發連線所需要的資源使得它很不理想。幸運的是,還有一種方案。

1.1.1 Java NIO

除了程式碼清單1-1中程式碼底層的阻塞系統呼叫之外,本地套接字型檔很早就提供了非阻塞呼叫,其為網路資源的利用率提供了相當多的控制:

  • 可以使用setsockopt()方法配置套接字,以便讀/寫呼叫在沒有資料的時候立即返回,也就是說,如果是一個阻塞呼叫應該已經被阻塞了[1]
  • 可以使用作業系統的事件通知API[2]註冊一組非阻塞套接字,以確定它們中是否有任何的套接字已經有資料可供讀寫。

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.1.2 選擇器

圖1-2展示了一個非阻塞設計,其實際上消除了上一節中所描述的那些弊端。

圖1-2 使用Selector的非阻塞I/O

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

總體來看,與阻塞I/O模型相比,這種模型提供了更好的資源管理:

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

儘管已經有許多直接使用Java NIO API的應用程式被構建了,但是要做到如此正確和安全並不容易。特別是,在高負載下可靠和高效地處理和排程I/O操作是一項繁瑣而且容易出錯的任務,最好留給高效能的網路程式設計專家——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] 完整的已知採用者列表參見http://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:http://docs.oracle.com/javase/8/docs/ api/java/nio/channels/package-summary.html。

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

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