掌握了标準 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 由以下核心元件組成:
-
通道和緩沖區
在标準 IO API 中,使用位元組流和字元流。 在 NIO 中使用通道和緩沖區。 資料總是從通道讀入緩沖區,或從緩沖區寫入通道。
-
非阻塞IO
NIO 可以執行非阻塞 IO 。 例如,當通道将資料讀入緩沖區時,線程可以執行其他操作。 并且一旦資料被讀入緩沖區,線程就可以繼續處理它。 将資料寫入通道也是如此。
-
選擇器
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 讀取和寫入資料通常遵循以下四個步驟:
- 将資料寫入緩沖區
- 調用 buffer.flip() 反轉讀寫模式
- 從緩沖區讀取資料
- 調用 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 個需要熟悉的屬性,以便了解緩沖區的工作原理。 這些是:
- capacity : 容量緩沖區的容量,是它所包含的元素的數量。不能為負并且不能更改。
- position :緩沖區的位置 是下一個要讀取或寫入的元素的索引。不能為負,并且不能大于 limit
- 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:
- 将資料從通道寫入緩沖區
- 通過緩沖區的 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 中讀取資料:
- 将資料從緩沖區讀入通道。
- 使用 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() 成立的條件:
- 它們的類型相同(byte,char,int等)
- 它們在緩沖區中具有相同數量的剩餘位元組,字元等。
- 所有剩餘的位元組,字元等都相等。
如上,equals 僅比較緩沖區的一部分,而不是它内部的每個元素。 實際上,它隻是比較緩沖區中的其餘元素。
compareTo() 方法比較兩個緩沖區的剩餘元素(位元組,字元等), 在下列情況下,一個 Buffer 被視為“小于”另一個 Buffer:
- 第一個不相等的元素小于另一個 Buffer 中對應的元素 。
- 所有元素都相等,但第一個 Buffer 在第二個 Buffer 之前耗盡了元素(第一個 Buffer 元素較少)。
如果覺得本文有所幫助,歡迎點【推薦】!文章錯誤之處煩請留言。
轉載說明:轉載後必須在文章開頭明顯地給出作者和原文連結;引用必須注明出處;需要二次修改釋出請聯系作者征得同意。