零拦惋、本文綱要
一、NIO三大組件
- Channel
- Buffer
- Selector
二、Buffer
- 基礎依賴
- ByteBuffer使用
- ByteBuffer結構
- ByteBuffer常見方法
三顾彰、Buffer使用模擬
- 情景模擬
- 模擬還原數據
一、NIO三大組件
NIO胃碾,non-blocking io 非阻塞 IO
Channel / Buffer / Selector
1. Channel
雙向通道涨享,可以從channel將數據讀入buffer,也可以將buffer的數據寫入channel仆百;
與stream對比厕隧,stream是單向的,要么輸入要么輸出俄周。
常見的Channel:
FileChannel / DatagramChannel / SocketChannel / ServerSocketChannel
2. Buffer
用來緩沖讀寫數據吁讨。
常見的Buffer:
ByteBuffer(MappedByteBuffer/DirectByteBuffer/HeapByteBuffer) /
ShortBuffer / IntBuffer / LongBuffer / FloatBuffer / DoubleBuffer / CharBuffer
3. Selector
① 多線程處理多個Socket連接
單個Thread對應單個Socket
內存占用高 / 線程上下文切換成本高 / 僅適合【連接數少】的場景
② 線程池處理多個Socket連接
單個Thread可以處理多個Socket
阻塞模式下線程只能處理一個Socket / 僅適合【短連接】的場景
③ selector配合線程處理多個Socket
selector 的作用就是配合一個線程來管理多個 channel,獲取這些 channel 上發(fā)生的事件栈源,這些 channel 工作在非阻塞模式下挡爵,不會讓線程吊死在一個 channel 上竖般。
適合連接數特別多甚垦,但流量低的場景(low traffic)。
調用 selector 的 select() 會阻塞直到 channel 發(fā)生了讀寫就緒事件涣雕,這些事件發(fā)生艰亮,select 方法就會返回這些事件交給 thread 來處理。
二挣郭、Buffer
0. 基礎依賴
netty-all 4.1.39.Final
lombok 1.16.18
gson 2.8.5
guava 19.0
logback-classic 1.2.3
protobuf-java 3.11.3
1. ByteBuffer使用
a迄埃、向 buffer 寫入數據,例如調用 channel.read(buffer)
b兑障、調用 flip() 切換至讀模式
c侄非、從 buffer 讀取數據,例如調用 buffer.get()
d流译、調用 clear() 或 compact() 切換至寫模式
e逞怨、重復 1~4 步驟
try (RandomAccessFile file = new RandomAccessFile("src/main/resources/data.txt", "rw")) {
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(16);
do {
//1. 向 buffer 寫入
int len = channel.read(buffer);
log.debug("讀到的字節(jié)數:{}", len);
if (len == -1) {
break;
}
//2. 切換 buffer 讀模式
buffer.flip();
while (buffer.hasRemaining()) {
log.debug("{}", (char) buffer.get());
}
//3. 切換 buffer 寫模式
buffer.clear();
} while (true);
} catch (IOException e) {
log.info(e.getMessage());
}
2. ByteBuffer結構
// Creates a new buffer with the given mark, position, limit, capacity,
// backing array, and array offset
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
mark 標記位
position 當前位
limit 界限位
capacity 容量
backing array 支撐數組
array offset 數組偏移
3. ByteBuffer常見方法
① allocate方法
用來給ByteBuffer分配空間
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) {...} //此時容量對應limit寫上線
② channel#read方法 / buffer#put方法
向 buffer 寫入數據
FileChannelImpl#read → IOUtil#readIntoNativeBuffer
public final ByteBuffer put(byte[] src) {
return put(src, 0, src.length);
}
public ByteBuffer put(byte[] src, int offset, int length) {
checkBounds(offset, length, src.length);
if (length > remaining())
throw new BufferOverflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
this.put(src[i]);
return this;
}
③ filp方法
切換至【讀模式】,重置position福澡、limit叠赦,可從buffer中讀取數據
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
注意:
a、filp方法將 寫limit定位到讀limit革砸,position重置為0除秀,進而讀取內容糯累。
b、另外此時mark也會被清除册踩。
④ hasRemaining方法
判斷是否仍有剩余數據
public final boolean hasRemaining() {
return position < limit;
}
⑤ buffer#get方法 / channel#write
HeapByteBuffer#get → Buffer#nextGetIndex
FileChannel#write(ByteBuffer[] srcs)
get方法注意點:
a泳姐、會使 position 讀指針向后走;
b暂吉、可以使用 rewind 方法仗岸,使 position 重置,而limit不變借笙,用來重復度扒怖;
c、調用 get(int i) 方法獲取索引 i 的內容业稼,它不會移動讀指針盗痒。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
注意:rewind方法會重置mark標記。
對比filp與rewind:后者 rewind 沒有改變 limit指針 所指向的讀上限低散。
⑥ clear方法
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
注意:clear方法并沒有清除內容俯邓,而是改變了指針的指向,提升了效率熔号。
⑦ compact方法
HeapByteBuffer#compact
注意:compact方法允許我們未讀完稽鞭,而且可以在未讀的后一個位置重新開始寫。
⑧ mark方法 & reset方法
public final Buffer mark() {
mark = position;
return this;
}
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
注意:mark方法與reset方法允許我們在任意mark位置重新讀引镊,rewind方法是從頭開始朦蕴。
⑨ 字符串 與 buffer 互相轉換
ByteBuffer buffer = StandardCharsets.UTF_8.encode("StrToBuffer");
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer);
三、Buffer使用模擬
1. 情景模擬
網絡通信:
a弟头、客戶端發(fā)送多條數據給服務端吩抓,數據間使用"\n"分隔;
b赴恨、數據接收時為了提升效率疹娶,數據會被服務端重新組合。
模擬數據為:
a伦连、Hello, NIO.\n
b雨饺、I`m Stone.\n
c、How are you?\n
此時惑淳,服務器將數據重組额港,出現(xiàn)ByteBuffer (黏包,半包)汛聚,如下:
a锹安、Hello, NIO.\nI`m Stone.\nHo 【24bytes】
c、w are you?\n 【11bytes】
2. 模擬還原數據
省略了buffer動態(tài)擴容與收縮的業(yè)務邏輯,實際使用時叹哭,框架內一般會有代碼實現(xiàn)忍宋。
@Slf4j
public class BufferDemo01 {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(32);
//1. 接收到第一組數據
//1.1 模擬接收到第一組數據
buffer.put("Hello, NIO.\nI`m Stone.\nHo".getBytes(StandardCharsets.UTF_8));
//1.2 處理第一組數據
split(buffer);
//2. 接收到第二組數據
//2.1 模擬接收到第二組數據
buffer.put("w are you?\n".getBytes(StandardCharsets.UTF_8));
//2.2 處理第二組數據
split(buffer);
}
public static void split(ByteBuffer buffer) {
//1. 切換至 讀模式
buffer.flip();
//2. 記錄當前 讀上限
int originLimit = buffer.limit();
//3. 處理當前數據
for (int i = 0; i < originLimit; i++) {
//3.1 如果讀取到的數據是規(guī)定的 分隔符"\n"
if (buffer.get(i) == '\n') {
log.debug("當前分隔符所在的位置:{},buffer.position():{}风罩。", i, buffer.position());
ByteBuffer message = ByteBuffer.allocate(i + 1 - buffer.position());
buffer.limit(i + 1); //3.2 調整當前讀上限為 message 容量
message.put(buffer); //3.3 從 buffer 讀糠排,向 message 寫
//debugAll(message); //該方法是打印當前 message 的方法
buffer.limit(originLimit); //3.4 調整當前讀上限為原先讀originLimit
}
}
//4. 如果當前數據有剩余,則將當前數據拼接至下組數據
buffer.compact();
}
}
四超升、結尾
以上即為Netty基礎-NIO(一)的全部內容入宦,感謝閱讀。