一. NIO 基礎 non-blocking io 非阻塞 IO 1. 三大組件 1.1 Channel & Buffer channel 有一點類似於 stream,它就是讀寫數據的雙向通道,可以從 channel 將數據讀入 buffer,也可以將 buffer 的數據寫入 channel,而之 ...
一. NIO 基礎
non-blocking io 非阻塞 IO
1. 三大組件
1.1 Channel & Buffer
channel 有一點類似於 stream,它就是讀寫數據的雙向通道,可以從 channel 將數據讀入 buffer,也可以將 buffer 的數據寫入 channel,而之前的 stream 要麼是輸入,要麼是輸出,channel 比 stream 更為底層
graph LR channel --> buffer buffer --> channel常見的 Channel 有
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
buffer 則用來緩衝讀寫數據,常見的 buffer 有
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
1.2 Selector
selector 單從字面意思不好理解,需要結合伺服器的設計演化來理解它的用途
多線程版設計
graph TD subgraph 多線程版 t1(thread) --> s1(socket1) t2(thread) --> s2(socket2) t3(thread) --> s3(socket3) end⚠️ 多線程版缺點
- 記憶體占用高
- 線程上下文切換成本高
- 只適合連接數少的場景
線程池版設計
graph TD subgraph 線程池版 t4(thread) --> s4(socket1) t5(thread) --> s5(socket2) t4(thread) -.-> s6(socket3) t5(thread) -.-> s7(socket4) end⚠️ 線程池版缺點
- 阻塞模式下,線程僅能處理一個 socket 連接
- 僅適合短連接場景
selector 版設計
selector 的作用就是配合一個線程來管理多個 channel,獲取這些 channel 上發生的事件,這些 channel 工作在非阻塞模式下,不會讓線程吊死在一個 channel 上。適合連接數特別多,但流量低的場景(low traffic)
graph TD subgraph selector 版 thread --> selector selector --> c1(channel) selector --> c2(channel) selector --> c3(channel) end調用 selector 的 select() 會阻塞直到 channel 發生了讀寫就緒事件,這些事件發生,select 方法就會返回這些事件交給 thread 來處理
2. ByteBuffer
有一普通文本文件 data.txt,內容為
1234567890abcd
使用 FileChannel 來讀取文件內容
@Slf4j
public class ChannelDemo1 {
public static void main(String[] args) {
try (RandomAccessFile file = new RandomAccessFile("helloword/data.txt", "rw")) {
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(10);
do {
// 向 buffer 寫入
int len = channel.read(buffer);
log.debug("讀到位元組數:{}", len);
if (len == -1) {
break;
}
// 切換 buffer 讀模式
buffer.flip();
while(buffer.hasRemaining()) {
log.debug("{}", (char)buffer.get());
}
// 切換 buffer 寫模式
buffer.clear();
} while (true);
} catch (IOException e) {
e.printStackTrace();
}
}
}
輸出
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 讀到位元組數:10
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 1
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 2
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 3
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 4
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 5
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 6
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 7
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 8
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 9
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 0
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 讀到位元組數:4
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - a
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - b
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - c
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - d
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 讀到位元組數:-1
2.1 ByteBuffer 正確使用姿勢
- 向 buffer 寫入數據,例如調用 channel.read(buffer)
- 調用 flip() 切換至讀模式
- 從 buffer 讀取數據,例如調用 buffer.get()
- 調用 clear() 或 compact() 切換至寫模式
- 重覆 1~4 步驟
2.2 ByteBuffer 結構
ByteBuffer 有以下重要屬性
- capacity
- position
- limit
一開始
寫模式下,position 是寫入位置,limit 等於容量,下圖表示寫入了 4 個位元組後的狀態
flip 動作發生後,position 切換為讀取位置,limit 切換為讀取限制
讀取 4 個位元組後,狀態
clear 動作發生後,狀態
compact 方法,是把未讀完的部分向前壓縮,然後切換至寫模式