假設你即將要為一個重要的大公司開發一個新的關鍵任務的應用程序。在第一次會議上,你瞭解到這個系統必須能無性能損耗地擴展到支持15萬個並發用戶。這時所有的人都看著你,你會說什麼?

如果你自信地說「沒問題」,那麼真心佩服你!但是我們中的大部分人可能還是說得更謹慎些,比如「聽起來是可行的」。然後,一旦回到電腦前,我們就會開始搜索「高性能Java網路編程(high performance Java networking)」。

如今,如果你搜索這個關鍵字,在第一頁的結果中,你將會看到這個:

Netty: Home

netty.io/

Netty是一個用於快速開發可維護的高性能協議伺服器和客戶端的非同步的事件驅動網路應用框架

如果你像很多人一樣,通過這種方式發現了Netty,那麼接下來你也許就會瀏覽官網,下載代碼,研讀Javadocs和一些博客,同時開始寫代碼。如果你已經有了紮實的網路編程經驗,你也許會取得很大的進步;否則的話,可能不會。

為什麼這麼說呢?實現像上述例子中的高性能系統需要超一流的編程技術,需要諸多複雜領域的經驗,包括網路,多線程和並發。Netty讓甚至是網路編程的新手都可以運用到這些專業知識。但到目前為止,缺少一份全面的指導使得這個習得過程變得很困難,所以纔有了本書。

我們寫這本書的主要目的是使Netty被最大可能範圍的開發人員所接受。這包括那些有創新內容和服務,但是既沒有時間也無意成為網路專家的人。如果你也是其中一員,我們相信你會驚喜地發現你會很快地準備好創建你的第一個Netty應用。另一方面,我們致力於支持那些尋找開發私有網路協議工具的高級開發者。

事實上,Netty確實提供了一個非常豐富的網路開發工具包,我們會花大部分的時間來探索它的性能。但是Netty終究是一個框架,它的架構方法和設計原則同它的技術內容一樣重要。因此,我們還將會提到這些:

- 關注點分離(解耦業務和網路邏輯)

- 模塊化和可重用性

- 做為第一需求的易測性

在第一章,我們先介紹高性能網路的背景,特別是高性能網路在Java開發套件(JDK)中的實現。在這個背景下,我們會引入Netty,以及它的核心概念和組件。到本章的結尾,你將會準備好創建你的第一個基於Netty的客戶端和伺服器。

1.1 Java網路編程

早年就開始網路編程的開發者會花很多時間學習C語言socket庫的繁複細節,還要對付在不同操作系統上可能突然出現的異常。早期的Java版本(1995-2002)引入了一個面向對象的外觀,來掩蓋這些棘手的細節,但同時也創建了一個複雜的需要很多樣本文件代碼(boilerplate code)的客戶端/伺服器協議(和為了讓這個協議工作順暢大量查閱底層細節的工作)。

那些第一代的Java APIs(java.net) 只支持本地系統的socket庫提供的所謂阻塞函數。下面的代碼清單是伺服器端代碼調用這些函數的例子:

代碼清單 1.1 阻塞I/O舉例

這個代碼清單實現了一個基本的Socket API 模式。最重要的幾個要點:

- accept() 一直阻塞直到ServerSocket上的連接建立完成(1), 然後返回一個新的socket,用於客戶端和伺服器直接的通訊。然後ServerSocket繼續監聽新的連接

- BufferedReader和PrintWriter是從這個Socket的輸入輸出流中導出的(2)。前者可以從一個字元輸入流中讀入文本,後者用於列印格式化的對象到一個文本輸出流

- readLine()阻塞直到讀到以換行或者回車結尾的字元串(3)

- 處理客戶端請求(4)

這份代碼一次只能處理一個連接。為了處理多個並發的客戶端,你需要為每個新的客戶端Socket分配一個新的線程,如圖1.1所示

圖1.1 使用阻塞I/O的多個連接

讓我們考慮下這個方案可能帶來的結果。首先,在任何時候,很多線程是休眠的狀態,在等待輸入輸出數據。這可能是個資源的浪費。第二,每個線程,根據操作系統的不同,需要分配默認大小從64KB到1M不等的棧內存。第三, 即使一個Java虛擬機(JVM)物理上可以支持很大數量的線程,早在這個數量到達上限前,比如達到1萬個連接的時候,上線文切換的開銷就已經成了大問題

雖然這個針對並發的方案對中小規模數量的客戶端也許能接受,但支持1萬或者更多並發連接所需的資源讓這個方案遠不夠理想。幸運的是,我們還有其他選擇。

1.1.1 Java NIO

除了列在代碼清單1.1裏的阻塞系統調用底層代碼, 本地的socket庫早就包含了非阻塞調用,這實現了對網路資源使用的更多控制。

