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 的對比和總結html

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

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

Java NIO 概覽

NIO 由如下核心組件組成:數組

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

  2. 非阻塞IO
    NIO 能夠執行非阻塞 IO 。 例如,當通道將數據讀入緩衝區時,線程能夠執行其餘操做。 而且一旦數據被讀入緩衝區,線程就能夠繼續處理它。 將數據寫入通道也是如此。網絡

  3. 選擇器
    NIO 包含「選擇器」的概念。 選擇器是一個能夠監視多個事件通道的對象(例如:鏈接打開,數據到達等)。 所以,單個線程能夠監視多個通道的數據。app

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

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 元素較少)。
相關文章
相關標籤/搜索