Java中的NIO與Netty框架

前言

  隨着移動互聯網的爆發性增長,小明公司的電子商務系統訪問量越來越大,由於現有系統是個單體的巨型應用,已經無法滿足海量的併發請求,拆分勢在必行。

  在微服務的大潮之中, 架構師通常會把系統拆分成了多個服務,根據需要部署在多個機器上,這些服務非常靈活,可以隨着訪問量彈性擴展。

  世界上沒有免費的午餐, 拆分成多個“微服務”以後雖然增加了彈性,但也帶來了一個巨大的挑戰:各個服務之間互相調用的開銷。

  比如說:原來用戶下一個訂單需要登錄,瀏覽產品詳情,加入購物車,支付,扣庫存等一系列操作,在單體應用的時候它們都在一臺機器的同一個進程中,說白了就是模塊之間的函數調用,效率超級高。

  現在好了,服務被安置到了不同的服務器上,一個訂單流程,幾乎每個操作都要越網絡,都是遠程過程調用(RPC), 那執行時間、執行效率可遠遠比不上以前了。

  遠程過程調用的第一版實現使用了HTTP協議,也就是說各個服務對外提供HTTP接口。HTTP協議雖然簡單明瞭,但是太過繁瑣太多,僅僅是給服務器發個簡單的消息都會附帶一大堆無用信息:

GET /orders/1 HTTP/1.1                                                                                             
Host: order.myshop.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; )
Accept: text/html;
Accept-Language: en-US,en;
Accept-Encoding: gzip
Connection: keep-alive

  看看那User-Agent,Accept-Language ,這個協議明顯是爲瀏覽器而生的!對於各個應用程序之間的調用,用HTTP協議得不償失。

  能不能自定義一個精簡的協議? 在這個協議中我們只需要把要調用方法名和參數發給服務器即可,根本不用這麼多亂七八糟的額外信息。

  但是自定義協議客戶端和服務器端就得直接使用“低級”的Socket了,尤其是服務器端,得能夠處理高併發的訪問請求才行。

阻塞IO與非阻塞IO

  至於服務器端的socket編程,最早的Java是所謂的阻塞IO(Blocking IO), 想處理多個socket的連接的話需要創建多個線程, 一個線程對應一個。

  這種方式寫起來倒是挺簡單的,但是連接(socket)多了就受不了了,如果真的有成千上萬個線程同時處理成千上萬個socket,佔用大量的空間不說,光是線程之間的切換就是一個巨大的開銷。

  更重要的是,雖然有大量的socket,但是真正需要處理的(可以讀寫數據的socket)卻不多,大量的線程處於等待數據狀態(這也是爲什麼叫做阻塞的原因),資源浪費得讓人心疼。

  後來Java爲了解決這個問題,又搞了一個非阻塞IO(NIO:Non-Blocking IO,有人也叫做New IO), 改變了一下思路:通過多路複用的方式讓一個線程去處理多個Socket。

  這樣一來,只需要使用少量的線程就可以搞定多個socket了,線程只需要通過Selector去查一下它所管理的socket集合,哪個Socket的數據準備好了,就去處理哪個Socket,一點兒都不浪費。

  這樣一來,只需要使用少量的線程就可以搞定多個socket了,線程只需要通過Selector去查一下它所管理的socket集合,哪個Socket的數據準備好了,就去處理哪個Socket,一點兒都不浪費。

  Java NIO 由三個核心組件組件:
  Buffer
  Channel
  Selector

  緩衝區和通道是NIO中的核心對象,通道Channel是對原IO中流的模擬,所有數據都要通過通道進行傳輸;Buffer實質上是一個容器對象,發送給通道的所有對象都必須首先放到一個緩衝區中。

  Buffer是一個數據對象,它包含要寫入或者剛讀出的數據。這是NIO與IO的一個重要區別,我們可以把它理解爲固定數量的數據的容器,它包含一些要寫入或者讀出的數據。在面向流的I/O中你將數據直接寫入或者將數據直接讀到stream中,在Java NIO中,任何時候訪問NIO中的數據,都需要通過緩衝區(Buffer)進行操作。讀取數據時,直接從緩衝區中讀取,寫入數據時,寫入至緩衝區。緩衝區實質上是一個數組。通常它是一個字節數組,但是也可以使用其他種類的數組。但是一個緩衝區不僅僅是一個數組。緩衝區提供了對數據的結構化訪問,而且還可以跟蹤系統的讀/寫進程。簡單的說Buffer是:一塊連續的內存塊,是NIO數據讀或寫的中轉地。NIO最常用的緩衝區則是ByteBuffer。下圖是 Buffer 繼承關係圖:

  從類圖可以看出NIO爲所有的原始數據類型都實現了Buffer緩存的支持。並且看JDK_API可以得知除了ByteBuffer中的方法有所不同之外,其它類中的方法基本相同。

  Channel 是一個對象,可以通過它讀取和寫入數據。拿 NIO 與原來的 I/O 做個比較,通道就像是流。正如前面提到的,所有數據都通過 Buffer 對象來處理。你永遠不會將字節直接寫入通道中,相反,你是將數據寫入包含一個或者多個字節的緩衝區。同樣,你不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。簡單的說Channel是:數據的源頭或者數據的目的地,用於向buffer提供數據或者讀取buffer數據,並且對I/O提供異步支持

  下圖是 Channel 的類圖

  Channel 爲最頂層接口,所有子 Channel 都實現了該接口,它主要用於 I/O 操作的連接。定義如下:

