选择器
最后,我们探索一下选择器。由于选择器内容比较多,所以本篇先偏理论地讲一下,后一篇讲代码,文章也没有什么概括、总结的,写到哪儿算哪儿了,只求能将选择器写明白,并且将一些相对重要的内容加粗标红。
选择器提供选择执行已经就绪的任务的能力,这使得多元I/O成为了可能,就绪执行和多元选择使得单线程能够有效地同时管理多个I/O通道。
某种程序上来说,理解选择器比理解缓冲区和通道类更困难一些和复杂一些,因为涉及了三个主要的类,它们都会同时参与到这整个过程中,这里先将选择器的执行分解为几条细节:
1、创建一个或者多个可选择的通道
2、将这些创建的通道注册到选择器对象中
3、选择键会记住开发者关心的通道,它们也会追踪对应的通道是否已经就绪
4、开发者调用一个选择器对象的select()方法时,相关的键会被更新,用来检查所有被注册到该选择器的通道
5、获取一个键的集合,从而找到当时已经就绪的通道,通过遍历这些键,开发者可以选择出每个从上次调用select()开始直到现在已经就绪的通道
对于选择器的理解,大致就是这么几步,OK,接下去再进一步,看一下和选择器相关的三个类。
选择器、可选择通道和选择键类
选择器(Selector)
选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态,当这么做的时候,可以选择将被激发的线程挂起直到有就绪的通道。
可选择通道(SelectableChannel)
这个抽象类提供了实现通道的可选择性所需要的公共方法,它是所有支持就绪检查的通道类的父类,FileChannel对象不是可选择的,因为它们没有继承SelectableChannel,所有Socket通道都是可选择的,包括从管道(Pipe)对象中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以设定对哪个选择器而言哪种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
选择键(SelectionKey)
选择键封装了特定的通道与特定的选择器的注册关系。选择键对象呗SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
用一张UML图来描述一下选择器、可选择通道和选择键:
建立选择器
前面讲了,选择器的作用是管理了被注册的通道集合和它们的就绪状态,假设我们有三个Socket通道的选择器,可能会有类似的代码:
... Selector selector = Selector.open(); channel1.register(selector, SelectionKey.OP_READ); channel2.register(selector, SelectionKey.OP_WRITE); channel3.register(selector, SelectionKey.OP_READ | OP_WRITE); channel4.register(selector, SelectionKey.OP_READ | OP_ACCEPT); ready = selector.select(10000); ...
这种操作用图表示就是:
代码创建了一个新的选择器,然后将这四个(已经存在的)Socket通道注册到选择器上,而且感兴趣的操作各不相同。select()方法在将线程置于睡眠状态直到这些感兴趣的事件中的一个发生或者10秒钟过去,这就是所谓的事件驱动。
再稍微看一下Selector的API细节:
public abstract class Selector { ... public static Selector open() throws IOException; public abstract boolean isOpen(); public abstract void close() throws IOException; public abstract SelectionProvider provider(); ... }
Selector是通过调用静态工厂方法open()来实例化的,这个从前面的代码里面也看到了,选择器不是像通道或流那样的基本I/O对象----数据从来没有通过他们进行传递。
通道是调用register方法注册到选择器上的,从代码里面可以看到register()方法接受一个Selector对象作为参数,以及一个名为ops的整数型参数,第二个参数表示关心的通道操作。在JDK1.4中,有四种被定义的可选择操作:读(read)、写(write)、连接(connect)和接受(accept)。
注意并非所有的操作都在所有的可选择通道上被支持,例如SocketChannel就不支持accept。
使用选择键
接下来看看选择键,选择键的API大致如下:
public abstract class SelectionKey { public static final int OP_READ; public static final int OP_WRITE; public static final int OP_CONNECT; public static final int OP_ACCEPT; public abstract SelectableChannel channel(); public abstract Selector selector(); public abstract void cancel(); public abstract boolean isValid(); public abstract int interestOps(); public abstract void iterestOps(int ops); public abstract int readyOps(); public final boolean isReadable(); public final boolean isWritable(); public final boolean isConnectable(); public final boolean isAcceptable(); public final Object attach(Object ob); public final Object attachment(); }
就像前面提到的,一个键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系,channel()方法和selector()方法反映了这种关系。
开发者可以使用cancel()方法终结这种关系,可以使用isValid()方法来检查这种有效的关系是否仍然存在,可以使用readyOps()方法来获取相关的通道已经就绪的操作。不过我们往往不需要使用readyOps()方法,SelectionKey类定义了四个便于使用的布尔方法来为开发者测试通道的就绪状态:
if (key.isWritable())
这种写法就等价于:
if ((key.readyOps() & SelectionKeys.OPWRITE) != 0)
isWritable()、isReadable()、isConnectable()、isAcceptable()四个方法在任意一个SelectionKey对象上都能安全地调用。