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


目錄:
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 網絡編程,推薦教程:

本文目的: 掌握了標准 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 元素較少)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM