Selector
是一个可以监控多个Channel
的Java NIO组件,它负责决定哪些Channel
可以进行读或写。这样一个线程就可以管理多个Channel
,从而管理多个网络连接。
Why Use a Selector?
单个线程管理多个Channel
的好处就是你只需要少量的线程就可以处理所有的Channel
。事实上,你可以只用一个线程来管理所有的Channel
。对操作系统来说,线程间的切换是比较耗资源的,而且线程本身也也会占用一些资源(内存)。因此,线程越少越好。
记住,现代操作系统和CPU在多任务处理方面已经表现得越来越好。因此,随着时间的推移,多线程的开销会越来越小。事实上,如果一个CPU有多个核,而你又不执行多线程任务,实际上是对CPU计算能力的一种浪费。不管怎样,怎样设计线程数已经超出了本文讨论的范畴。这里只需说明,你可以通过Selector
使用一个线程处理多个Channel
。
以下是通过Selector
使用一个线程处理三个Channel
的图示:

Creating a Selector
你可以像下面这样通过调用Selector.open()
方法来创建一个Selector
:
Selector selector = Selector.open();
Registering Channels with the Selector
为了使用带有Channel
的Selector
,你必须将Channel
注册进Selector
。注册操作可以通过调用SelectableChannel.register()
方法来实现,如下所示:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Channel
必须处于非阻塞模式,才能与Selector
一起使用。意味着你不能将FileChannel
和Selector
一起使用,因为FileChannel
不能切换到非阻塞模式。但SocketChannel
可以和Selector
一起很好的使用。
注意register()
方法的第二个参数。它是一个"interest set",意思是那些你感兴趣的发生在Channel
上的事件。一共有以下四种事件你可以监听:
- Connect
- Accept
- Read
- Write
Channel
触发事件也意味着它为该事件做好了准备。因此,一个Channel
成功连接到一个远程服务就是"connect"就绪。一个ServerSocketChannel
接收到一个连入连接就是accpet
就绪。一个Channel
准备好数据等待被读就是"read"就绪。一个Channel
准备好等待你的数据写入就是"write"就绪。
这四个事件由以下四个SelectionKey
的常量表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果你对一个以上的事件感兴趣,用或(|
)将它们连起来,像这样:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey's
正如你在前一节看到的,当你调用register()
方法将Channel
注册进Selector
的时候,会返回一个SelectionKey
类型的对象。这个对象包含一些有趣的属性:
- interest set
- ready set
- Channel
- Selector
- attached object(可选)
Interest Set
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;
如你所见,你可以用与(&
)将interest set和SelectionKey
里的某个事件常量连接起来,用以判断该事件是否被包含在interest set里。
Ready Set
ready set就是Channel
准备就绪的操作集。一般来说,在一次select()
之后,你通常都需要访问ready set。至于select()
,我们后面再讲。你可以像这样访问ready set:
int readySet = selectionKey.readOps();
你可以像前一节判断interest set里是否包含某个特定事件一样的方法来判断某个特定的事件/操作是否已经就绪。但是,你可以用以下四个返回boolean
类型值的方法来代替:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Attaching Objects
你可以附加一个对象到SelectionKey
上,以便识别给定Channel
或将进一步信息附加到Channel
。例如,你可以附加一个正在和Channel
一起使用的Buffer
,或者一个包含更多聚合信息的对象。下面是如何附加对象的示例:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
你也可以在register()
方法中,在向Selector
注册Channel
时附加一个对象。就像下面这样:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
Selecting Channels via a Selector
当你向Selector
注册了一个或多个Channel
之后,你就可以调用select()
方法了。select()
方法会放回那些已经对你感兴趣的事件集(connect、accept、read或write)准备就绪的Channel
。换句话说,如果你对Channel
是否准备好读取数据,通过select()
方法你就会收到那些已经准备好读取数据的Channel
。
以下是所有的select()
方法:
- int select()
- int select(long timeout)
- selectNow()
select()
方法会阻塞至至少一个你刚兴趣的事件已经准备好。
select(long timeout)
方法和select()
一样,但是它最多阻塞timeout
毫秒。
selectNow()
不会阻塞,无论有没有Channel
准备好,它都会立即返回。
select
方法返回的int
代表到底有多少Channel
已经就绪。也就是说,有多少Channel
从你上次调用select()
方法后又变得准备就绪了。如果有一个Channel
准备好了,那么select
方法就回返回1,再次调用select
方法,又有一个Channel
准备好了,它将再次返回1。如果你对第一个就绪的Channel
啥也没干,现在你就又两个就绪的Channel
了,但在你两次调用select
方法之间只有一个Channel
就绪。
selectedKeys()
如果你调用了select()
方法,而它的返回值也指明了至少一个Channel
已经就绪,那么你就可以通过调用Selector
的selectedKeySet()
方法返回的"selected key set"来访问这些Channel
了。就像下面这样:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
当你向Selector
注册Channel
的时候,Channel.register()
方法会返回一个SelectionKey
对象。这个key就代表着Selector
里面的Channel
。你可以通过selectedKeys()
方法来访问这些key。
你可以遍历"selected key set"来访问就绪的Channel
。就像下面这样:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ServerSocketChannel接受到一个新的连接
} else if (key.isConnectable()) {
// 连接远程服务成功
} else if (key.isReadable()) {
// 读就绪
} else if (key.isWritable()) {
// 写就绪
}
keyIterator.remove();
}
这个循环遍历了"selected key set"里面的所有key。对于每一个key,我们都测试这个key对应的Channel
到底做好了什么准备。
注意每次遍历最后一句keyIterator.remove()
。Selector
不会自己把SelectionKey
实例从"selected key set"里面移除的。当你处理完Channel
后,必须手动移除。下一次Channel
准备就绪的时候,Selector
会再次把它加到"selected key set"里面。
SelectionKey.channel()
方法返回的Channel
需要强制转换成你想要的类型,比如:ServerSocketChannel
、SocketChannel
等等。
wakeUp()
调用select()
方法的线程会被阻塞,但它同样可以被唤醒,就算没有任何Channel
就绪。通过在另一个线程调用同一个Selector
的wakeup()
方法,可以唤醒正在调用这个Selector
的select()
方法的线程。正在被select()
方法阻塞的线程会立即返回。
如果一个不同的线程调用了wakeup()
方法,并且现在没有任何线程正在调用select()
方法,那么下一个调用select()
方法的线程会立即被唤醒。
close()
当使用完Selector
后,请调用close()
方法。这会关闭Selector
并使所有注册到该Selector
的SelectionKey
实例无效。但Channel
本身不会被关闭。
Full Selector Example
以下是关于打开一个Selector
,注册Channel
(不包括Channel
的初始化),并监控其四个事件(accpet、connect、read、write)的就绪的完整示例:
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_ACCEPT);
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()) {
// ServerSocketChannel接受到一个新的连接
} else if (key.isConnectable()) {
// 连接远程服务成功
} else if (key.isReadable()) {
// 读就绪
} else if (key.isWritable()) {
// 写就绪
}
keyIterator.remove();
}
}
说明
发现貌似有人在看这个系列文章了,有必要说明下,这个Java NIO系列来源于jenkov.com,本文只是翻译,希望大家千万不要误会,本文不是原创。原文地址:Java NIO。
网友评论