深入理解Java NIO

在 JDK1.4 之後,爲了提高 Java IO 的效率,Java 提供了一套 New IO (NIO),之所以稱之爲 New NIO,原因在於它相對於之前的 IO 類庫是新增的。此外,舊的 IO 類庫提供的 IO 方法是阻塞的,New IO 類庫則讓 Java 可支持非阻塞 IO,所以,更多的人喜歡稱之爲非阻塞 IO(Non-blocking IO)。

NIO 應用非常廣泛,是 Java 進階的必學知識,此外,在 Java 相關崗位的面試中也是“常客”,對於準備深入學習 Java 的讀者,瞭解 NIO 確有必要。

本場 Chat,我將分享以下內容:

  1. IO 與 NIO 有何不同?

  2. NIO 核心對象 Buffer 詳解;

  3. NIO 核心對象 Channel 詳解;

  4. NIO 核心對象 Selector 詳解;

  5. Reactor 模式介紹。

1. IO 和 NIO 相關的預備知識

1.1 IO 的含義

講 NIO 之前,我們先來看一下 IO。

Java IO 即 Java 輸入輸出。在開發應用軟件時,很多時候都需要和各種輸入輸出相關的媒介打交道。與媒介進行 IO 操作的過程十分複雜,需要考慮衆多因素,比如:進行 IO 操作媒介的類型(文件、控制檯、網絡)、通信方式(順序、隨機、二進制、按字符、按字、按行等等)。

Java 類庫提供了相應的類來解決這些難題,這些類就位於 java.io 包中, 在整個 java.io 包中最重要的就是 5 個類和一個接口。5 個類指的是 File、OutputStream、InputStream、Writer、Reader;一個接口指的是 Serializable。

由於老的 Java IO 標準類提供 IO 操作(如 read(),write())都是同步阻塞的,因此,IO 通常也被稱爲阻塞 IO(即 BIO,Blocking I/O)。

1.2 NIO 含義

在 JDK1.4 之後,爲了提高 Java IO 的效率,Java 又提供了一套 New IO(NIO),原因在於它相對於之前的 IO 類庫是新增的。此外,舊的 IO 類庫提供的 IO 方法是阻塞的,New IO 類庫則讓 Java 可支持非阻塞 IO,所以,更多的人喜歡稱之爲非阻塞 IO(Non-blocking IO)。

1.3 四種 IO 模型

同步阻塞 IO:

在此種方式下,用戶進程在發起一個 IO 操作以後,必須等待 IO 操作的完成,只有當真正完成了 IO 操作以後,用戶進程才能運行。 Java 傳統的 IO 模型屬於此種方式!

同步非阻塞 IO:

在此種方式下,用戶進程發起一個 IO 操作以後 便可返回做其它事情,但是用戶進程需要時不時的詢問 IO 操作是否就緒,這就要求用戶進程不停的去詢問,從而引入不必要的 CPU 資源浪費。其中目前 Java 的 NIO 就屬於同步非阻塞 IO 。

異步阻塞 IO:

此種方式下是指應用發起一個 IO 操作以後,不等待內核 IO 操作的完成,等內核完成 IO 操作以後會通知應用程序,這其實就是同步和異步最關鍵的區別,同步必須等待或者主動的去詢問 IO 是否完成,那麼爲什麼說是阻塞的呢?因爲此時是通過 select 系統調用來完成的,而 select 函數本身的實現方式是阻塞的,而採用 select 函數有個好處就是它可以同時監聽多個文件句柄,從而提高系統的併發性!

異步非阻塞 IO:

在此種模式下,用戶進程只需要發起一個 IO 操作然後立即返回,等 IO 操作真正的完成以後,應用程序會得到 IO 操作完成的通知,此時用戶進程只需要對數據進行處理就好了,不需要進行實際的 IO 讀寫操作,因爲 真正的 IO 讀取或者寫入操作已經由 內核完成了。目前 Java 中還沒有支持此種 IO 模型。

1.4 小結

所有的系統 I/O 都分爲兩個階段:等待就緒和操作。舉例來說,讀函數,分爲等待系統可讀和真正的讀;同理,寫函數分爲等待系統可以寫和真正的寫。Java IO 的各種流是阻塞的。這意味着當線程調用 write() 或 read() 時,線程會被阻塞,直到有一些數據可用於讀取或數據被完全寫入。

需要說明的是等待就緒引起的 “阻塞” 是不使用 CPU 的,是在 “空等”;而真正的讀寫操作引起的“阻塞” 是使用 CPU 的,是真正在”幹活”,而且這個過程非常快,屬於 memory copy,帶寬通常在 1GB/s 級別以上,可以理解爲基本不耗時。因此,所謂 “阻塞” 主要是指等待就緒的過程。

以socket.read()爲例子:

傳統的阻塞 IO(BIO) 裏面 socket.read(),如果接收緩衝區裏沒有數據,函數會一直阻塞,直到收到數據,返回讀到的數據。

而對於非阻塞 IO(NIO),如果接收緩衝區沒有數據,則直接返回 0,而不會阻塞;如果接收緩衝區有數據,就把數據從磁盤讀到內存,並且返回給用戶。