- 用setsockopt()你可以配置sockets,如果沒有數據,讀/寫調用會立即返回;原本,一個阻塞調用會一直阻塞;

- 你可以通過用系統的事件通知API註冊一組非阻塞sockets,來判斷是否這些sockets中的任何一個有待讀取的數據

2002年,Java在JDK1.4的java.nio包中開始支持非阻塞I/O。

是「新(new)」還是「非(non)」阻塞?

NIO一開始是新輸入/輸出(New Input/Output)的縮略詞,但是這個Java API已經存在了夠長的事件,所以不再那麼新了。目前大部分使用者把NIO看做非阻塞的簡稱,而阻塞I/O是OIO或者說老輸入/輸出(old Input/Output)。你也許還能碰到普通(plain) I/O的說法。

1.1.2 Selectors

圖2是一個非阻塞的例子,從事實上克服了前一小節那個例子的缺點。

圖1.2 使用Selector的非阻塞I/O

Java.nio.channels.Selectors這個類是Java 非阻塞I/O實現的關鍵。它採用了事件通知API來表明在一組非阻塞sockets中,誰已經準備好讀寫。因為在任何時候任意讀寫的操作都會被檢查它的完成狀況,一個單獨的線程,如圖1.2所示,可以處理多個並發的連接。

總體上來說,這個模型比阻塞的I/O模型提供了更好的資源管理:

- 很多連接被更少的線程處理,因此由內存管理和上下文切換帶來的開銷更少;

- 當沒有I/O需要處理時,線程可以被指派其他任務

雖然很多應用程序直接使用了Java NIO API,但是要安全地使用並且正確無誤並非易事。特別在高負載下,可靠並且高效地在處理和指派I/O是一項困難和容易出錯的任務。這個任務最好交給高性能網路專家-Netty。

1.2 Netty簡介

不久以前,我們在一開始展現的那個場景—支持成千上萬的並發用戶—會被認為是不可能的事。如今,做為系統用戶,我們認為這種能力是理所當然的,做為開發者,我們期望這個極限(bar)可以更高。我們知道,總會出現對更大吞吐量,更高擴展性的低成本需求。

不要低估上一個觀點的重要性。我們從漫長而痛苦的經歷中學習到,低層次APIs的直接使用會暴露複雜度,同時帶來對稀缺技能的嚴重依賴。所以,一個面向對象的基本概念是:用簡化的抽象隱藏底層實現的複雜性。

這個原則刺激了許多框架的出現,這些框架將常規編程任務封裝成現成解決的方案,很多還和分散式系統開發密切相關。也許可以這麼斷言,所有的職業Java開發者熟悉至少一種框架。對於我們很多人來說,這些框架是必不可少的,不僅滿足了我們的技術需求,也滿足了我們的開發進度。

在網路編程領域,Netty是出色的Java框架。通過使用簡單易用的API來享用到Java高級APIs的威力,Netty讓你可以專註於真正讓你感興趣的東西——你的應用的獨特價值

在我們開始仔細瞭解Netty之前,請仔細看下錶1.1中總結的Netty關鍵特性。有些是技術性的,其他則是結構性的或者有哲學意味的。在這本書中我們會不止一次再來回顧這些特性。

表1.1 Netty特性總結

分類Netty特性設計用於多種傳輸類型的統一API,包括阻塞和非阻塞。簡單但是強大的線程模型真正的無連接數據報socket支持鏈式的支持復用的邏輯組件(Chaining of logic components to support reuse)易用性大量的文檔(Javadoc)和例子庫除了JDK1.6+沒有別的依賴(一些可選特性可能需要Java 1.7+和/或 額外的依賴)性能比core Java APIs更好的吞吐量和低延遲因為池化和復用,減少了資源消耗儘可能小的內存拷貝魯棒性沒有因為慢連接,快連接或者超載連接造成的OutOfMemeoryError在高速的網路上消除了NIO應用不公平的讀/寫比例安全完整的SSL/TLS和StartTLS的支持適用於受限的環境比如Applet或者OSGI中社區推動早發布,頻繁發布

1.2.1 誰在用Netty?

Netty有一個活躍的不斷成長的使用團體,除了一些流行的開源代碼項目比 Infinispan,HornetQ,Vert.x,Apache Cassandra和Elastic seartch,還包括一些大公司,比如Apple,Twitter,Facebook,Google,Square和Instagram。他們都在覈心代碼裏採用了Netty強大的網路抽象。在初創公司中,Firebase和Urban Airship在使用Netty,前者用於HTTP長連接,後者用於各種推送通知。

每當你用Twitter時,你就在用Finagle,一個用於內部系統通信的基於Netty的框架。Facebook在他們的Apache Thrift服務Nifty中用了Netty。可擴展性和性能是這兩個公司的關注重點,同時他們也是Netty項目的定期貢獻者。

