Java NIO Selector
Java NIO Selector
Java NIO Selector是一个可以检查一个或多个Java NIO Channel实例的组件,并确定哪些Channel可以用于例如读或写。 这样,单个线程可以管理多个Channel,从而管理多个网络连接。
Why Use a Selector
仅使用单个线程来处理多个Channel的优点是可以用更少的线程来处理Channel。 实际上,您只需使用一个线程来处理所有Channel。 对于操作系统而言,在线程之间切换是昂贵的,并且每个线程也占用操作系统中的一些资源(存储器)。 因此,您使用的线程越少越好。
但请记住,现代操作系统和CPU在多任务处理中变得越来越好,因此随着时间的推移,多线程的开销会变得越来越小。 事实上,如果一个CPU有多个内核,你可能会因单任务而浪费CPU能力。 无论如何,该设计讨论不在本文范畴。 这里可以说,您可以使用选择器使用单个线程处理多个通道。
以下是使用Selector处理3个Channel的线程图示:
Creating a Selector
您可以通过调用Selector.open()方法创建一个Selector,如下所示:
Selector selector = Selector.open();
Registering Channels with the Selector
要使用带选择器的通道,必须使用选择器注册通道。 这是使用SelectableChannel.register()方法完成的,如下所示:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Channel必须处于非阻塞模式才能与选择器一起使用。 这意味着您无法将FileChannel与Selector一起使用,因为FileChannel无法切换到非阻塞模式。 套接字通道可以正常工作。
注意register()方法的第二个参数。 这是一个“兴趣集”,意味着您有兴趣在通道中通过选择器收听哪些事件。 您可以收听四种不同的活动:
- Connect
- Accept
- Read
- Write
一个“发起事件”的通道也被称为“准备好”该事件。 因此,已成功连接到另一台服务器的通道是“准备就绪”。 接受传入连接的服务器套接字通道已准备好“接受”。 准备好要读取数据的通道已准备好“读取”。 准备好为您写入数据的通道已准备好“写入”。
这四个事件由四个SelectionKey常量表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果您对多个事件感兴趣,将常量放在一起做位或运算,如下所示:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey
正如您在上一节中看到的,当您使用Selector注册Channel时,register()方法返回一个SelectionKey对象。 这个SelectionKey对象包含一些有趣的属性:
- The Interest Set(感兴趣的事件集)
- The ready set(就绪的事件集)
- The Channel
- The Selector
- An attached object (optional)
Interest Set
兴趣集是您感兴趣的“选择”事件集,如“使用选择器注册频道”部分所述。 您可以通过SelectionKey读取和写入该兴趣集,如下所示:
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
就绪集是通道准备好的一组操作。 您将主要在选择后访问就绪集。 选择将在后面的部分中解释。 您可以像这样访问就绪集:
int readySet = selectionKey.readyOps();
您可以使用与兴趣集相同的方式进行测试通道准备好的事件/操作。 但是,您也可以使用这四种方法,它们将会返回一个布尔值:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel + Selector
从SelectionKey访问通道和选择器是轻而易举的。 以下是它的完成方式:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Attaching Objects
您可以将对象附加到SelectionKey,这是识别给定通道或将更多信息附加到通道的便捷方式。 例如,您可以将和通道一起使用的缓冲区或包含更多聚合数据的对象相关联。 以下是如何附加对象:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
您还可以在register()方法中使用选择器注册Channel时附加对象。 这是看起来如何:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
Selecting Channels via a Selector
使用Selector注册一个或多个通道后,可以调用其中一个select()方法。 这些方法返回您感兴趣且“准备好”的事件(连接,接受,读取或写入)的相关通道。 换句话说,如果您对准备好阅读的频道感兴趣,您将收到准备好从select()方法中读取的通道。
以下是select()方法:
- int select()
- int select(long timeout)
- int selectNow()
select()阻塞,直到至少有一个通道为您注册的事件做好准备。
select(long timeout)与select()的作用相同,只是它会阻塞最大超时毫秒数(参数)。
selectNow()根本不会阻塞。 它会立即返回任何已准备好的通道。
select()方法返回的int表示准备好了多少个通道。 也就是说,自上次调用select()以来已经准备好了多少个通道。 如果您调用select()并且它返回1,因为一个通道已准备好,并且您再次调用select(),并且另一个通道已准备好,它将再次返回1。 如果你没有对第一个准备好的通道做任何事情,你现在有2个就绪通道,但每个select()调用之间只有一个频道准备就绪。
selectedKeys()
一旦调用了其中一个select()方法并且其返回值指示一个或多个通道已就绪,您可以通过调用选择器selectedKeys()方法,通过SelectionKey访问就绪通道。 就像这样:
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(); //important!!!
}
wakeUp()
已调用被阻塞的select()方法的线程可以脱离阻塞,即使尚未准备好任何通道。 这是通过让一个不同的线程调用Selector上的Selector.wakeup()方法来完成的,
如果另一个线程调用wakeup()并且当前在select()中没有阻塞线程,则调用select()的下一个线程将立即“唤醒”。
close()
使用完选择器后,调用其close()方法。 这将关闭Selector并使使用此Selector注册的所有SelectionKey实例无效。 Channel本身并不会关闭。
Full Selector Example
这是一个完整的例子,它打开一个Selector,用它注册一个通道(通道实例化被省略),并继续监视Selector以获得四个事件(接受,连接,读,写)的“准备就绪”。
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();
}
}