說得接地氣一點,BIO 裏用戶最關心 “我要讀”,NIO 裏用戶最關心” 我可以讀了”。NIO 一個重要的特點是:socket 主要的讀、寫、註冊和接收函數,在等待就緒階段都是非阻塞的,真正的 I/O 操作是同步阻塞的(消耗 CPU 但性能非常高)。

2. NIO 核心對象 Buffer 詳解

爲什麼說 NIO 是基於緩衝區的 IO 方式呢?因爲,當一個鏈接建立完成後,IO 的數據未必會馬上到達,爲了當數據到達時能夠正確完成 IO 操作,在 BIO(阻塞 IO)中,等待 IO 的線程必須被阻塞,以全天候地執行 IO 操作。爲了解決這種 IO 方式低效的問題,引入了緩衝區的概念,當數據到達時,可以預先被寫入緩衝區,再由緩衝區交給線程,因此線程無需阻塞地等待 IO。

在正式介紹 Buffer 之前,我們先來 Stream,以便更深刻的理解 Java IO 與 NIO 的不同。

2.1 Stream

Java IO 是面向流的 I/O,這意味着我們需要從流中讀取一個或多個字節。它使用流來在數據源/槽和 Java 程序之間傳輸數據。使用此方法的 I/O 操作較慢。下面來看看在 Java 程序中使用輸入/輸出流的數據流圖 (注意:圖中輸入/輸出均以 Java Program 爲參照物):

2.2 Buffer

Buffer 是一個對象,它包含一些要寫入或讀出的數據。在 NIO 中,數據是放入 Buffer 對象的,而在 IO 中,數據是直接寫入或者讀到 Stream 對象的。應用程序不能直接對 Channel 進行讀寫操作,而必須通過 Buffer 來進行,即 Channel 是通過 Buffer 來讀寫數據的,如下示意圖。

 

在 NIO 中,所有的數據都是用 Buffer 處理的,它是 NIO 讀寫數據的中轉池。Buffer 實質上是一個數組,通常是一個字節數據,但也可以是其他類型的數組。但一個緩衝區不僅僅是一個數組,重要的是它提供了對數據的結構化訪問,而且還可以跟蹤系統的讀寫進程。

Buffer 讀寫步驟:

使用 Buffer 讀寫數據一般遵循以下四個步驟:

  • 寫入數據到 Buffer;

  • 調用 flip() 方法;

  • 從 Buffer 中讀取數據;

  • 調用 clear() 方法或者 compact() 方法。

當向 Buffer 寫入數據時,Buffer 會記錄下寫了多少數據。一旦要讀取數據,需要通過 flip() 方法將 Buffer 從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到 Buffer 的所有數據。

一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:調用 clear() 或 compact() 方法。clear() 方法會清空整個緩衝區。compact() 方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。

Buffer種類:

Buffer主要有如下幾種:

CharBuffer、DoubleBuffer、IntBuffer、LongBuffer、ByteBuffer、ShortBuffer、FloatBuffer

上述緩衝區覆蓋了我們可以通過 I/O 發送的基本數據類型:

characters,double,int,long,byte,short和float

2.3 Buffer 結構

Buffer 有幾個重要的屬性如下,

結合如下結構圖解釋一下:

  • position 記錄當前讀取或者寫入的位置,寫模式下等於當前寫入的單位數據數量,從寫模式切換到讀模式時,置爲 0,在讀的過程中等於當前讀取單位數據的數量;

  • limit 代表最多能寫入或者讀取多少單位的數據,寫模式下等於最大容量 capacity;從寫模式切換到讀模式時,等於 position,然後再將 position 置爲 0,所以,讀模式下,limit 表示最大可讀取的數據量,這個值與實際寫入的數量相等。

  • capacity 表示 buffer 容量,創建時分配。

之所以介紹這一節是爲了更好的解釋爲何寫/讀模式切換時需要調用 flip() 方法,通過上述解釋,相信讀者已經明白爲何寫/讀模式切換需要調用 flip() 方法了。附上 flip() 方法的解釋:

Flips this buffer. The limit is set to the current position and then the position is set to zero. If the mark is defined then it is discarded.

2.4 Buffer 的選擇

通常情況下,操作系統的一次寫操作分爲兩步:

  1. 將數據從用戶空間拷貝到系統空間(即從 JVM 內存拷貝到系統內存)。

  2. 從系統空間往網卡寫。

同理,讀操作也分爲兩步:

  1. 將數據從網卡拷貝到系統空間;

  2. 將數據從系統空間拷貝到用戶空間。

對於 NIO 來說,緩存的使用可以使用DirectByteBuffer(堆外內存,關於堆外內存,如果存在疑問請閱讀我的另一篇文章)和 HeapByteBuffer(堆外內存)。如果使用了 DirectByteBuffer,一般來說可以減少一次系統空間到用戶空間的拷貝。但Buffer創建和銷燬的成本更高,更不宜維護,通常會用內存池來提高性能。如果數據量比較小的中小應用情況下,可以考慮使用 heapBuffer;反之可以用 directBuffer。

2.5 Buffer 使用實例

import java.io.FileOutputStream;

import java.nio.ByteBuffer;

