Java NIO 學習筆記(一)----概述,Channel/Buffer

目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----聚集和分散,通道到通道
Java NIO 學習筆記(三)----Selector

Java NIO (來自 Java 1.4)可以替代標準 IO 和 Java Networking API ,NIO 提供了與標準 IO 不同的使用方式。學習 NIO 之前建議先掌握標準 IO 和 Java 網絡編程,推薦教程:

本文目的: 掌握了標準 IO 之後繼續學習 NIO 知識。主要參考 JavaDoc 和 Jakob Jenkov 的英文教程 Java NIO Tutorial

Java NIO 概覽

NIO 由以下核心組件組成:

  1. 通道和緩衝區
    在標準 IO API 中,使用字節流和字符流。 在 NIO 中使用通道和緩衝區。 數據總是從通道讀入緩衝區,或從緩衝區寫入通道。

  2. 非阻塞IO
    NIO 可以執行非阻塞 IO 。 例如,當通道將數據讀入緩衝區時,線程可以執行其他操作。 並且一旦數據被讀入緩衝區,線程就可以繼續處理它。 將數據寫入通道也是如此。

  3. 選擇器
    NIO 包含“選擇器”的概念。 選擇器是一個可以監視多個事件通道的對象(例如:連接打開,數據到達等)。 因此,單個線程可以監視多個通道的數據。

NIO 有比這些更多的類和組件,但在我看來,Channel,Buffer 和 Selector 構成了 API 的核心。 其餘的組件,如 Pipe 和 FileLock ,只是與三個核心組件一起使用的實用程序類。

Channels/Buffers 通道和緩衝區

通常,NIO 中的所有 IO 都以 Channel 開頭,頻道有點像流。 數據可以從 Channel 讀入 Buffer,也可以從 Buffer 寫入 Channel :
通道將數據讀入緩衝區,緩衝區將數據寫入通道

有幾種 Channel 和 Buffer ,以下是 NIO 中主要 Channel 實現類的列表,這些通道包括 UDP + TCP 網絡 IO 和文件 IO:

  • FileChannel :文件通道
  • DatagramChannel :數據報通道
  • SocketChannel :套接字通道
  • ServerSocketChannel :服務器套接字通道

這些類也有一些有趣的接口,但爲了簡單起見,這裏暫時不提,後續會進行學習的。

以下是 NIO 中的核心 Buffer 實現,其實就是 7 種基本類型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

NIO 還有一個 MappedByteBuffer,它與內存映射文件一起使用,同樣這個後續再講。

Selectors 選擇器

選擇器允許單個線程處理多個通道。 如果程序打開了許多連接(通道),但每個連接只有較低的流量,使用選擇器就很方便。 例如,在聊天服務器中, 以下是使用 Selector 處理 3 個 Channel 的線程圖示:
1個線程使用選擇器處理3個通道

要使用選擇器,需要使用它註冊通道。 然後你調用它的 select() 方法。 此方法將阻塞,直到有一個已註冊通道的事件準備就緒。 一旦該方法返回,該線程就可以處理這些事件。 事件可以是傳入連接,接收數據等。

Channel (通道)

NIO 通道類似於流,但有一些區別:

  • 通道可以讀取和寫入。 流通常是單向的(讀或寫)。
  • 通道可以異步讀取和寫入。
  • 通道始終讀取或寫入緩衝區,即它只面向緩衝區。

如上所述,NIO 中總是將數據從通道讀取到緩衝區,或將數據從緩衝區寫入通道。 這是一個例子:

// 文件內容是 123456789
RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw");
FileChannel fileChannel = accessFile.getChannel();

ByteBuffer buffer = ByteBuffer.allocate(48);

int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區,返回讀入到緩衝區的字節數

Buffer(緩衝區)

使用 Buffer 與 Channel 交互,數據從通道讀入緩衝區,或從緩衝區寫入通道。
緩衝區本質上是一個可以寫入數據的內存塊,之後可以讀取數據。 Buffer 對象包裝了此內存塊,提供了一組方法,可以更輕鬆地使用內存塊。

Buffer 的基本用法

使用 Buffer 讀取和寫入數據通常遵循以下四個步驟:

  1. 將數據寫入緩衝區
  2. 調用 buffer.flip() 反轉讀寫模式
  3. 從緩衝區讀取數據
  4. 調用 buffer.clear() 或 buffer.compact() 清除緩衝區內容

