Java NIO 學習筆記(一)----概述,Channel/Buffer
Java IO/">NIO (來自 Java 1.4)可以替代標準 IO 和 Java Networking API ,NIO 提供了與標準 IO 不同的使用方式。學習 NIO 之前建議先掌握標準 IO 和 Java 網路程式設計,推薦教程:
- ofollow,noindex" target="_blank">系統學習 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 元素較少)。