import java.nio.channels.FileChannel;

public class IO_Demo

{

    public static void main(String[] args) throws Exception

    {
        String infile = "D:\\Users\\data.txt";
        String outfile = "D:\\Users\\dataO.txt";
        // 獲取源文件和目標文件的輸入輸出流
        FileInputStream fin = new FileInputStream(infile);

        FileOutputStream fout = new FileOutputStream(outfile);
        // 獲取輸入輸出通道
        FileChannel fileChannelIn = fin.getChannel();

        FileChannel fileChannelOut = fout.getChannel();

        // 創建緩衝區,分配1K堆內存

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true)

        {
            // clear方法重設緩衝區,使它可以接受讀入的數據
            buffer.clear();
            // 從輸入通道中讀取數據數據並寫入buffer

            int r = fileChannelIn.read(buffer);

            // read方法返回讀取的字節數,可能爲零,如果該通道已到達流的末尾,則返回-1

            if (r == -1)

            {

                break;
            }

            // flip方法將 buffer從寫模式切換到讀模式
            buffer.flip();

            // 從buffer中讀取數據然後寫入到輸出通道中

            fileChannelOut.write(buffer);

        }

        //關閉通道

        fileChannelOut.close();

        fileChannelIn.close();
        fout.close();

        fin.close();

    }

}

3. NIO 核心對象 Channel 詳解

3.1 簡要回顧

第一節中的例子所示,當執行:fileChannelOut.write(buffer),便將一個 buffer 寫到了一個通道中。相較於緩衝區,通道更加抽象,因此,我在第一節詳細介紹了緩衝區,並穿插了通道的內容。

引用 Java NIO 中權威的說法:通道是 I/O 傳輸發生時通過的入口,而緩衝區是這些數據傳輸的來源或目標。對於離開緩衝區的傳輸,需要輸出的數據被置於一個緩衝區,然後寫入通道。對於傳回緩衝區的傳輸,一個通道將數據寫入緩衝區中。

例如:

有一個服務器通道serverChannel,一個客戶端通道 SocketChannel clientChannel;

服務器緩衝區:serverBuffer,客戶端緩衝區:clientBuffer。

  • 當服務器想向客戶端發送數據時,需要調用 clientChannel.write(serverBuffer)。當客戶端要讀時,調用 clientChannel.read(clientBuffer)

  • 當客戶端想向服務器發送數據時,需要調用 serverChannel.write(clientBuffer)。當服務器要讀時,調用 serverChannel.read(serverBuffer)

3.2 關於 Channel

Channel 是一個對象,可以通過它讀取和寫入數據。可以把它看做 IO 中的流。但是它和流相比還有一些不同:

  • Channel 是雙向的,既可以讀又可以寫,而流是單向的(所謂輸入/輸出流);

  • Channel 可以進行異步的讀寫;

  • 對 Channel 的讀寫必須通過 buffer 對象;

正如上面提到的,所有數據都通過 Buffer 對象處理,所以,輸出操作時不會將字節直接寫入到 Channel 中,而是將數據寫入到 Buffer 中;同樣,輸入操作也不會從 Channel 中讀取字節,而是將數據從 Channel 讀入 Buffer,再從 Buffer 獲取這個字節。

因爲 Channel 是雙向的,所以 Channel 可以比流更好地反映出底層操作系統的真實情況。特別是在 Unix 模型中,底層操作系統通常都是雙向的。

在 Java NIO 中 Channel 主要有如下幾種類型:

  • FileChannel:從文件讀取數據的

  • DatagramChannel:讀寫 UDP 網絡協議數據

  • SocketChannel:讀寫 TCP 網絡協議數據

  • ServerSocketChannel:可以監聽 TCP 連接

4. NIO 核心對象 Selector 詳解

4.1 關於 Selector

通道和緩衝區的機制,使得 Java NIO 實現了同步非阻塞 IO 模式,在此種方式下,用戶進程發起一個 IO 操作以後便可返回做其它事情,而無需阻塞地等待 IO 事件的就緒,但是用戶進程需要時不時的詢問 IO 操作是否就緒,這就要求用戶進程不停的去詢問,從而引入不必要的 CPU 資源浪費。

鑑於此,需要有一個機制來監管這些 IO 事件,如果一個 Channel 不能讀寫(返回 0),我們可以把這件事記下來,然後切換到其它就緒的連接(channel)繼續進行讀寫。在 Java NIO 中,這個工作由 selector 來完成,這就是所謂的同步。

Selector 是一個對象,它可以接受多個 Channel 註冊,監聽各個 Channel 上發生的事件,並且能夠根據事件情況決定 Channel 讀寫。這樣,通過一個線程可以管理多個 Channel,從而避免爲每個 Channel 創建一個線程,節約了系統資源。如果你的應用打開了多個連接(Channel),但每個連接的流量都很低,使用 Selector 就會很方便。

要使用 Selector,就需要向 Selector 註冊 Channel,然後調用它的 select() 方法。這個方法會一直阻塞到某個註冊的通道有事件就緒,這就是所說的輪詢。一旦這個方法返回,線程就可以處理這些事件。

下面這幅圖展示了一個線程處理 3 個 Channel 的情況:

