Selector是Java NIO中的一个组件,用于检查一个或者多个NIO Channel,并确定哪一个Channel已经准备好读或者写了。
这样一个进程能管理多个通道,也意味着多个网络连接。
优点
Selector的优势在于一个进程能够处理多个通道,这样可以用更少的进程来控制通道了。事实上,可以只用一个线程处理所有的通道。在操作系统中切换进程开销是很大的,并且每个线程也需要占用一定的系统资源。因此,线程的使用越少越好。
要记住,现代操作系统和CPU在多任务处理方面表现越来越好,所以多线程的开销也变得越来越小。事实上,如果CPU有多个核心,不使用多任务是浪费了CPU的能力。不管怎样,关于设计的讨论属于不同的文章了。在这里,只需要知道Selector能够用一个进程管理多个通道就可以了。
下面是用一个Selector处理三个Channel的示例图:
创建Selector
通过调用Selector.open方法,创建一个Selector:
Selector selector = Selector.open();
用Selector注册Channels
为了将Channel和Selector配合使用,需要用Selector注册Channel。通过Selector.register()方法来实现:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);
和Selector一起使用时,Channel必须属于非阻塞模式。这意味着FileChannel不能喝Selector一起使用,因为FileChannel不能切换为非阻塞模式。套接字(Socket)的通路可以。
注意register()方法的第二个参数,这是个“兴趣集合”,意思是通过Selector监听Channel时,对哪些事件感兴趣。有四种事件可以监听:
- 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对象。包括一些有趣的属性:
- interest集合
- ready集合
- Channel
- Selector
- 附加对象(可选)
下面讲述这些属性
Interest 集合
interest集合是在“Selecting”中感兴趣的事件集合。可以通过SelectionKey读写interest集合,例如:
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常量对interest集合进行与操作来确定事件是否在interest集合中。
Ready 集合
Ready集合是通道已经就绪的操作的集合。在selection之后,将首先访问这个集合。selection将会在下一小节解释。
可以这样访问ready集合:
int readySet = selectionKey.readyOps();
能用检测interest集合同样的方式检测通道中就绪的事件或操作。但是也可以使用下面四种方法,都返回布尔值:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel +Selector
从SelectionKey中访问Channel+selector很简单,如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
附加对象
能在SelectionKey上附加一个对象,用来识别给定的通道,或者附上Channel更多的信息。例如,可以附上和通道一起使用的Buffer,或者包含更多聚合对象的对象。使用方法如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
还可以在用register()方法向Selector注册Channel的时候附上一个对象:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
通过Selector选择通道
一旦向Selector注册了一个或多个通道,就能调用selector()方法中的一个。这些方法返回你感兴趣的事件(连接,接收,读,写)已经就绪的那些通道,换句话说,如果对读就绪的通道感兴趣,select()方法会返回准备好进行读取的通道。
以下是select()方法:
- int select()
- int select(long timeout)
- int selectNow()
select()方法会阻塞,直到至少有一个通道在你注册的事件上就绪了。
select(long timeout)和select()方法一样。除了它最多只阻塞timeout毫秒。
selectNow()完全不阻塞,无论通通道是否就绪都立即返回。
select()方法返回的int是有多少通道就绪了。也就是自上一次调用select()之后有多少通道就绪。如果调用select()返回1,就是说有一个通道就绪了,如果再次调用一个select()方法,又有一个通道就绪了,就会再次返回1。如果在第一个通道就绪之后没有调用,现在就有两个就绪的通道了。但是两次调用select()方法之间只有一个通道就绪。
selectedKeys()
一旦调用了一个select()方法,返回的值表明有一个或者多个通道就绪了,就能够调用选择器selectedKeys()方法通过“选择的key集合”访问就绪的集合:
Set selectedKeys = selector.selectedKeys();
用Selector注册通路时,Channels.register()方法会返回一个SelectionKey对象。这个键代表用该Selector注册的证书。也就SelectionKey中通过selectedKeySet()访问的键。
对选择的键集合进行迭代来访问就绪的通道,例如:
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
if(key.isAccepted()) {
// 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实例。需要再处理完通道之后显式移除。下次通道就绪后Selector会把它重新加入键集合中。
需要把SelectionKey.channel()方法返回的通道转换为要处理的类型,例如ServerSocketChannel或者SocketChannel等。
wakeUp()
一个线程调用select()方法阻塞后,即使没有通道就绪,也能从select()方法返回。只要让其他线程在第一个调用select()方法的线程上的对象上调用Selector.wake()方法。在select()方法上等待的线程会立即返回。
如果另外的线程调用wakeup()但是没有线程阻塞在select()方法,下一个调用select()方法的线程会立即“wake up”。
完整的Selector范例
下面是一个打开Selector,用它注册通道(省略通道实例),然后监视Selector的四个事件(接受,连接,读,写)进行监听。
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 selectedKeys = selector.selectedKeys();
Iterator 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();
}
}