天天看點

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

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

目錄:

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

Java NIO 學習筆記(二)----聚集和分散,通道到通道

Java NIO 學習筆記(三)----Selector

Java NIO 學習筆記(四)----檔案通道和網絡通道

Java NIO 學習筆記(五)----路徑、檔案和管道 Path/Files/Pipe

Java NIO 學習筆記(六)----異步檔案通道 AsynchronousFileChannel

Java NIO 學習筆記(七)----NIO/IO 的對比和總結

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

  • 系統學習 Java IO----目錄,概覽
  • 初步接觸 Java Net 網絡程式設計

本文目的: 掌握了标準 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 的線程圖示:

要使用選擇器,需要使用它注冊通道。 然後你調用它的 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 元素較少)。

如果覺得本文有所幫助,歡迎點【推薦】!文章錯誤之處煩請留言。

轉載說明:轉載後必須在文章開頭明顯地給出作者和原文連結;引用必須注明出處;需要二次修改釋出請聯系作者征得同意。