Java NIO Selector是一个组件,它可以检测Channel对象,并且决定哪些channel是已经ready、readable、writable等状态。通过这个组件,一个线程可以管理多个channel,进而管理多个网络连接。
为什么使用Selector
只使用一个线程来处理多个Channel的优势是,需要更少的线程来处channel。事实上,你可以使用一个线程管理所有的channel。线程切换对于操作系统的消耗是十分巨大的,而且多个线程会占去系统更多的内存。因此线程的数量越少越好。
记住,现代操作系统里CPU处理多线程的能力变得越来越强,因此多线程的开销会随年代的推移越来越不那么重要。事实上,如果一个CPU是多核的,你不使用多线程会浪费CPU的性能。
您可以使用Selector在一个线程中处理多个Channel。如图所示:
创建一个Selector
调用Selector的静态方法open,就能创建一个Selector对象了。
Selector selector = Selector.open();
把Selector注册到Channel中
为了使用Selector管理channel,必须把selector注册到channel中。使用channel的register方法即可:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
channel必须是非阻塞的模式下才能使用selector,因此第一行就设置了非阻塞模式。(FileChannel就不能配合selector使用,因为它就不能设置成非阻塞模式,SocketChannel就没问题了)
注意第二个参数,它是用来表示“关注点”,它意味着你具体关心什么事件。下面有四种不同的事件你可以监听:
- Connect
- Accept
- Read
- Write
一个channel触发了某个事件,也可以称作某个事件已经就绪。因此一个channel成功的连接到一个服务应该触发“connect ready”(也就是Connect)事件。
一个SocketChannel收到了连接会触发“accept”事件。
一个channel有数据可以读取了会触发“read”事件。
一个channel已经准备好可以被写入了会触发“write”事件。
这四个事件有四种代码上的表达方式:
- Connect == SelectionKey.OP_CONNECT
- Accept == SelectionKey.OP_ACCEPT
- Read == SelectionKey.OP_READ
- Write == SelectionKey.OP_WRITE
如果需要多个事件同时监听,可以使用“或”逻辑,像这样:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey
正如你看到的,像上一段代码中,把selector注册到channel中会返回一个SelectionKey的对象。这个对象包含一些重要属性:
- The interest set
- The ready set
- The Channel
- The Selector
- An attached object (optional)
Interest 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;
可以使用 “与”逻辑符,来查看监听了哪些事件。
Ready Set
这部分是指channel已经就绪了哪些事件:
int readySet = selectionKey.readyOps();
可以使用Interest Set的检测方式来测试,也可以直接使用下面的四个方法来代替:
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或将更多信息附加到channel上的简便方法。例如,可以将正在使用的buffer与channel或包含更多聚合数据的对象关联到一起。
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
也可以在向channel中注册selector时添加对象。
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
通过Selector选择Channel
一旦使用selector注册到一个或多个channel之后,就可以调用Selector的select方法了。会返回selector注册的事件已经触发了的channel。换言之,如果你注册了read事件,你就会收到可以读的channel。
几个类似的select方法:
- int select()
- int select(long timeout)
- int selectNow()
select()
阻塞直到有指定事件触发为止
select(long timeout)
和上一个类似,多加了一个超时时间
selectNow()
绝对不会阻塞,它会立刻返回哪些channel是满足条件的(如果没有就是空,这个方法用来监控连接状态还不错)
select()方法返回的int值表示有多少通道已经就绪。也就是说,自从上次调用select()以来,已经准备好了多少通道。如果您调用select(),它将返回1,因为一个通道已经就绪,而您再次调用select(),又有一个通道已经就绪,那么它将再次返回1。如果对第一个就绪的通道不做任何操作,那么现在就有两个就绪通道,但是在每个select()调用之间只有一个通道就绪。
selectedKeys()
一旦你调用了select()方法中的一个,并且它的返回值表明一个或多个channel已经就绪,就可以通过“selected key set”来访问就绪channel,方法是调用selectors selectedKeys()方法。它是这样的:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
当你使用selector注册一个channel时,channel.register()方法将返回一个SelectionKey对象。这个值就代表了注册的那个选择器。可以通过selectedKeySet()方法获取这些值。然后迭代这个Set:
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(); // notice this line
}
这个循环遍历集合中的key。对于每个key,都要判断以确定这个key所对应的channel是否已经准备好了。
请注意在每次迭代结束时调用keyIterator.remove()。选择器不会从所选的键集本身删除SelectionKey实例。下一次通道“就绪”时,选择器将再次将其添加到选定的键集。
应该将SelectionKey.channel()方法返回的通道类型转换为需要使用的通道类型。
wakeUp()
调用了被阻塞的select()方法的线程可以离开select()方法,即使还没有通道就绪。这是通过让另一个线程调用选择器上的select.wakeup()方法来实现的,第一个线程在这个选择器上调用了select()。在select()中等待的线程将立即返回。
close()
selector使用完毕后,调用它的close()方法关闭选择器,并把该选择器已注册的所有SelectionKey对象都置为失效状态。但是channel本身并没有关闭,不受selector的关闭影响。
完整Selector示例
下面的完整例子,包含了打开selector,注册到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();
}
}