学习NIO——Selector
现在我们看看关于NIO三大组件之一的Selector究竟做了些什么?简单来说,Selector就是“维护“另外一个组件Channel的。从Channel注册到最终注销整个生命周期将由Selector间接管理。
那么上述说到的”维护“和间接究竟是什么意思呢?我们先来看看Selector本身的一些属性——
维护三个集合
Selector维护有三个集合,他们分别如下——
Key Set
:所有注册到Selector中的SelectionKey集合Selected Key
:有事件发生的SelectionKey集合Cancelled Key
:被取消SelectionKey集合
注意:以上的
Selected Key
和Cancelled Key
均为Key Set
的子集。
接下来我们从Channel的”生命周期“来研究Selector的作用
Channel的“生命周期”
1. 注册
一个Channel想要使用NIO的体系,就必须注册到Selector中进行管理。Channel通过调用SelectableChannel#register(Selector sel, int ops)
进行注册,后面的参数表明此Channel感兴趣的I/O操作类型。
① Channel注册的时候会生成一个与之相关联的SelectionKey对象,此对象将会后续被Selector进行管理和维护。SelectionKey中含有Channel和他感兴趣I/O操作类型的信息,因此管理SelectionKey就相当于是管理了Channel,这也就是我们上文中提及的”间接“管理的含义了。
② 生成的SelectionKey将会被添加到上述Selector维护的三个集合中的第一个集合Key Set
中。
至此注册的过程就已经完成。
2. 通信
这个过程是I/O中最重要的一环,就是通过这个Channel和客户端进行交流。
Selector通过调用Selector#select()
或者类似方法获取已经准备好进行I/O操作(连接、接受连接、读、写)的Channel,注意此时选出的Channel是依照注册时设置的感兴趣IO事件获取的已经准备好进行I/O操作的(由底层操作系统完成)。
根据以上的体系,我们知道已经备好I/O操作Channel将会被Selector纳管到
Selected Key
这个集合中。我们需要首先判断Channel对应的SelectionKey是否已经存在于Selected Key
中,然后再进行下一步的操作。
- 如果Channel对应的SelectionKey不存在于
Selected Key
集合中,首先将Channel对应的SelectionKey添加到Selected Key
;第二步:先清除SelectionKey中ready-operation内容,然后向其中新增本次Channel的I/O操作类型;(具体的新增方法我们等会提及) - 如果Channel对应的SelectionKey已经存在于
Selected Key
集合。那么只需要更新SelectionKey中ready-operation的I/O操作类型即可。
如果key set
中所有的SelectionKey都没有感兴趣的IO事件,那么Selected key
和ready-operation将都不会更新
SelectinoKey其中其实也维护了一个”集合“,说是集合,但实际上是通过一个整数来存储的。这个集合就是
ready set
也就是上面的ready-operation,用于存放Channel中已经准备好的I/O类型。在更新的时候就是将新的就绪事件和ready set
做或运算得到新的ready set
。
接下来看看SelectionKey是怎样维护I/O操作类型的。我们看看SelectionKey中对于事件是这样定义的——
public static final int OP_READ = 1 << 0; // 0000 0001
public static final int OP_WRITE = 1 << 2; // 0000 0100
public static final int OP_CONNECT = 1 << 3; // 0000 1000
public static final int OP_ACCEPT = 1 << 4; // 0001 0000
我们直接看可能一时无法理解,但是我在后面给大家写出了他们的二进制,我们观察到这些不同的I/O类型的二进制中只有一个1,并且这个1的位置也不同,基于这样的特征,对于一个ready set
我们可以判断该数字的某一位是否为1进而判断是否有相应的I/O类型已经准备好。
在二进制运算中通过ready set
和IO类型做”与“运算(具体逻辑大家可以画一画),如果结果恰好等于IO类型对应的数值;那么就说明原始的ready set
中IO类型那一位为1。所以我们可以看看SelectionKey中判断是否有对应事件的实现过程——
// 判断读事件是否就绪的实现逻辑,其他的类型一样
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0; // readyOps()就是获取ready set的一个方法
}
通过以上的过程就将I/O就绪的Channel选出来了。
注意:在从
selected key
中取出对应的Channel进行消费之后,还需要手动将SelectionKey从selected key
中移除。因为消费之后不会自动从selected key
这个集合中删除。
2. 注销
当Channel被关闭的时候,因为Selector并不是直接维护Channel,因此还需要一些后续的收尾工作要完成。首先就是Channel对应的SelectionKey将会被放入到Selector维护的Cancelled key
中,等待下一次Selector调用select的时候对这个集合进行清理。