Java NIO Selector 是一个可以选择一个或多个 Channel
实例、确定哪个 Channel
处于可写或可读状态的组件。
通过这种方式,一个线程可以管理多个 Channel
、多个网络连接。
为什么使用 Selector
只使用一个线程去处理多个channel
, 相较于使用多个线程来处理是有优势的。事实上,我们可以只使用一个线程去处理所有的channels
。
在操作系统中,切换线程的开销是昂贵的,且每一个线程都会占用操作系统的资源(内存等)。因此使用更少的线程性能也就更好。
需要注意的是,现代操作系统和CPU,在多任务处理的支持上是越来越好了,因此随着时间的推移多线程的开销会变得更小。
事实上,如果一个CPU有多个核心,不使用多任务会是一种浪费。在这里不讨论这个问题,只需要说明,可以使用selector
通过一个线程来管理多个channel
。
下面是一个示例图,使用selector
管理3个channel
:
创建 Selector
通过调用Selector.open()
方法来创建一个Selector
:
Selector selector = Selector.open();
注册 Channel 到 Selector
为了通过Selector
来使用Channel
, 就必须注册Channel
. 通过调用SelectableChannel.register()
方法来实现:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
使用Selector
有一个前提, Channel
必须是非阻塞模式(non-blocking)。也就是说,FileChannel
及其继承类就不可以使用(因为其不能切换到非阻塞模式),SocketChannel
是可以使用的。
需要注意下register()
方法的第二个参数,需要声明这个channel
的事件,总共有四个不同的事件可供选择:
- SelectionKey.OP_READ (read operations.)
- SelectionKey.OP_WRITE (write operations.)
- SelectionKey.OP_CONNECT (socket-connect operations.)
- SelectionKey.OP_ACCEPT (socket-accept operations.)
每一个事件都相当于是ready
状态, 一些解释如下:
- 当channel准备好读数据时,状态是
read ready
- 当channel准备好写数据时,状态是
write ready
- 当channel连接到另一个server成功时,状态是
connect ready
- 当server socket chennel 接收到连接请求时,状态是
accept ready
如果想需要同时多个事件,可以这样传参:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey
注册Channel时调用的register()
方法会返回一个SelectionKey
对象,这个对象的一些属性我们简单来看下:
Interest Set
interest set 是一组事件组合。我们可以通过SelectionKey
来读写inserset set.
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
可以通过&
操作符来判断所需操作是否在inserset set
Ready Set
ready set 是一组channel准备进行的操作集合。
int readySet = selectionKey.readyOps();
// 可以通过下面几个方法来测试 操作是否可以执行
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel + Selector
如何从SelectionKey
中获取Channel和Selector呢?
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Attaching Objects
我们可以把一个对象附加到SelectionKey
,这样就可以方便的识别给定的channel。举个例子:
// 入
selectionKey.attach(theObject);
// 出
Object attachedObj = selectionKey.attachment();
当然也可以在channel注册时进行附加
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
从Selector查询Channels
一旦你通过selector注册了一个或多个channel,就可以调用select()
方法,这些方法返回处在所定义的ready
状态的channels。
换句话说,如果你希望读状态就绪的channel,你就会通过select()
方法拿到所有已经处在read ready状态的channels.
下面列举了一些select()
方法:
- int select()
- int select(long timeout)
- int selectNow()
select()
是阻塞的,一直等到至少有一个channel是所需事件ready状态时才会返回。
select(long timeout)
同样是阻塞的,但是允许给定最长等待时间,毫秒级。
selectNow()
非阻塞,会立即返回已经ready的channels。
select()
方法返回的是有多少个channel是ready状态。也就是说,自上次调用select()
以来又有多少个channel是ready状态。
如果调用select()
返回了1则是因为有1个已是ready状态。当调用select()
多于1次时,且又一个channel已ready,则仍返回1. 如果在两次调用之间没有对ready的channel执行任何操作,现在就有两个channel是ready的,但是只有一个channel变为ready在两次调用select()
之间。
selectedKeys()
当调用select()
方法时,返回的结果表示有几个channel是ready状态,可以通过"selected key set"来获取ready状态的channel,方法是selectedKeys()
:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
通过channel.register()
注册channel到一个Selector后,方法返回了一个SelectionKey
对象。
可以从SelectionKey
通过selectedKeySet()
来获取这些channels。
可以通过迭代这个keySet来获取ready的channel.
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
主要注意的是,keyIterator.remove();
方法要在最后被调用。Selector
自己是不会进行移除的。
SelectionKey.channel()
返回的channel实例,需要强制转换为需要的Channel类。比如ServerSocketChannel
.
wakeUp()
当线程调用阻塞的select()
方法时,即时没有channel是ready状态也可以让线程退出select()
方法。
怎么做到呢?需要另外一个线程去调用被阻塞线程使用的Selector
的wakeup()
方法,之后被select()
阻塞的线程就会立即返回。
注意 如果另外一个线程调用wakeUp()
时没有其它线程被select()
阻塞,则下一个线程调用select()
时会立即返回.
close()
当使用完Selector
时需要调用close()
方法,它会销毁Selector
及所有使用SelectorKey
的实例。Channel并不会被销毁。
本例代码
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.selectNow();
if(readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}