反過來,Netty也從這些項目中獲益,通過實現比如FTP,SMTP,HTTP,WebSocket和其他基於文本及二進位的協議,提高了Netty的應用範圍和靈活性。

1.2.2 非同步和事件驅動

我們會頻繁用非同步這個詞,所以現在是闡明其背景的好時機。非同步,也就是非同步的事件大家肯定很熟悉了。比如電子郵件:你可能,也可能不會立刻收到你發出的消息的回復,或者你會在發消息的同時收到一個意外的消息。非同步的事件也可以有個有序的關係。比如你通常會在你問了問題之後纔得到這個問題的答案,並且你在等答案的同時可以做一些別的事情。

在日常生活中,非同步就這麼發生了,所以你可能都沒多想。但是要讓一個電腦程序也這麼工作會出現一些非常特殊的問題。本質上,一個非同步和事件驅動的系統向會我們展現一個特殊的非常有價值的狀態:它可以響應任何時候任何順序發生的事件。

這個能力對於獲取最高級別的可擴展性是很關鍵的。可擴展性是指「一個系統,網路或者程序能以擴展來滿足持續增長的工作量的能力」

非同步和可擴展性直接有什麼聯繫呢?

- 非阻塞網路調用讓我們不用再一直等到操作完成:一個非同步的方法立即返回,並且當完成時再直接或者稍後通知用戶

- Selectors讓我們可以用更少的線程監控許多連接事件的發生

把這些元素放在一起,採用非阻塞I/O,我們可以比阻塞I/O更快速更經濟地處理非常大數目的事件。從網路編程的角度講,這是我們希望創建的系統的關鍵,接下來你會看到,這也是Netty由下至上設計的關鍵。

在下一小節我們會先了解下Netty的核心組件。目前,你可以把這些組件看成域對象而不是具體的Java類。過一段時間,我們會看到它們是如何協同合作來通知發生在網路上的事件,並且讓這些事件變得可被處理的。

1.3 Netty的核心組件

在這一節我們會討論Netty的主要組成模塊:

- Channels

- Callbacks

- Futures

- Events和handlers

這些模塊代表了不同類型的概念:資源,邏輯和通知。你的應用將會利用這些模塊來獲取網路和網路上的數據。

對每個組件,我們會給出一個基本的定義,並且在合適的情況下,用一個簡單的代碼實例說明它的用法。

1.3.1 Channels

一個Channel是Java NIO的一個基本抽象。它代表了:一個連接到比如硬體設備,文件,網路socket等實體的開放連接,或者是一個能夠完成一種或多種譬如讀或寫等不同I/O操作的程序

目前,可以把一個Channel想像成一個輸入和輸出數據的媒介。同樣地,它可以被打開或者關閉,連接或者斷開。

1.3.2 Callbacks

一個callback就是一個方法,一個提供給另一個的方法的引用。這讓另一個方法可以在適當的時候回過頭來調用這個callback方法。Callbacks在很多編程情形中被廣泛使用,是用於通知相關方某個操作已經完成最常用的方法之一。

Netty在處理事件時內部使用了callback;當一個callback被觸發,事件可以被ChannelHandler的介面實現處理。下面的代碼清單是這樣一個例子:當一個新的連接建立後,ChannelHandler的callback方法channelActive()會被調用,然後列印一條消息。

代碼清單 1.2 ChannelHandler被一個callback觸發

1.3.3 Future

一個Future提供了另一個當操作完成時如何通知應用的方法。Future對象充當了一個存放非同步操作結果的佔位符(placeholder)角色;它會在將來某個時間完成並且提供對操作結果的訪問。

JDK搭載了介面java.util.concurrent.Future, 但是提供的介面實現只允許你手動檢查操作是否已經完成,或者就一直阻塞到操作完成。這非常麻煩,所以Netty提供了它自己的ChannelFuture實現,用於執行非同步操作。

ChannelFuture提供了額外的方法讓我們可以註冊一個或者多個ChannelFutureListener實例。監聽者的callback方法operationComplete()在操作完成時被調用。然後監聽者可以查看這個操作是否成功完成,還是出錯了。如果出錯了,我們可以從future獲取Throwable。簡單來說,ChannelFutureListener提供的通知機制免去了手動檢查操作完成情況的麻煩。

每個Netty輸出的I/O操作都會返回一個ChannelFuture;就是說,沒有一個操作是阻塞的。就像我們之前所說的,Netty由下至上都是非同步和事件驅動的。

