Java NIO 入門
很多人用過 InputStream 和 OutputStream 接口,用來操作 文件 、 Socket 等等 IO 操作。
如果是簡單的,速度較快的 IO 操作,我們用 Stream 類的接口,依然可以風生水起。
如果你要使用非阻塞的 IO 的話,他們可能就滿足不了你了。
熟悉操作系統的人會知道,操作非阻塞 IO 無非幾種多路復用:
- select
- poll
- epoll
- kqueue
- IOCP
這里的復用模型有幾個是操作系統相關的——也就是說,并不是所有的操作系統都可以用,典型的就是 IOCP 是 Windows 的”專利”, kqueue 是 BSD 的”專利”(比如macOS)。
那么 java 作為一門跨平臺的語言解決方案,是如何在虛擬機上使用 non-blocking IO 的呢?
具體的實現我們可以不管,它使用了 Selector 的 API,調用方式非常類似 select 。
Channel & ByteBuffer vs Stream
在 nio 中,不再使用 Stream API 對 Socket 進行交互,而是使用 Channel 和 ByteBuffer 進行交互,
Channel 負責管道的工作, ByteBuffer 負責緩存的工作。
原先 InputStream 和 OutputStream 的工作就由 Channel 做掉了,如果這個 Channel 支持 Select 模型的話,它就是 SelectableChannel 的子類。
那么,在消息循環的模型中,首先要建立循環,像我們的 Looper.loop() 一樣,我們先用 Selector.open() 新建一個 Selector
Selector eventSelector = Selector.open();
// 設置這個 channel 是非阻塞的
socketChannel.configureBlocking(false);
// 注冊到 selector 里,并設置好關心的事件
socketSelectionKey = socketChannel.register(eventSelector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
Selector
接下來調用 eventSelector.select() 阻塞,就能在你關心的事件到來的時候,阻塞就會被喚醒,處理事件。
sample:
while (connected) {
eventSelector.select();
Set<SelectionKey> keys = eventSelector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
// 當 socket 可讀
internalOnRead((ReadableByteChannel) key.channel());
}
if (key.isWritable()) {
// 當 socket 可寫
internalOnWrite((WritableByteChannel) key.channel());
}
iterator.remove();
}
}
總結
nio 對于客戶端的優勢幾乎沒有,但是可以讓代碼更好管理; 如果這時候你使用的是 ServerSocket ,好處就立馬體現了,因為你的業務需求很可能是這樣:
- master 線程,開啟 accept.
- 如果有客戶接入,開啟一個 worker,用來服務 client。
- 服務完后,保持或者關閉這個連接。
(這個業務模型類似Apache httpd)這樣的業務模型可能導致過多的線程開銷,使得并發量并不高。
那么,老生常談的 event-driven 的模型在 java 中,就差不多是這樣的邏輯:
- master 線程,開啟 selector, 并為 ServerSocket 注冊 accept, read, write 等事件。
- 客戶接入,為 client socket 注冊 read, write 事件,依舊在該線程里面進行循環。
- 當 event trigger 的時候,處理相關業務邏輯。
第二個模型只啟動了一個線程,所有的 IO 操作都在 OS 里面完成了,用戶空間內的資源消耗大大降低,這也是我們把 Server 端的 IO 改成 nio 的優勢。
來自:https://geminiwen.xyz/2017/02/21/learn-nio/