關于多路復用
很多人用過InputStream和OutputStream接口,用來操作文件、Socket等等 IO 操作。
如果是簡單的,速度較快的 IO 操作,我們用Stream類的接口,依然可以風生水起。
如果你要使用非阻塞的 IO 的話,他們可能就滿足不了你了。
熟悉操作系統(tǒng)的人會知道,操作非阻塞 IO 無非幾種多路復用:
- select
- poll
- epoll
- kqueue
- IOCP
這里的復用模型有幾個是操作系統(tǒng)相關的——也就是說,并不是所有的操作系統(tǒng)都可以用,典型的就是IOCP是Windows的"專利",kqueue是BSD的"專利"(比如macOS)。
那么 java 作為一門跨平臺的語言解決方案,是如何在虛擬機上使用 non-blocking IO 的呢?
具體的實現(xiàn)我們可以不管,它使用了Selector的 API,調(diào)用方式非常類似select。
Channel & ByteBuffer vs Stream
在nio中,不再使用Stream API對Socket進行交互,而是使用Channel和ByteBuffer進行交互,
Channel負責管道的工作,ByteBuffer負責緩存的工作。
原先InputStream和OutputStream的工作就由Channel做掉了,如果這個Channel支持Select模型的話,它就是SelectableChannel的子類。
那么,在消息循環(huán)的模型中,首先要建立循環(huán),像我們的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
接下來調(diào)用 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 對于客戶端的優(yōu)勢幾乎沒有,但是可以讓代碼更好管理; 如果這時候你使用的是ServerSocket,好處就立馬體現(xiàn)了,因為你的業(yè)務需求很可能是這樣:
- master 線程,開啟 accept.
- 如果有客戶接入,開啟一個 worker,用來服務 client。
- 服務完后,保持或者關閉這個連接。
(這個業(yè)務模型類似Apache httpd)這樣的業(yè)務模型可能導致過多的線程開銷,使得并發(fā)量并不高。
那么,老生常談的event-driven的模型在java中,就差不多是這樣的邏輯:
- master 線程,開啟 selector, 并為 ServerSocket 注冊 accept, read, write 等事件。
- 客戶接入,為 client socket 注冊 read, write 事件,依舊在該線程里面進行循環(huán)。
- 當 event trigger 的時候,處理相關業(yè)務邏輯。
第二個模型只啟動了一個線程,所有的IO操作都在 OS 里面完成了,用戶空間內(nèi)的資源消耗大大降低,這也是我們把 Server 端的 IO 改成 nio 的優(yōu)勢。