將數據寫入Buffer 時,Buffer 會跟蹤寫入的數據量。 當需要讀取數據時,就使用 flip() 方法將緩衝區從寫入模式切換到讀取模式。 在讀取模式下,緩衝區允許讀取寫入緩衝區的所有數據。

讀完所有數據之後,就需要清除緩衝區,以便再次寫入。 可以通過兩種方式執行此操作:通過調用 clear() 或調用 compact() 。區別在於 clear() 是方法清除整個緩衝區,而 compact() 方法僅清除已讀取的數據,未讀數據都會移動到緩衝區的開頭,新數據將在未讀數據之後寫入緩衝區。

這是一個簡單的緩衝區用法示例:

public class ChannelExample {
    public static void main(String[] args) throws IOException {
	// 文件內容是 123456789
        RandomAccessFile accessFile = new RandomAccessFile("D:\\test\\1.txt", "rw");
        FileChannel fileChannel = accessFile.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(48); //創建容量爲48字節的緩衝區

        int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區,返回讀入到緩衝區的字節數
        while (data != -1) {
            System.out.println("Read " + data); // Read 9
            buffer.flip(); // 將 buffer 從寫入模式切換爲讀取模式
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get()); // 每次讀取1byte,循環輸出 123456789
            }
            buffer.clear(); // 清除當前緩衝區
            data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區
        }
        accessFile.close();
    }
}
Buffer 的 capacity,position 和 limit

緩衝區有 3 個需要熟悉的屬性,以便了解緩衝區的工作原理。 這些是:

  1. capacity : 容量緩衝區的容量,是它所包含的元素的數量。不能爲負並且不能更改。
  2. position :緩衝區的位置 是下一個要讀取或寫入的元素的索引。不能爲負,並且不能大於 limit
  3. limit : 緩衝區的限制,緩衝區的限制不能爲負,並且不能大於 capacity

另外還有標記 mark ,
標記、位置、限制和容量值遵守以下不變式:
0 <= mark<= position <= limit<= capacity

position 和 limit 的含義取決於 Buffer 是處於讀取還是寫入模式。 無論緩衝模式如何,capacity 總是一樣的表示容量。

以下是寫入和讀取模式下的容量,位置和限制的說明:

capacity

作爲存儲器塊,緩衝區具有一定的固定大小,也稱爲“容量”。 只能將 capacity 多的 byte,long,char 等寫入緩衝區。 緩衝區已滿後,需要清空它(讀取數據或清除它),然後才能將更多數據寫入。

position

將數據寫入緩衝區時,可以在某個位置執行操作。 position​ 初始值爲 ​0 ,當一個 byte,long,char 等已寫入緩衝區時,position 被移動,指向緩衝區中的下一個單元以插入數據。 position 最大值爲 capacity -1

從緩衝區讀取數據時,也可以從給定位置開始讀取數據。 當緩衝區從寫入模式切換到讀取模式時,position 將重置爲 0 。當從緩衝區讀取數據時,將從 position 位置開始讀取數據,讀取後會將 position 移動到下一個要讀取的位置。

limit

在寫入模式下,Buffer 的 limit 是可以寫入緩衝區的數據量的限制,此時 limit=capacity。

將緩衝區切換爲讀取模式時,limit 表示最多能讀到多少數據。 因此,當將 Buffer 切換到讀取模式時,limit被設置爲之前寫入模式的寫入位置(position ),換句話說,你能讀到之前寫入的所有數據(例如之前寫寫入了 6 個字節,此時 position=6 ,然後切換到讀取模式,limit 代表最多能讀取的字節數,因此 limit 也等於 6)。

分配緩衝區

要獲取 Buffer 對象,必須先分配它。 每個 Buffer 類都有一個 allocate() 方法來執行此操作。 下面是一個顯示ByteBuffer分配的示例,容量爲48字節:

ByteBuffer buffer = ByteBuffer.allocate(48); //創建容量爲48字節的緩衝區
將數據寫入緩衝區

可以通過兩種方式將數據寫入 Buffer:

  1. 將數據從通道寫入緩衝區
  2. 通過緩衝區的 put() 方法,自己將數據寫入緩衝區。

這是一個示例,顯示了 Channel 如何將數據寫入 Buffer:

int data = fileChannel.read(buffer); // 將 Channel 的數據讀入緩衝區,返回讀入到緩衝區的字節數
buffer.put(127); // 此處的 127 是 byte 類型