4.2 Selector 使用

1.創建 Selector 對象

通過 Selector.open() 方法,我們可以創建一個選擇器:

Selector selector = Selector.open();

2. 將 Channel 註冊到選擇器中

爲了使用選擇器管理 Channel,我們需要將 Channel 註冊到選擇器中:

channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);

注意,註冊的 Channel 必須設置成異步模式纔可以,否則異步 IO 就無法工作,這就意味着我們不能把一個 FileChannel 註冊到 Selector,因爲 FileChannel 沒有異步模式,但是網絡編程中的 SocketChannel 是可以的。

需要注意 register() 方法的第二個參數,它是一個“interest set”,意思是註冊的 Selector 對 Channel 中的哪些事件感興趣,事件類型有四種(對應 SelectionKey 的四個常量):

  • OP_ACCEPT

  • OP_CONNECT

  • OP_READ

  • OP_WRITE

通道觸發了一個事件意思是該事件已經 Ready(就緒)。所以,某個 Channel 成功連接到另一個服務器稱爲 Connect Ready。一個 ServerSocketChannel 準備好接收新連接稱爲 Accept Ready,一個有數據可讀的通道可以說是 Read Ready,等待寫數據的通道可以說是 Write Ready。

如果你對多個事件感興趣,可以通過 or 操作符來連接這些常量:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 

3. 關於 SelectionKey

請注意對 register() 的調用的返回值是一個 SelectionKey。 SelectionKey 代表這個通道在此 Selector 上的這個註冊。當某個 Selector 通知您某個傳入事件時,它是通過提供對應於該事件的 SelectionKey 來進行的。SelectionKey 還可以用於取消通道的註冊。SelectionKey 中包含如下屬性:

  • The interest set

  • The ready set

  • The Channel

  • The Selector

  • An attached object (optional)

這幾個屬性很好理解,interest set 代表感興趣事件的集合;ready set 代表通道已經準備就緒的操作的集合;Channel 和 Selector:我們可以通過 SelectionKey 獲得 Selector 和註冊的 Channel;attached object :可以將一個對象或者更多信息 attach 到 SelectionKey 上,這樣就能方便的識別某個給定的通道。例如,可以附加與通道一起使用的 Buffer。

SelectionKey 還有幾個重要的方法,用於檢測 Channel 中什麼事件或操作已經就緒,它們都會返回一個布爾類型:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable(); 

4. 通過 SelectionKeys() 遍歷

從上文我們知道,對於每一個註冊到 Selector 中的 Channel 都有一個對應的 SelectionKey,那麼,多個 Channel 註冊到 Selector 中,必然形成一個 SelectionKey 集合,通過 SelectionKeys() 方法可以獲取這個集合。因此,當 Selector 檢測到有通道就緒後,我們可以通過調用 selector.selectedKeys() 方法返回的 SelectionKey 集合來遍歷,進而獲得就緒的 Channel,再進一步處理。實例代碼如下:

 //獲取註冊到selector中的Channel對應的selectionKey集合

Set<SelectionKey> selectedKeys = selector.selectedKeys();

// 通過迭代器進行遍歷,獲取已經就緒的Channel,

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) 

{ 
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) 
{
// a connection was accepted by a ServerSocketChannel.
// 可通過Channel()方法獲取就緒的Channel並進一步處理
SocketChannel channel = (SocketChannel)key.channel();
// TODO 
} 
else if (key.isConnectable()) 
{
// TODO
} 
else if (key.isReadable()) 
{
// TODO
} 
else if (key.isWritable()) 
{
// TODO
}
// 刪除處理過的事件
keyIterator.remove();
}

 

5. select() 方法檢測 Selector 中是否有 Channel 就緒

在進行遍歷之前,我們至少應該知道是否已經有 Channel 就緒,否則遍歷完全是徒勞。Selector 提供了 select() 方法,它會返回一個數值,代表就緒 Channel 的數量,如果沒有 Channel 就緒,將一直阻塞。除了 select(),還有其它幾種,如下:

  • int select(): 阻塞到至少有一個通道就緒;

  • int select(long timeout):select() 一樣,除了最長會阻塞 timeout 毫秒(參數),超時後返回0,表示沒有通道就緒;

  • int selectNow():不會阻塞,不管什麼通道就緒都立刻返回,此方法執行非阻塞的選擇操作。如果自從前一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回零。

加入 select() 方法後的代碼:

// 反覆循環,等待IO
while(true)
{
// 等待某信道就緒,將一直阻塞,直到有通道就緒
selector.select();
// 獲取註冊到selector中的Channel對應的selectionKey集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
// 通過迭代器進行遍歷,獲取已經就緒的Channel,
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) 
{ 
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) 
{
// a connection was accepted by a ServerSocketChannel.
// 可通過Channel()方法獲取就緒的Channel並進一步處理
SocketChannel channel = (SocketChannel)key.channel();
// TODO 
} 
else if (key.isConnectable()) 
{
// TODO
} 
else if (key.isReadable()) 
{
// TODO
} 
else if (key.isWritable()) 
{
// TODO
}
// 刪除處理過的事件
keyIterator.remove();
}
}

