Java NIO Selector

Java NIO 选择器,原文地址:http://tutorials.jenkov.com/java-nio/selectors.html

选择器是 Java NIO 中的一个模块,它可以管理一个或多个 NIO 管道,并且决定哪一个管道可以进行读写操作。这种方式可以让一个线程管理多个管道或者说多个网络连接。

为什么使用选择器

使用一个线程去处理多个管道的优势在于你仅仅需要很少数量的线程就可以处理很多的管道。实际上,你可以只用一个线程去处理你所有的管道。操作系统在线程间切换是非常耗时的,并且每个线程都有占有一定的操作系统资源(内存)。因此,你的程序使用的线程越少越好。
考虑到现在操作系统和 CPU 在运行多任务方面已经变得越来越好了,因此在多线程间的切换所花销的时间变得越来越小。实际上,现在的 CPU 有多个核心,你也许会觉得不使用多线程可能浪费 CPU 资源。但是无论如何,我们不会在此讨论这些问题。这里我们优先讨论的是你可以使用选择器让一个线程去管理多个管道。
下面的插图展示了使用选择器后让一个线程管理 3 个管道:
Java NIO: A Thread users a Selector to handle 3 Channel's

创建一个选择器

你可以像下面写的一样通过调用 Selector.open() 方法来创建一个选择器:

1
Selector selector = Selector.open();

在管道上注册选择器

为了把管道交给选择器管理,你必须在管道上注册选择器。管道上注册选择器需要调用 SelectableChannel.register() 方法,正如下面所写的一样:

1
2
3
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

交给选择器管理的管道必须使用非阻塞模式。这也就意味着你不能把文件管道交给选择器管理,因为文件管道不能切换成非阻塞模式。而套接字管道则可以非常便利的交给选择器管理。
请注意 register() 方法的第二个参数。它是“感兴趣设置”,即管道想要选择器监听的事件。下面是可以监听的事件的四种类型:

  1. 连接
  2. 接受
  3. 可读
  4. 可写

一个管道第一个事件也叫做“准备好”。因此一个管道已经成功连接了其他的服务器就是指“连接准备好”。一个服务器端套接字管道接受了一个到来的连接就是“接受”。一个管道中已经有数据可读了就是“可读”状态。同样的,一个管道可以接受写入的数据时就是“可写”状态。
SelectionKey 对象的四个常量代表了这四种状态:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

如果你想监听多个事件或者所有事件,可以像下面这么写:

1
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

下面我将继续讲解这些事件类型。

SelectionKey’s

正如你在前面看到的,当你在一个管道上通过 channel.register(selector, SelectionKey.OP_READ); 方法注册了选择器后,该方法将会返回一个 SelectionKey 对象。这个 SelectionKey 对象包含如下几个令人感兴趣的属性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object(optional)

下面我会逐个讲解这些属性。

Interest Set

正如在上面小节“Registering Channels with the Selector”所说的,感兴趣集合表示你感兴趣的事件。你可以通过 Selectionkey 对这些感兴趣集合进行读写,就如下面这样:

1
2
3
4
5
6
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;

你可以使用给定的 SelectionKey 的常量和感兴趣的集合进行位与操作来判断感兴趣集合是否包含某个特定的事件。

Ready Set

“就绪集合”是指管道已经准备好可以进行的一系列操作的集合。在注册了选择器后,你才可以访问其“就绪集合”。选择器我们下一章才会讲到。你可以像下面这样访问“就绪集合”:

1
int readySet = selectionKey.readyOps();

你可以像测试感兴趣集合那样测试就绪集合来看看管道上那些操作已经在就绪状态。但是,你需要使用下面四种方法代替,这四种方法都返回一个布尔值:

1
2
3
4
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

管道+选择器

SelectionKey 中访问管道和选择器是非常简单的。下面是访问的例子:

1
2
3
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();

附加的对象

你可以在 SelectionKey 上附加一个对象或者为管道附加更详细的信息,这在识别给定的管道时是非常方便的。例如,你也许会附加一个与管道配合使用的缓冲区对象,或者聚合着更多数据的一个其他对象。下面展示了怎么添加一个额外的对象:

1
2
3
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

你也可以在给管道注册选择器时附加一个对象,正如下面这样:

1
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

通过选择器选择管道

一旦你在一个选择器上注册了一个或者多个管道之后,你就可以调用选择器的 select() 方法来获取特定条件的管道了。这些方法会返回你关注的事件在就绪状态的管道。换句话说,如果你关注的是处于可读状态的管道,那么通过调用 select() 方法你就会得到可读状态的管道。
下面是几种 select() 方法:

  • int select()
  • int select(long timeout)
  • int selectNow()

select() 会阻塞线程直到可以找到一个你关注的事件在就绪状态的管道。
select(long timeout) 的作用和 select() 相同,只是它阻塞线程的时间是有限的,即其参数 timeout
selectNow() 方法并不会阻塞线程。无论是否有符合条件的管道,它都会立即返回(PS:这句话看得也不太懂。。。)。

三种 select() 方法返回的 int 类型的值表示有多少个管道在就绪状态。这也就是说,自从你上次调用 select() 方法之后,有多少个管道变成了就绪状态。如果你调用了 select() 方法,并且因为只有一个管道在就绪状态所以它返回了 1,然后你再次调用 select() 方法,并且另外一个管道也变成了就绪状态,那么第二次调用 select() 方法还是返回 1。如果你没有使用第一个已经就绪的管道,那么现在有两个管道在就绪状态,但是每次调用 select() 的时候只有一个管道变成了就绪状态。

selectedKeys()

一旦你调用了一种 select() 方法,那么它就会返回目前在就绪状态的管道的数量,你可以通过“selected key set”来访问在就绪状态的管道,即调用选择器的 selectedKeys() 方法。正如下面这样:

1
Set<SelectionKey> selectedKeys = selector.selectedKeys();

当你调用 Channelregister() 方法为一个管道注册了选择器后,它会返回一个 SelectionKey 对象。这个对象代表着你在选择器上注册的管道。你可以通过 selectedKeySet() 方法来访问这个对象。
你可以遍历这个已选择的键集合来访问就绪状态的管道。就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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() 方法。选择器自身并不会移出被选择的键。所以在我们处理完管道之后,我们需要自己溢出相应的键。下一次这个管道变为“就绪”状态时,选择器会再次将其加入被选择的键集合中。
SelectionKey.channel() 方法返回的管道应该可以转换为你需要的管道类型,例如:服务端套接字管道或者套接字管道等等。

wakeUp()

某个线程调用了 select() 方法阻塞了,即使没有管道在就绪状态,也有办法让其从 select() 方法返回。只要其他线程在第一个线程调用 select() 方法的那个对象上调用了 Selector.wakeup() 方法即可。阻塞在 select() 方法上的线程会立即返回。
如果其他线程调用了 wakeup() 方法,但当前没有线程阻塞在 select() 方法上,那么下个调用 select() 方法的线程会立即“醒来(wake up)”(PS:应该就是会立即返回吧)。

close()

你在用完选择器之后可以调用它的 close() 方法来关闭它。这个方法会关闭选择器和注册在选择器上的被选择的键集合。但是管道本身并不会关闭。

Full Selector Example

下面是一个完整的例子,演示了开启一个选择器,并为其注册一个管道(管道初始化没有考虑在内),然后监听选择器里对四种事件(可接受,连接,可读,可写)处于就绪状态的管道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
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();
}
}