先回顧下NIO中的"三劍客"模型:selector、channel、buffer
<!-- p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 19.0px; font: 13.0px 'Helvetica Neue'; color: #118eff} -->

對于網絡通訊而言,代碼最常處理的就是3件事:管理連接配接、讀取資料、寫入資料。上圖中,selector就是用來管理連接配接的(通常隻需要一個selector線程處理就行,可避免上下文切換),selector上注冊了一堆channel(通道),channel是雙向的(in/out),讀寫資料時,channel必須通過buffer(緩沖) 。selector通過Event事件輪詢來選擇(或者叫激活)某個channel。
這篇主要回顧buffer的用法,先看類圖:
Buffer是一個抽象類,下面派生出了幾種基本類型的子類,比如網絡通訊中最常用的ByteBuffer,每個子類中真正用于存放資料的,是一個叫hb的數組。Buffer本身是可讀可寫的,為了友善辨別讀/寫的位置,裡面有3個非常重要的辨別變量:
position :表示下1個讀或寫的位置(即:hb數組的下标索引)
limit:hb數組中有效資料的最大位置
capacity:hb數組的容量(注:capacity與limit的差別,比如1個數組的容量是10,但是裡面隻存放了6個有效資料,那麼在讀取時,capacity為10,而limit則為6)
jdk源碼上,已經明确說明: position <= limit <= capacity。
經常容易忽視的方法:flip()
由于buffer是可讀可寫的,以IntBuffer為例,“寫入時”每向buffer中放入1個int數字,position就向後移1位,所有資料寫完後,position就指向最後1個數字的末尾了。“讀取時”需要調用flip()方法把position重新移到位置0,才能讀到資料。
package com.cnblogs.yjmyzz;
import java.nio.IntBuffer;
public class BufferTest {
public static void main(String[] args) {
IntBuffer intBuffer = IntBuffer.allocate(6);
System.out.println("after init => ");
System.out.println("pos:" + intBuffer.position() + ",limit:" + intBuffer.limit() + ",cap:" + intBuffer.capacity() + "\n");
System.out.println("begin write => ");
for (int i = 0; i < 4; i++) {
intBuffer.put(i + 1);
System.out.println("pos:" + intBuffer.position() + ",limit:" + intBuffer.limit() + ",cap:" + intBuffer.capacity());
}
System.out.println("\n-------before flip----------");
System.out.println("pos:" + intBuffer.position() + ",limit:" + intBuffer.limit() + ",cap:" + intBuffer.capacity());
intBuffer.flip();
System.out.println("\n-------after flip----------");
System.out.println("pos:" + intBuffer.position() + ",limit:" + intBuffer.limit() + ",cap:" + intBuffer.capacity() + "\n");
System.out.println("begin read =>");
for (int i = 0; i < 4; i++) {
System.out.println(intBuffer.get());
System.out.println("pos:" + intBuffer.position() + ",limit:" + intBuffer.limit() + ",cap:" + intBuffer.capacity());
}
}
}
複制
輸出結果:
after init =>
pos:0,limit:6,cap:6
begin write =>
pos:1,limit:6,cap:6
pos:2,limit:6,cap:6
pos:3,limit:6,cap:6
pos:4,limit:6,cap:6
-------before flip----------
pos:4,limit:6,cap:6
-------after flip----------
pos:0,limit:4,cap:6
begin read =>
1
pos:1,limit:4,cap:6
2
pos:2,limit:4,cap:6
3
pos:3,limit:4,cap:6
4
pos:4,limit:4,cap:6
複制
圖解如下,這是剛初始化後:
寫入4個元素後:
flip之後:
所有資料讀取完成後:
接下來說一說mark(),這個方法必須與reset()結合才能看出效果,字面了解:mark就是在某個位置打個标志(做個記号),資料讀取過程中,position會不斷向前移,如果又想回到剛才做了記号的位置,調用reset即可。
IntBuffer intBuffer = IntBuffer.allocate(6);
for (int i = 0; i < 4; i++) {
intBuffer.put(i + 1);
}
intBuffer.flip();
System.out.println(intBuffer.get());
System.out.println(intBuffer.get());
System.out.println("-------after read 2 times----------");
System.out.println("pos:" + intBuffer.position() + ",limit:" + intBuffer.limit() + ",cap:" + intBuffer.capacity() + "\n");
intBuffer.mark();
System.out.println("-------after read 3 times----------");
System.out.println(intBuffer.get());
System.out.println("pos:" + intBuffer.position() + ",limit:" + intBuffer.limit() + ",cap:" + intBuffer.capacity() + "\n");
intBuffer.reset();
System.out.println("-------after reset----------");
System.out.println("pos:" + intBuffer.position() + ",limit:" + intBuffer.limit() + ",cap:" + intBuffer.capacity() + "\n");
複制
輸出:
仔細觀察上面的輸出,在讀取了2個數字後,position向前移動到了位置2,這時調用mark做了标記,然後再讀取1個數,position繼續向前移到3,然後再調用reset,position又退回到剛才的位置2。利用mark和reset,可以在緩沖區中,重複讀取某一段資料。
ByteBuffer的資料類型問題
網絡傳輸中ByteBuffer恐怕是用得最多的一種緩沖,特别要注意下“資料類型”問題。寫入資料時,除了put方法外,ByteBuffer還提供了一系列的putXXX方法
類似的,讀取資料時除了get之外,還有一系列的getXXX方法
比如在第位置1,調用了putChar('楊')寫入資料,相應的讀取位置1時,要調用getChar(),如果不比對,可能會有異想不到的問題。
ByteBuffer buffer = ByteBuffer.allocate(5);
buffer.putShort((short) 1); // (1)
buffer.putChar('楊'); // (2)
buffer.flip();
System.out.println(buffer.getShort()); //這裡要與(1)處put的類型一緻
System.out.println(buffer.getChar()); //這裡要與(2)處put的類型一緻
複制
參考文檔:
https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/nio/Buffer.html