public interface Channel extends Closeable {
    /**
     * 判斷此通道是否處於打開狀態。 
     */
    public boolean isOpen();
    /**
     *關閉此通道。
     */
    public void close() throws IOException;
}

  最爲重要的Channel實現類爲:
  FileChannel:一個用來寫、讀、映射和操作文件的通道
  DatagramChannel:能通過UDP讀寫網絡中的數據
  SocketChannel: 能通過TCP讀寫網絡中的數據
  ServerSocketChannel:可以監聽新進來的 TCP 連接,像 Web 服務器那樣。對每一個新進來的連接都會創建一個SocketChannel

  使用以下三個方法可以得到一個FileChannel的實例

FileInputStream.getChannel()
FileOutputStream.getChannel()
RandomAccessFile.getChannel()

  上面提到Channel是數據的源頭或者數據的目的地,用於向bufer提供數據或者從buffer讀取數據。那麼在實現了該接口的子類中應該有相應的read和write方法。

  在FileChannel中有以下方法可以使用:

/**
 * 讀取一串數據到緩衝區
 */
public long read(ByteBuffer[] dsts)
/**
 * 將緩衝區中指定位置的一串數據寫入到通道
 */
public long write(ByteBuffer[] srcs)

  多路複用器 Selector,它是 Java NIO 編程的基礎,它提供了選擇已經就緒的任務的能力。從底層來看,Selector 提供了詢問通道是否已經準備好執行每個 I/O 操作的能力。簡單來講,Selector 會不斷地輪詢註冊在其上的 Channel,如果某個 Channel 上面發生了讀或者寫事件,這個 Channel 就處於就緒狀態,會被 Selector 輪詢出來,然後通過 SelectionKey 可以獲取就緒 Channel 的集合,進行後續的 I/O 操作

  Selector 就是你註冊對各種 I/O 事件的地方,而且當那些事件發生時,就是這個對象告訴你所發生的事件。Selector 允許一個線程處理多個 Channel ,也就是說只要一個線程複雜 Selector 的輪詢,就可以處理成千上萬個 Channel ,相比於多線程來處理勢必會減少線程的上下文切換問題

/**
 * 第一步:創建一個Selector
 */
Selector selector = Selector.open();
/**
 * 第二步:打開一個遠程連接
 */
InetSocketAddress socketAddress =
new InetSocketAddress("www.baidu.com", 80);
SocketChannel sc = SocketChannel.open(socketAddress);
sc.configureBlocking(false);
/**
 * 第三步:選擇鍵,註冊
 */
SelectionKey key = sc.register(selector, SelectionKey.OP_CONNECT);
/**
 * 註冊時第一個參數總是當前的這個selector。
 * 註冊讀事件
 * 註冊寫事件
 */
SelectionKey key = sc.register(selector, SelectionKey.OP_READ);
SelectionKey key = sc.register(selector, SelectionKey.OP_WRITE);
/**
 * 第四步:內部循環處理
 */
int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
    SelectionKey key = (SelectionKey)it.next();
    SelectionKey selectionKey = iterator.next();
    iterator.remove();
    //handleKey(selectionKey);
    // ... deal with I/O event ...
}

  首先,我們調用 Selector 的 select() 方法。這個方法會阻塞,直到至少有一個已註冊的事件發生。當一個或者更多的事件發生時, select() 方法將返回所發生的事件的數量。該方法必須首先執行

  接下來,我們調用 Selector 的 selectedKeys() 方法,它返回發生了事件的 SelectionKey 對象的一個集合。SelectionKey中共定義了四種事件,OP_ACCEPT(socket accept)、OP_CONNECT(socket connect)、OP_READ(read)、OP_WRITE(write)。我們通過迭代 SelectionKeys 並依次處理每個 SelectionKey 來處理事件。對於每一個 SelectionKey,您必須確定發生的是什麼 I/O 事件,以及這個事件影響哪些 I/O 對象

  在處理 SelectionKey 之後,我們幾乎可以返回主循環了。但是我們必須首先將處理過的 SelectionKey 從選定的鍵集合中刪除。如果我們沒有刪除處理過的鍵,那麼它仍然會在主集合中以一個激活的鍵出現,這會導致我們嘗試再次處理它。我們調用迭代器的 remove() 方法來刪除處理過的 SelectionKey

基於NIO的高併發RPC框架

  開發一個具有較好的穩定性和可靠性的 NIO 程序還是挺有難度的。於是 Netty 出現,把我們從水深火熱當中解救出來。說說Netty到底是何方神聖, 要解決什麼問題吧。回到前言中提到的例子,如果使用Java NIO來自定義一個高性能的RPC框架,調用協議,數據的格式和次序都是自己定義的,現有的HTTP根本玩不轉,那使用Netty就是絕佳的選擇。

  使用Netty的開源框架,可以快速地開發高性能的面向協議的服務器和客戶端。 易用、健壯、安全、高效,你可以在Netty上輕鬆實現各種自定義的協議!其實遊戲領域是個更好的例子,長連接,自定義協議,高併發,Netty就是絕配。

  因爲Netty本身就是一個基於NIO的網絡框架, 封裝了Java NIO那些複雜的底層細節,給你提供簡單好用的抽象概念來編程。

  注意幾個關鍵詞,首先它是個框架,是個“半成品”,不能開箱即用,你必須得拿過來做點定製,利用它開發出自己的應用程序,然後才能運行(就像使用Spring那樣)。一個更加知名的例子就是阿里巴巴的Dubbo了,這個RPC框架的底層用的就是Netty。 另外一個關鍵詞是高性能,如果你的應用根本沒有高併發的壓力,那就不一定要用Netty了。


參考資料:https://blog.csdn.net/bjweimengshu/article/details/78786315

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章