第六节 netty前传-NIO Selector
Selector 选择器作为java NIO中三大重要的组件之一。它的作用能够检查一个或多个NIO通道channel,然后确定哪些通道可以用于符合我们定义的操作,例如读写、socket连接等。也是因为选择器的存在,使得我们可以使用单个线程来管理多个通道,大大提高了资源的利用
nio中selector中的优点
可以使用单个线程来处理多个channel来节省资源。对于操作系统而言,线程之间切换是昂贵的,并且每个线程也占用操作系统中的一些资源(存储器)。 因此,使用的线程越少越好。当然,现代操作系统和CPU在多任务处理中变得越来越好,多线程的开销也会变得越来越小。所以具体的使用还需要根据自己的实际需求和相应的硬件资源。
下面使用
- 创建selector选择器
使用Selector的open方法创建选择器
Selector selector = Selector.open();
- 通常selector和channel一起使用,所以将channel注册到selector中,如下面这段代码
//1 通道必须处于非阻塞模式才能与选择器一起使用
channel.configureBlocking(false);
//2 下面这个方法的第二个参数是我们监听通道的兴趣事件
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
- 注意: 上面中,因为selector的使用需要通道为非阻塞模式,所以类似阻塞模式的filechannel就不能使用了。第二个方法中的兴趣事件包含
Connect:表示准备连,使用 SelectionKey.OP_CONNECT表示
Accept:表示准备接受,使用 SelectionKey.OP_ACCEPT表示
Read:读事件,使用 SelectionKey.OP_READ表示
Write:写事件,使用 SelectionKey.OP_WRITE表示
当然如果想使用多个也可以使用类似下面作为参数即可:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
- 处理兴趣事件,我们注册到selector中的register方法返回一个SelectionKey ,这个SelectionKey对象包含一些很有用的属性:
我们注册的兴趣集
已经准备就绪兴趣事件
注册的channel
注册的选择器selector
附加对象(可选)
首先先介绍一下上面几种属性
- 兴趣集:
选择器中共注册兴趣事件,可以通过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;
- 已经准备就绪兴趣事件:
就绪兴趣事件集是channel准备好的一组操作。可以通过下面获取就绪集:
int readySet = selectionKey.readyOps();
不过这种方式需要同故宫上述兴趣事件来判断那些兴趣事件准备就绪。通常我们使用下面这种方式:
//selectionKey.isAcceptable();
//selectionKey.isConnectable();
//selectionKey.isReadable();
//selectionKey.isWritable();
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();
}
- channel和selector的获取方式如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
- 附加对象
我们可以将对象附加到SelectionKey,这样做可以很方便的识别给定通道。
或者将更多信息附加到通道上。 比如,将通道使用的buffer附加上。 以下是如何附加对象:
//附加对象到selectionKey上
selectionKey.attach(theObject);
//从selectionKey种取得附加对象
Object attachedObj = selectionKey.attachment();
//可以在register()方法中使用Selector注册Channel时附加一个对象
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
- 通过选择器选择我们感兴趣得通道
在Selector中注册一个或多个通道后,可以调用selector.select()方法。 这个方法返回感兴趣的事件(连接,接受,读取或写入)已准备好的通道得数量。源码 javadoc描述:这是一个阻塞方法,直到有事件准备好了,返回准备好的事件数量。
/**
* Selects a set of keys whose corresponding channels are ready for I/O
* operations.
*
* <p> This method performs a blocking <a href="#selop">selection
* operation</a>. It returns only after at least one channel is selected,
* this selector's {@link #wakeup wakeup} method is invoked, or the current
* thread is interrupted, whichever comes first. </p>
*
* @return The number of keys, possibly zero,
* whose ready-operation sets were updated
* @throws IOException
* If an I/O error occurs
* @throws ClosedSelectorException
* If this selector is closed
*/
public abstract int select() throws IOException;
当一个线程调用select()方法时,会被阻塞到有准备事件到来,但是如果我们想释放这个线程,可以通过在另外的线程中调用selector.wakeup()方法。这里的这个选择器对象应该是同一个。
- 通过select()方法并且其返回值指示一个或多个通道已就绪,可以通过调用选择器selectedKeys()方法,取得之前已经注册得兴趣事件,在取判断这些兴趣事件,哪些事准备就绪状态。
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处理后,需要手动调用remove()删除。 当下一次channel变为“就绪”状态时,selector会再次将其添加到SelectionKey中去。
给出一个完整的例子,包含打开一个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<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();
}
}