4.3 Selector 使用實例

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class TCPServer
{
    // 超時時間,單位毫秒
    private static final int TimeOut = 3000;
    // 本地監聽端口
    private static final int ListenPort = 1978;
    public static void main(String[] args) throws IOException
    {
        // 創建選擇器
        Selector selector = Selector.open();
        // 打開監聽信道
        ServerSocketChannel listenerChannel = ServerSocketChannel.open();
        // 與本地端口綁定
        listenerChannel.socket().bind(new InetSocketAddress(ListenPort));
        // 設置爲非阻塞模式
        listenerChannel.configureBlocking(false);
        // 將選擇器綁定到監聽信道,只有非阻塞信道纔可以註冊選擇器.並在註冊過程中指出該信道可以進行Accept操作
        // 一個serversocket channel準備好接收新進入的連接稱爲“接收就緒”
        listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 反覆循環,等待IO
        while (true)
        {
            // 等待某信道就緒(或超時)
            int keys = selector.select(TimeOut);
            //剛啓動時連續輸出0,client連接後一直輸出1
            if (keys == 0)
            {
                System.out.println("獨自等待.");
                continue;
            }
            // 取得迭代器,遍歷每一個註冊的通道
            Set<SelectionKey> set = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = set.iterator();
            while (keyIterator.hasNext())
            {
                SelectionKey key = keyIterator.next();
                if(key.isAcceptable()) 
                {
                    // a connection was accepted by a ServerSocketChannel.
                    // 可通過Channel()方法獲取就緒的Channel並進一步處理
                    SocketChannel channel = (SocketChannel)key.channel();
                    // TODO 
                } 
                else if (key.isConnectable()) 
                {
                    // TODO
                } 
                else if (key.isReadable()) 
                {
                    // TODO
                } 
                else if (key.isWritable()) 
                {
                    // TODO
                }
                // 刪除處理過的事件
                keyIterator.remove();
            }
        }
    }
}

特別說明:例子中 selector 只註冊了一個 Channel,註冊多個 Channel 操作類似。如下:

for (int i=0; i<3; i++)
{
// 打開監聽信道
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
// 與本地端口綁定
listenerChannel.socket().bind(new InetSocketAddress(ListenPort+i));
// 設置爲非阻塞模式
listenerChannel.configureBlocking(false);
// 註冊到selector中
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
}

在上面的例子中,對於通道 IO 事件的處理並沒有給出具體方法,在此,舉一個更詳細的例子:

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NIO_Learning
{
    private static final int BUF_SIZE = 256;
    private static final int TIMEOUT = 3000;
    public static void main(String args[]) throws Exception
    {
        // 打開服務端 Socket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 打開 Selector
        Selector selector = Selector.open();
        // 服務端 Socket 監聽8080端口, 並配置爲非阻塞模式
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        // 將 channel 註冊到 selector 中.
        // 通常我們都是先註冊一個 OP_ACCEPT 事件, 然後在 OP_ACCEPT 到來時, 再將這個 Channel 的 OP_READ 註冊到 Selector 中.
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true)
        {
            // 通過調用 select 方法, 阻塞地等待 channel I/O 可操作
            if (selector.select(TIMEOUT) == 0)
            {
                System.out.print("超時等待...");
                continue;
            }
            // 獲取 I/O 操作就緒的 SelectionKey, 通過 SelectionKey 可以知道哪些 Channel 的哪類 I/O 操作已經就緒.
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext())
            {
                SelectionKey key = keyIterator.next();
                // 當獲取一個 SelectionKey 後, 就要將它刪除, 表示我們已經對這個 IO 事件進行了處理.
                keyIterator.remove();
                if (key.isAcceptable())
                {
                    // 當 OP_ACCEPT 事件到來時, 我們就有從 ServerSocketChannel 中獲取一個 SocketChannel,
                    // 代表客戶端的連接
                    // 注意, 在 OP_ACCEPT 事件中, 從 key.channel() 返回的 Channel 是 ServerSocketChannel.
                    // 而在 OP_WRITE 和 OP_READ 中, 從 key.channel() 返回的是 SocketChannel.
                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                    clientChannel.configureBlocking(false);
                    //在 OP_ACCEPT 到來時, 再將這個 Channel 的 OP_READ 註冊到 Selector 中.
                    // 注意, 這裏我們如果沒有設置 OP_READ 的話, 即 interest set 仍然是 OP_CONNECT 的話, 那麼 select 方法會一直直接返回.
                    clientChannel.register(key.selector(), SelectionKey.OP_READ,
                            ByteBuffer.allocate(BUF_SIZE));
                }
                if (key.isReadable())
                {
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buf = (ByteBuffer) key.attachment();
                    long bytesRead = clientChannel.read(buf);
                    if (bytesRead == -1)
                    {
                        clientChannel.close();
                    }
                    else if (bytesRead > 0)
                    {
                        key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                        System.out.println("Get data length: " + bytesRead);
                    }
                }
                if (key.isValid() && key.isWritable())
                {
                    ByteBuffer buf = (ByteBuffer) key.attachment();
                    buf.flip();
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    clientChannel.write(buf);
                    if (!buf.hasRemaining())
                    {
                        key.interestOps(SelectionKey.OP_READ);
                    }
                    buf.compact();
                }
            }
        }
    }
}

