[TOC](Nett的概念及體繫結構) # 第一章 Java網路編程 最早期的 Java API(java.net)只支持由本地系統套接字型檔提供的所謂的阻塞函數,像下麵的那樣 ```java //創建一個新的 ServerSocket,用以監聽指定埠上的連接請求 ServerSocket serv ...
目錄
第一章 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 是簡單而緊湊的。