put() 方法有許多其他版本,允許以多種不同方式將數據寫入 Buffer 。 例如,在特定位置寫入,或將一個字節數組寫入緩衝區。

flip() 切換緩衝區的讀寫模式

flip() 方法將 Buffer 從寫入模式切換到讀取模式。 調用 flip() 會將 position 設置回 0,並將 limit 的值設置爲切換之前的 position 值。換句話說,limit 表示之前寫進了多少個 byte、char 等 —— 現在能讀取多少個 byte、char 等。

從緩衝區讀取數據

有兩種方法可以從 Buffer 中讀取數據:

  1. 將數據從緩衝區讀入通道。
  2. 使用 get() 方法之一,自己從緩衝區讀取數據。

以下是將緩衝區中的數據讀入通道的示例:

int bytesWritten = fileChannel.write(buffer);
byte aByte = buffer.get();    

和 put() 方法一樣,get() 方法也有許多其他版本,允許以多種不同方式從 Buffer 中讀取數據。有關更多詳細信息,請參閱JavaDoc以獲取具體的緩衝區實現。

以下列出 ByteBuffer 類的部分方法:

方法 描述
byte[] array() 返回實現此緩衝區的 byte 數組,此緩衝區的內容修改將導致返回的數組內容修改,反之亦然。
CharBuffer asCharBuffer() 創建此字節緩衝區作爲新的獨立的char 緩衝區。新緩衝區的內容將從此緩衝區的當前位置開始
XxxBuffer asXxxBuffer() 同上,創建對應的 Xxx 緩衝區,Xxx 可爲 Short/Int/Long/Float/Double
byte get() 相對 get 方法。讀取此緩衝區當前位置的字節,然後該 position 遞增。
ByteBuffer get(byte[] dst, int offset, int length) 相對批量 get 方法,後2個參數可省略
byte get(int index) 絕對 get 方法。讀取指定索引處的字節。
char getChar() 用於讀取 char 值的相對 get 方法。
char getChar(int index) 用於讀取 char 值的絕對 get 方法。
xxx getXxx(int index) 用於讀取 xxx 值的絕對 get 方法。index 可以選,指定位置。
衆多 put() 方法 參考以上 get() 方法
static ByteBuffer wrap(byte[] array) 將 byte 數組包裝到緩衝區中。
rewind() 倒帶

Buffer對象的 rewind() 方法將 position 設置回 0,因此可以重讀緩衝區中的所有數據, limit 則保持不變。

clear() 和 compact()

如果調用 clear() ,則將 position 設置回 0 ,並將 limit 被設置成 capacity 的值。換句話說,Buffer 被清空了。 但是 Buffer 中的實際存放的數據並未清除。

如果在調用 clear() 時緩衝區中有任何未讀數據,數據將被“遺忘”,這意味着不再有任何標記告訴讀取了哪些數據,還沒有讀取哪些數據。

如果緩衝區中仍有未讀數據,並且想稍後讀取它,但需要先寫入一些數據,這時候應該調用 compact() ,它會將所有未讀數據複製到 Buffer 的開頭,然後它將 position 設置在最後一個未讀元素之後。 limit 屬性仍設置爲 capacity ,就像 clear() 一樣。 現在緩衝區已準備好寫入,並且不會覆蓋未讀數據。

mark() 和 reset()

以通過調用 Buffer 對象的 mark() 方法在 Buffer 中標記給定位置。 然後,可以通過調用 Buffer.reset() 方法將位置重置回標記位置,就像在標準 IO 中一樣。

buffer.mark();
// 調用 buffer.get() 等方法讀取數據...

buffer.reset();  // 設置 position 回到 mark 位置。
equals() 和 compareTo()

可以使用 equals() 和 compareTo() 比較兩個緩衝區。

equals() 成立的條件:

  1. 它們的類型相同(byte,char,int等)
  2. 它們在緩衝區中具有相同數量的剩餘字節,字符等。
  3. 所有剩餘的字節,字符等都相等。

如上,equals 僅比較緩衝區的一部分,而不是它內部的每個元素。 實際上,它只是比較緩衝區中的其餘元素。

compareTo() 方法比較兩個緩衝區的剩餘元素(字節,字符等), 在下列情況下,一個 Buffer 被視爲“小於”另一個 Buffer:

  1. 第一個不相等的元素小於另一個 Buffer 中對應的元素 。
  2. 所有元素都相等,但第一個 Buffer 在第二個 Buffer 之前耗盡了元素(第一個 Buffer 元素較少)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章