4.4 小結

如從上述實例所示,可以將多個 Channel 註冊到同一個 Selector 對象上,實現一個線程同時監控多個 Channel 的請求狀態,但有一個不容忽視的缺陷:

所有讀/寫請求以及對新連接請求的處理都在同一個線程中處理,無法充分利用多 CPU 的優勢,同時讀/寫操作也會阻塞對新連接請求的處理。因此,有必要進行優化,可以引入多線程,並行處理多個讀/寫操作。

一種優化策略是:

將 Selector 進一步分解爲 Reactor,從而將不同的感興趣事件分開,每一個 Reactor 只負責一種感興趣的事件。這樣做的好處是:

  • 分離阻塞級別,減少了輪詢的時間;

  • 線程無需遍歷 set 以找到自己感興趣的事件,因爲得到的 set 中僅包含自己感興趣的事件。下文將要介紹的 Reactor 模式便是這種優化思想的一種實現。

5. Reactor 模式介紹

【特別說明】Reactor 模式是一種設計模式,不是爲 Java 量身定做的,C++ 等語言也可以實現 Reactor 模式。因此,切忌先入爲主將 Reactor 的概念與 Java 對號入座。

關於 Reactor 模式,Wikipedia 上釋義爲:

“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.”

從這個描述中,我們知道 Reactor 模式首先是事件驅動的,有一個或多個併發輸入源,有一個 Service Handler,有多個 Request Handlers;這個 Service Handler 會同步的將輸入的請求(Event)多路複用的分發給相應的 Request Handler。如果用圖來表達:

從結構上,這有點類似生產者消費者模式,即有一個或多個生產者將事件放入一個 Queue 中,而一個或多個消費者主動的從這個 Queue 中 Poll 事件來處理;而 Reactor 模式則並沒有 Queue 來做緩衝,每當一個 Event 輸入到 Service Handler 之後,該 Service Handler 會主動的根據不同的 Event 類型將其分發給對應的 Request Handler 來處理。

5.1 Reactor 模式實現

Reactor 模式中有些概念對於初學者來說會比較生澀難懂,鑑於此,我們先來看一個例子(出處:Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events),該例子以 Logging Server 來分析 Reactor 模式,其實現完全遵循 Reactor 描述。Logging Server 中的 Reactor 模式實現分兩個部分:Client 連接到 Logging Server 和 Client 向 Logging Server 寫 Log。因而對它的描述分成這兩個部分。

Part-I:Client 連接到 Logging Server

交互步驟:

  1. Logging Server 註冊 LoggingAcceptor 到 InitiationDispatcher。

  2. Logging Server 調用 InitiationDispatcher 的 handle_events() 方法啓動。

  3. InitiationDispatcher 內部調用 select() 方法(Synchronous Event Demultiplexer),阻塞等待 Client 連接。

  4. Client 連接到 Logging Server。

  5. InitiationDisptcher 中的 select() 方法返回,並通知 LoggingAcceptor 有新的連接到來。

  6. LoggingAcceptor 調用 accept 方法 accept 這個新連接。

  7. LoggingAcceptor 創建新的 LoggingHandler。

  8. 新的 LoggingHandler 註冊到 InitiationDispatcher 中(同時也註冊到 Synchonous Event Demultiplexer 中),等待 Client 發起寫 log 請求。

Part-II:Client向Logging Server寫Log

交互步驟:

  1. Client 發送 log 到 Logging server。

  2. InitiationDispatcher 監測到相應的 Handle 中有事件發生,返回阻塞等待,根據返回的 Handle 找到 LoggingHandler,並回調 LoggingHandler 中的 handle_event() 方法。

  3. LoggingHandler 中的 handle_event() 方法中讀取 Handle 中的 log 信息。

  4. 將接收到的 log 寫入到日誌文件、數據庫等設備中。3.4 步驟循環直到當前日誌處理完成。

  5. 返回到 InitiationDispatcher 等待下一次日誌寫請求。

小結

看了上述 Reactor 模式的例子,是不是感覺和 Selector 的例子有些類似?不用懷疑,它們不只是類似,在 Java 的 NIO 中,對 Reactor 模式有無縫的支持。

5.2 Java NIO 對 Reactor 的實現(單線程版)

網上有很多關於 Java NIO 對 Reactor 實現的例子,這些例子的素材基本都是出自紐約州立大學 Doug Lea 的 PPT:Scalable IO in Java,在此,我同樣借鑑 Doug Lea 的思想進行舉例。

5.2.1 服務端代碼

1. 創建 Reactor

