在NIO中,數據的讀寫操作始終是與緩沖區相關聯的.讀取時信道(SocketChannel)將數據讀入緩沖區,寫入時首先要將發送的數據按順序填入緩沖區.緩沖區是定長的,基本上它只是一個列表,它的所有元素都是基本數據類型.ByteBuffer是最常用的緩沖區,它提供了讀寫其他數據類型的方法,且信道的讀寫方法只接收ByteBuffer.因此ByteBuffer的用法是有必要牢固掌握的.
1.創建ByteBuffer
1.1 使用allocate()靜態方法
ByteBuffer
buffer=ByteBuffer.allocate(256);
以上方法將創建一個容量為256字節的ByteBuffer,如果發現創建的緩沖區容量太小,唯一的選擇就是重新創建一個大小合適的緩沖區.
1.2 通過包裝一個已有的數組來創建
如下,通過包裝的方法創建的緩沖區保留了被包裝數組內保存的數據.
ByteBuffer
buffer=ByteBuffer.wrap(byteArray);
如果要將一個字符串存入ByteBuffer,可以如下操作:
String sendString="你好,服務器. ";
ByteBuffer
sendBuffer=ByteBuffer.wrap(sendString.getBytes("UTF-16"));
2.回繞緩沖區
buffer.flip();
這個方法用來將緩沖區準備為數據傳出狀態,執行以上方法后,輸出通道會從數據的開頭而不是末尾開始.回繞保持緩沖區中的數據不變,只是準備寫入而不是讀取.
3.清除緩沖區
buffer.clear();
這個方法實際上也不會改變緩沖區的數據,而只是簡單的重置了緩沖區的主要索引值.不必為了每次讀寫都創建新的緩沖區,那樣做會降低性能.相反,要重用現在的緩沖區,在再次讀取之前要清除緩沖區.
4.從套接字通道(信道)讀取數據
int bytesReaded=socketChannel.read(buffer);
執行以上方法后,通道會從socket讀取的數據填充此緩沖區,它返回成功讀取并存儲在緩沖區的字節數.在默認情況下,這至少會讀取一個字節,或者返回-1指示數據結束.
5.向套接字通道(信道)寫入數據
socketChannel.write(buffer);
此方法以一個ByteBuffer為參數,試圖將該緩沖區中剩余的字節寫入信道.
ByteBuffer buffer = ByteBuffer.allocate(1024); //分配一定的空間,1024
int i = 90;
buffer.putInt(i);
byte[] array = buffer.array(); //獲取該buffer的數組,這個數組是跟該buffer一一對應的
for(int j =0; j <4;j++){
System.out.println(Integer.toBinaryString(array[j] & 0xFF));
}
byte[] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);
等效于
ByteBuffer buffer = ByteBuffer.allocate(1024); //分配一定的空間,1024
反轉此緩沖區。首先對當前位置設置限制,然后將該位置設置為零。如果已定義了標記,則丟棄該標記。
當將數據從一個地方傳輸到另一個地方時,經常將此方法與 compact 方法一起使用。
我最終的理解是: 文檔翻譯得太差了, 把不應該翻譯的內容也譯成了中文, 所以反而不容易理解.
關鍵就在以下 2 處:
當前位置: 這個可以直觀地理解為緩沖區中的當前數據指針, 或是 SQL 中的游標, 記為 curPointer.
限制: 這個可以理解成實際操作的緩沖區段的結束標記, 記為 endPointer.
反轉: 這個完全是對 flip 這個詞不負責的翻譯, 如果參照 DirectX 里的 flip() 而譯為翻轉/翻頁, 那就好理解得多, 就像寫信/看信, 寫/看完一頁后, 翻到下一頁, 眼睛/筆從頁底重新移回頁首.
這個翻轉背后的操作其實就是 "把 endPointer 定位到 curPointer 處, 并把 curPointer 設為 0".
關于標記, 在這里不涉及. 下一句說到常與 compact 方法一起使用, 是可以想像的, 因為 compact 方法對數據進
行了壓縮, 有效數據的真實長度發生了變化, 肯定需要用 flip 重新定位結束標記.
在填充, 壓縮等數據操作時, curPointer 估計都是自動更新了位置的, 總是指向最后一個有效數據, 所以每次調
用 flip() 后, endPointer 就指向了有效數據的結尾, 而 curPointer 指向了 0 (緩沖起始處).
舉個圖例:
(c 和 e 分別代表 curPointer 和 endPointer 兩個指針)
* 先是一個空的 ByteBuffer (大小為 10 字節)
-------------------
-------------------
c
e
* 然后填充 5 字節數據
-------------------
0 1 2 3 4
-------------------
e c
此時, endPointer 尚在 0 處, curPointer 移到了數據結尾.
經測試, 此時若取數據, 將得到 5 個字節, 內容通常為 0 (也有可能是未知), 因為實際上取到的是從 c 處到緩沖
區實際結束處的 5 個未初始化的字節.
(QZone 字體處理不正確, 此處 c 是在 4 的下面, e 在 0 的下面)
* 調用一次 flip() 后
-------------------
0 1 2 3 4
-------------------
c e
此時, endPointer 先被移到 curPointer, 然后 curPointer 移到 0.
通過測試可見, ByteBuffer 取數據時, 是從 curPointer 起, 到 endPointer 止, 若 curPointer > endPointer, 則取到緩沖區結束.
(QZone 字體處理不正確, 此處 c 是在 0 的下面, e 在 4 的下面)
再看上面代碼的關鍵片段, 行 8 處調用 flip() 即有兩個作用, 一是將 curPointer 移到 0, 二是將 endPointer 移到有效數據結尾.
此行可由以下兩行代替:
buff.limit( buff.position());
buff.position( 0 );
可見對其工作原理的理解, 應該是正確的
2. 每一次 get 或 put 后, curPointer 都將向緩沖區尾部移動, 移動量=操作的數據量.
3. get/put 均是從 curPointer 起, 到 curPointer + 操作的數據長度止.
4. get/put 操作中, 若 curPointer 超過了 endPointer 或緩沖區總長度, 將拋出 java.nio.BufferUnderflowException 異常.
注: curPointer 和 endPointer 只是為文中方便描述命名的, 實際分別對應到 ByteBuffer.position() 和 ByteBuffer.limit() 兩個方法.
比如 我們有初始化一個ByteBuffer 后有
ByteBuffer buffer = ByteBuffer.allocate(1024);
這是 終止位置limit在1024, 而起始位置position在 0
如果我們添加一個數據,
buffer.putint(90);
這會使起始位置 position 移到4, 也就是說postion始終都在第一個可寫字節的位置上. limit 則不會發生改變
而如果這時,我們調用
buffer.flip();
position轉到0, limit轉到 4 也就是原來的position 所在位置
這里的flip, 從另外一個角度上來說, 是在讀數據時,操作的
然而, 如果我此時在寫
buffer.putInt(90);
就會將原來的覆蓋掉
如果我們在寫, 這時就不行了, 就會重現問題了. 因為我們的limit是4, 我們寫入數據不能超過這個limit,(當然還有capacity)
所以在寫的時候,最好先清空buffer.clear();
如果真的不想清空, 也可以調用
buffer.limit(newlimit);
設置一個較大的limit, 再寫入
當然不能超過capacity, 可以等于 capacity