关于多路复用
很多人用过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 的优势。