仔細閱讀代碼,你會發現與 Selector 實例很相似。

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;
public class Reactor implements Runnable
{
    public final Selector selector;
    public final ServerSocketChannel serverSocketChannel;
    public Reactor(int port) throws IOException
    {
        // 創建選擇器
        selector = Selector.open();
        // 打開監聽信道
        serverSocketChannel = ServerSocketChannel.open();
        // 與本地端口綁定
        InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
        System.out.println(InetAddress.getLocalHost());
        serverSocketChannel.socket().bind(inetSocketAddress);
        // 設置爲非阻塞模式,只有非阻塞信道纔可以註冊選擇器,否則異步IO就無法工作
        serverSocketChannel.configureBlocking(false);
        // 向selector註冊該channel    
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 利用selectionKey的attache功能綁定Acceptor
        selectionKey.attach(new Acceptor(this));
    }
    @Override
    public void run()
    {
        try
        {
            while (!Thread.interrupted())
            {
                System.out.println("等待....");
                // 阻塞等待某信道就緒
                selector.select();
                // 取得已就緒事件的key集合,遍歷每一個註冊的通道(本文作爲舉例,只有一個通道)
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                //Selector如果發現channel有OP_ACCEPT或READ事件發生,下列遍歷就會進行。  
                while (it.hasNext())
                {  
                    SelectionKey selectionKey = it.next();
                    // 根據事件的key進行調度
                    dispatch(selectionKey);
                    // 刪除處理過的事件
                    it.remove();
                }
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
    void dispatch(SelectionKey key)
    {
        Runnable r = (Runnable) (key.attachment());
        if (r != null)
        {
            r.run();//調度事件對應的處理流程
        }
    }
}

與 Selector 實例最大的不同在於,使用 selectionKey 對象的 attach() 方法,爲每一個註冊到 Selector 中的 Channel 都綁定了一個 Handler,當 Channel 有事件就緒時,可通過調度 Handler 進行處理。

2. 創建 Acceptor

Acceptor 本質上也是一個 Handler,只不過特殊一些:用於處理 Client 的連接請求,即它感興趣的通道事件類型是 OP_ACCEPT。

import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
public class Acceptor implements Runnable
{
    private Reactor reactor;
    public Acceptor(Reactor reactor)
    {
        this.reactor = reactor;
    }
    @Override
    public void run()
    {
        try
        {
            // 接受client連接請求
            SocketChannel sc = reactor.serverSocketChannel.accept();   
            System.out.println(sc.socket().getRemoteSocketAddress().toString() + " is connected.");
            if (sc != null)
            {
                sc.configureBlocking(false);
                // SocketChannel向selector註冊一個OP_READ事件,然後返回該通道的key 
                SelectionKey sk = sc.register(reactor.selector, SelectionKey.OP_READ); 
                // 使一個阻塞住的selector操作立即返回
                reactor.selector.wakeup(); 
                // 通過key爲新的通道綁定一個附加的TCPHandler對象
                sk.attach(new TCPHandler(sk, sc)); 
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}

3. 創建 Handler

從 Acceptor 的代碼中可以看出,當服務端接受客戶端的連接請求後(通過 serverSocketChannel 調用 accept() 獲得了一個新的通道 SocketChannel),會將新的通道註冊到 Selector 並綁定一個 Handler,用於處理讀寫事件,本例中,這個 Handler 命名爲 TCPHandler。

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
public class TCPHandler implements Runnable
{
    private final SelectionKey sk;
    private final SocketChannel sc;
    int state;
    public TCPHandler(SelectionKey sk, SocketChannel sc)
    {
        this.sk = sk;
        this.sc = sc;
        state = 0; // 初始狀態設置爲READING,client連接後,讀取client請求  
    }
    @Override
    public void run()
    {
        try
        {
            if (state == 0)
                read(); // 讀取客戶端請求數據
            else
                send(); // 向客戶端發送反饋數據
        }
        catch (IOException e)
        {
            System.out.println("[Warning!] A client has been closed.");
            closeChannel();
        }
    }
    private void closeChannel()
    {
        try
        {
            sk.cancel();
            sc.close();
        }
        catch (IOException e1)
        {
            e1.printStackTrace();
        }
    }
    private synchronized void read() throws IOException
    {
        // 創建一個讀取通道數據的緩衝區
        ByteBuffer inputBuffer = ByteBuffer.allocate(1024);
        inputBuffer.clear();
        int numBytes = sc.read(inputBuffer); // 讀取數據
        if (numBytes == -1)
        {
            System.out.println("[Warning!] A client has been closed.");
            closeChannel();
            return;
        }
        // 將讀取的字節轉換爲字符串類型
        String str = new String(inputBuffer.array()); 
        if ((str != null) && !str.equals(" "))
        {
            process(str); // 進一步處理獲取的數據
            System.out.println(sc.socket().getRemoteSocketAddress().toString() + " > " + str);
            // 切換狀態,準備向客戶端發送反饋數據
            state = 1; 
            // 通過key改變通道註冊的事件類型
            sk.interestOps(SelectionKey.OP_WRITE);
            sk.selector().wakeup();
        }
    }
    private void send() throws IOException
    {
        String str = "Your message has sent to " + sc.socket().getLocalSocketAddress().toString()
                + "\r\n";
        // 創建發送數據的緩存區並寫入數據
        ByteBuffer outputBuffer = ByteBuffer.allocate(1024);
        outputBuffer.put(str.getBytes());
        outputBuffer.flip();
        // 向客戶端發送反饋數據
        sc.write(outputBuffer); 
        // 切換狀態
        state = 0; 
        // 通過key改變通道註冊的事件類型
        sk.interestOps(SelectionKey.OP_READ);
        sk.selector().wakeup();
    }
    void process(String str)
    {
        // do process(decode, logically process, encode)..
        // 略
    }
}

4. 服務端主函數 Main

import java.io.IOException;
public class Main
{
    public static void main(String[] args)
    {
        try
        {
            Reactor temp = new Reactor(12345);
            temp.run();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}

5.2.2 客戶端代碼

客戶端相對來說很簡單,不做過多解釋。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client
{
    public static void main(String[] args) throws UnknownHostException
    {
        // 初始化待連接服務端的地址
        String hostName = InetAddress.getLocalHost().toString();
        int port = 12345;
        try
        {
            Socket client = new Socket(InetAddress.getLocalHost(), port); 
            System.out.println("Connected to " + InetAddress.getLocalHost().toString());
            // 分別創建客戶端端輸入輸出流和控制檯輸入流
            PrintWriter out = new PrintWriter(client.getOutputStream());
            BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
            String input;
            while ((input = stdIn.readLine()) != null)
            { 
                //打印來自服務端的反饋數據
                out.println(input); 
                out.flush(); 
                if (input.equals("exit"))
                {
                    break;
                }
                // 打印控制檯輸入,即客戶端向服務端發送的數據
                System.out.println("server: " + in.readLine());
            }
            client.close();
            System.out.println("client stop.");
        }
        catch (UnknownHostException e)
        {
            System.err.println("Don't know about host: " + hostName);
        }
        catch (IOException e)
        {
            System.err.println("Couldn't get I/O for the socket connection");
        }
    }
}

小結

上面例子是 Java NIO 對 Reactor 模式的單線程版實現,多線程版及更多內容可參考 Doug Lea 的 PPT:Scalable IO In Java

5.3 Reactor 模式介紹

上面的實例,建議讀者自己運行一下,切實感受一下 Reactor 模式。有了前面這麼多鋪墊,是時候推出 Reactor 模式了,先來看一下 Reactor 模式的模塊關係圖(圖片出自:Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events):

概念解釋:

  • Handle:即操作系統中的句柄,是對資源在操作系統層面上的一種抽象,它可以是打開的文件、一個連接 (Socket)、Timer 等。由於 Reactor 模式一般使用在網絡編程中,因而這裏一般指 Socket Handle,即一個網絡連接(Connection,即 Java NIO 中的 Channel)。這個 Channel 註冊到 Synchronous Event Demultiplexer 中,以監聽 Handle 中發生的事件,對 ServerSocketChannnel 可以是 CONNECT 事件,對 SocketChannel 可以是 READ、WRITE、CLOSE 事件等。

  • Synchronous Event Demultiplexer:阻塞等待一系列的 Handle 中的事件到來,如果阻塞等待返回,即表示在返回的 Handle 中可以不阻塞的執行返回的事件類型。這個模塊一般使用操作系統的 select 來實現。在 Java NIO 中用 Selector 來封裝,當 Selector.select() 返回時,可以調用 Selector 的 selectedKeys() 方法獲取 Set<SelectionKey>,一個 SelectionKey 表達一個有事件發生的 Channel 以及該 Channel 上的事件類型。上圖的“Synchronous Event Demultiplexer ---notifies--> Handle”的流程如果是對的,那內部實現應該是 select() 方法在事件到來後會先設置 Handle 的狀態,然後返回。

  • Initiation Dispatcher:用於管理 Event Handler,即 EventHandler 的容器,用以註冊、移除 EventHandler 等;另外,它還作爲 Reactor 模式的入口調用 Synchronous Event Demultiplexer 的 select 方法以阻塞等待事件返回,當阻塞等待返回時,根據事件發生的 Handle 將其分發給對應的 Event Handler 處理,即回調 EventHandler 中的 handle_event() 方法。

  • Event Handler:定義事件處理方法:handle_event(),以供 InitiationDispatcher 回調使用。

  • Concrete Event Handler:事件 EventHandler 接口,實現特定事件處理邏輯。

Reactor 模式各個模塊之間的交互

下面這幅圖同樣出自上面列出的文章。結合圖片可以很清晰的看出各個模塊交互的過程,以 main 函數爲入口,流程如下:

  1. 初始化 InitiationDispatcher,並初始化一個 Handle 到 EventHandler 的 Map。

  2. 註冊 EventHandler 到 InitiationDispatcher 中,每個 EventHandler 包含對相應 Handle 的引用,從而建立 Handle 到 EventHandler 的映射(Map)。

  3. 調用 InitiationDispatcher 的 handle_events() 方法以啓動 Event Loop。在 Event Loop 中,調用 select() 方法(Synchronous Event Demultiplexer)阻塞等待 Event 發生。

  4. 當某個或某些 Handle 的 Event 發生後,select() 方法返回,InitiationDispatcher 根據返回的 Handle 找到註冊的 EventHandler,並回調該 EventHandler 的 handle_events() 方法。

  5. 在 EventHandler 的 handle_events() 方法中還可以向 InitiationDispatcher 中註冊新的 Eventhandler,比如對 AcceptorEventHandler 來,當有新的 client 連接時,它會產生新的 EventHandler 以處理新的連接,並註冊到 InitiationDispatcher 中。

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