代碼清單1.3中,一個ChannelFuture做為一個I/O操作的一部分被返回。這裡,connect()會無阻塞地直接返回,調用會在後臺完成。什麼時候會完成取決於多個因素,但是這個擔心已經從代碼裏被抽離(abstract away)出來了。因為這個線程沒有阻塞在等待這個操作完成,它可以同時做其他事情,因此更有效率地利用資源。

代碼清單1.3 非同步連接

碼清單1.4展示瞭如何利用ChannelFutureListener。首先你連接到一個遠端。然後用connect()返回的ChannelFuture註冊一個新的ChannelFutureListener。當監聽器被通知連接建立時,檢查狀態(1)。如果這個操操作成功,你寫數據到這個Channel。否則你從ChannelFuture中讀取Throwable。

代碼清單1.4 運作中的Callback

注意錯誤處理完全取決於你,取決於某個具體錯誤施加的限制。比如說,連接錯誤發生時,你可以試著重連或者和另一個遠端建立連接。

如果你認為一個ChannelFutureListener是一個callback的複雜版本,那你想對了。事實上,callbacks和Futures是互補的機制;兩者結合起來構成了Netty的關鍵模塊之一。

1.3.4 Events和handlers

Netty用細分的events來通知我們狀態的變化或者操作的狀況。這讓我們可以基於發生的events來觸發適當的行為。這類行為可能包括

- 日誌記錄

- 數據傳送- 流控制- 應用邏輯

Netty是一個網路編程框架,所以events按它們和輸入或者輸出數據流的關係來分類。可能被輸入數據或者相關狀態改變觸發的events包括:

- 活躍或者停用的連接

- 讀數據- 用戶events- 錯誤events

而輸出event則是會觸發將來行為的操作的結果,可能會是:

- 打開或者關閉到遠端的連接

- 寫或者刷數據到一個socket

每一個event都可以被分派到一個用戶實現的handler對象的方法。這是一個事件驅動的模型如何直接轉變為應用模塊的好例子。圖1.3展示了一個event如何被一串這樣的event handler處理。

圖1-3 經過一串ChannelHandler的輸入輸出事件

Netty的ChannelHandler提供瞭如圖1.3中展示的handler的基本抽象。我們在適當的時候會更多地談論到ChannelHandler,但是現在你可以認為每個handler實例就是一種響應某個具體event的callback。

Netty提供了大量你可以馬上拿來用的預定義handler,包括HTTP和SSL/TLS等協議的handler。在內部,ChannelHandler自己也用events和futures,和你的應用是同樣抽象的消費者。

1.3.5 匯總

在這一章,你初次接觸了Netty針對高性能網路編程的方案,以及Netty實現的一些主要模塊。讓我們把討論過的東西總結下。

FUTURES,CALLBACKS和HANDLERS

Netty的非同步編程模型是建立在Futures和callbacks概念之上的,在更深一層分派事件到handler方法。這些元素結合起來提供了一個處理環境,讓你的應用邏輯可以逐步發展而不用關心網路操作。這一個Netty設計方案的一個關鍵目標。

快速地攔截操作和傳送輸入輸出數據只要求你提供callbacks或者用操作返回的Futures。這讓鏈式操作變得容易和有效,同時促進了可重用和通用代碼的編寫。

SELECTORS, EVENTS, AND EVENT LOOPS

Netty通過引發事件把Selector從應用中抽象出來,省掉了所有原本需要手寫得調度代碼。在內部,一個EventLoop被分配到每個Channel來處理所有的events,包括

- 註冊感興趣的events

- 分派events到ChannelHandlers- 安排將來的行為

EventLoop自己僅由一個線程驅動,這個線程處理一個Channel所有的I/O事件,這個關係在Eventloop的生命週期內不會改變。這個簡單強大的設計消除了任何你可能對ChannelHandler同步的顧慮,因此你能夠專註於在數據被處理時,提供正確的執行邏輯。在我們詳細探討Netty的線程模型時將會看到的,這個API簡潔並且緊湊。

1.4 小結

在這一章,我們瞭解了Netty的背景,包括Java網路編程API的進化史,阻塞和非阻塞網路操作的區別,以及非同步操作在高容量高性能網路方面的優勢。

然後我們對Netty的特性,設計和優點進行了概述。涉及Netty非同步模型下的機制,包括callbacks,Futures,和它們的組合使用。我們也提到了事件是如何產生的以及事件是如果被解析和處理的。

下一步,我們會更深入地探討這個豐富的工具集是如何被用來滿足你的應用的具體需求。

在下一章,我們會探究Netty API和編程模型的基本要點,而你會開始寫你的第一個客戶端和伺服器。


本文譯自《Netty in action》,有興趣的同學評論或私信,獲取原著版書籍PDF!關注《程序員權威指南》公眾號,獲取書籍和視頻資料,我在這等著你的加入!


推薦閱讀:
相關文章