首页 > 代码库 > 选择器,可选择通道和选择键类
选择器,可选择通道和选择键类
选择器,可选择通道和选择键类
现在,您也许还对这些用于就绪选择的Java成员感到困惑。让我们来区分这些活动的零件并了解它们是如何交互的吧。图4-1的UML图使得情形看起来比真实的情况更为复杂了。看看图4-2,然后您会发现实际上只有三个有关的类API,用于执行就绪选择:
选择器(Selector)
选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。
可选择通道(SelectableChannel)
这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。FileChannel对象不是可选择的,因为它们没有继承SelectableChannel(见图4-2)。所有socket通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以指定对那个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
选择键(SelectionKey)
选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register( ) 返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
图 4-1. 就绪选择相关类的继承关系图
让我们看看SelectableChannel的相关API方法
public abstract class SelectableChannel extends AbstractChannel implements Channel { // This is a partial API listing public abstract SelectionKey // register (Selector sel, int ops) throws // ClosedChannelException; public abstract SelectionKey // register (Selector sel, int ops, Object att) throws // ClosedChannelException; public abstract boolean // isRegistered( ); public abstract SelectionKey keyFor(Selector sel); public abstract int validOps(); public abstract void configureBlocking(boolean block) throws IOException; public abstract boolean isBlocking(); public abstract Object blockingLock();}
非阻塞特性与多元执行特性的关系是十分密切的——以至于java.nio的架构将两者的API放到了一个类中。
我们已经探讨了如何用上面列出的SelecableChannel的最后三个方法来配置并检查通道的阻塞模式 。通道在被注册到一个选择器上之前,必须先设置为非阻塞模式(通过调用configureBlocking(false))。
图 4-2. 就绪选择相关类的关系
调用可选择通道的register( )方法会将它注册到一个选择器上。如果您试图注册一个处于阻塞状态的通道,register( )将抛出未检查的IllegalBlockingModeException异常。此外,通道一旦被注册,就不能回到阻塞状态。试图这么做的话,将在调用configureBlocking( )方法时将抛出IllegalBlockingModeException异常。
并且,理所当然地,试图注册一个已经关闭的SelectableChannel实例的话,也将抛出ClosedChannelException异常,就像方法原型指示的那样。
在我们进一步了解register( )和SelectableChannel的其他方法之前,让我们先了解一下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(); public abstract int select() throws IOException; public abstract int select(long timeout) throws IOException; public abstract int selectNow() throws IOException; public abstract void wakeup(); public abstract Set keys(); public abstract Set selectedKeys();}
尽管SelectableChannel类上定义了register( )方法,还是应该将通道注册到选择器上,而不是另一种方式。选择器维护了一个需要监控的通道的集合。一个给定的通道可以被注册到多于一个的选择器上,而且不需要知道它被注册了那个Selector对象上。将register( )放在SelectableChannel上而不是Selector上,这种做法看起来有点随意。它将返回一个封装了两个对象的关系的选择键对象。重要的是要记住选择器对象控制了被注册到它之上的通道的选择过程。
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 interestOps (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( ) }
选择器才是提供管理功能的对象,而不是可选择通道对象。选择器对象对注册到它之上的通道执行就绪选择,并管理选择键。
对于键的interest(感兴趣的操作)集合和ready(已经准备好的操作)集合的解释是和特定的通道相关的。每个通道的实现,将定义它自己的选择键类。在register( )方法中构造它并将它传递给所提供的选择器对象。
在下面的章节里,我们将了解关于这三个类的方法的更多细节。
4.1.2 建立选择器
现在您可能仍然感到困惑,您在前面的三个清单中看到了大量的方法,但无法分辨出它们具体做什么,或者它们代表了什么意思。在钻研所有这一切的细节之前,让我们看看一个经典的应用实例。它可以帮助我们将所有东西放到一个特定的上下文中去理解。
为了建立监控三个Socket通道的选择器,您需要做像这样的事情(参见图4-2):
Selector selector = Selector.open(); channel1.register(selector, SelectionKey.OP_READ); channel2.register(selector, SelectionKey.OP_WRITE); channel3.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); // Wait up to 10 seconds for a channel to become ready readyCount = selector.select(10000);
这些代码创建了一个新的选择器,然后将这三个(已经存在的)socket通道注册到选择器上,而且感兴趣的操作各不相同。select( )方法在将线程置于睡眠状态,直到这些刚兴趣的事情中的操作中的一个发生或者10秒钟的时间过去。
现在让我们看看Selector的API的细节:
public abstract class Selector { // This is a partial API listing public static Selector open( ) throws IOException public abstract boolean isOpen( ); public abstract void close( ) throws IOException; public abstract SelectionProvider provider( ); }
Selector对象是通过调用静态工厂方法open( )来实例化的。选择器不是像通道或流(stream)那样的基本I/O对象:数据从来没有通过它们进行传递。类方法open( )向SPI发出请求,通过默认的SelectorProvider对象获取一个新的实例。通过调用一个自定义的SelectorProvider对象的openSelector( )方法来创建一个Selector实例也是可行的。您可以通过调用provider( )方法来决定由哪个SelectorProvider对象来创建给定的Selector实例。
大多数情况下,您不需要关心SPI;只需要调用open( )方法来创建新的Selector对象。在那些您必须处理它们的罕见的情况下,您可以参考在附录B中总结的通道的SPI包。
继续关于将Select作为I/O对象进行处理的话题的探讨:当您不再使用它时,需要调用close( )方法来释放它可能占用的资源并将所有相关的选择键设置为无效。一旦一个选择器被关闭,试图调用它的大多数方法都将导致ClosedSelectorException。注意ClosedSelectorException是一个非检查(运行时的)错误。您可以通过isOpen( )方法来测试一个选择器是否处于被打开的状态。
我们将结束对Selector 的API的探讨,但现在先让我们看看如何将通道注册到选择器上。下面是一个之前章节中出现过的SelectableChannel 的API的简化版本:
public abstract class SelectableChannel extends AbstractChannel implements Channel { // This is a partial API listing public abstract SelectionKey register(Selector sel, int ops) throws ClosedChannelException; public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException; public abstract boolean isRegistered( ); public abstract SelectionKey keyFor(Selector sel); public abstract int validOps( ); }
就像之前提到的那样,register( )方法位于SelectableChannel类,尽管通道实际上是被注册到选择器上的。您可以看到register( )方法接受一个Selector对象作为参数,以及一个名为ops的整数参数。第二个参数表示所关心的通道操作。这是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码。特定的操作比特值在SelectonKey类中被定义为public static字段。
在JDK 1.4中,有四种被定义的可选择操作:读(read),写(write),连接(connect)和接受(accept)。
并非所有的操作都在所有的可选择通道上被支持。例如,SocketChannel不支持accept。试图注册不支持的操作将导致IllegalArgumentException。您可以通过调用validOps( )方法来获取特定的通道所支持的操作集合。我们可以在第三章中探讨的socket通道类中看到这些方法。
选择器包含了注册到它们之上的通道的集合。在任意给定的时间里,对于一个给定的选择器和一个给定的通道而言,只有一种注册关系是有效的。但是,将一个通道注册到多于一个的选择器上允许的。这么做的话,在更新interest集合为指定的值的同时,将返回与之前相同的选择键。实际上,后续的注册都只是简单地将与之前的注册关系相关的键进行更新.
一个例外的情形是当您试图将一个通道注册到一个相关的键已经被取消的选择器上,而通道仍然处于被注册的状态的时候。通道不会在键被取消的时候立即注销。直到下一次操作发生为止,它们仍然会处于被注册的状态(见4.3小节)。在这种情况下,未检查的CancelledKeyException将会被抛出。请务必在键可能被取消的情况下检查SelectionKey对象的状态。
在之前的清单中,您可能已经注意到了register( )的第二个版本,这个版本接受object参数。这是一个方便的方法,可以传递您提供的对象引用,在调用新生成的选择键的attach( )方法时会将这个对象引用返回给您。我们将会在下一节更进一步地了解SelectionKey的API。
一个单独的通道对象可以被注册到多个选择器上。可以调用isRegistered( )方法来检查一个通道是否被注册到任何一个选择器上。这个方法没有提供关于通道被注册到哪个选择器上的信息,而只能知道它至少被注册到了一个选择器上。此外,在一个键被取消之后,直到通道被注销为止,可能有时间上的延迟。这个方法只是一个提示,而不是确切的答案。
任何一个通道和选择器的注册关系都被封装在一个SelectionKey对象中。keyFor( )方法将返回与该通道和指定的选择器相关的键。如果通道被注册到指定的选择器上,那么相关的键将被返回。如果它们之间没有注册关系,那么将返回null。
4.2 使用选择键
让我们看看SelectionKey类的API:
package java.nio.channels; 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 interestOps (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( )方法返回与该键相关的SelectableChannel对象,而selector( )则返回相关的Selector对象。这没有什么令人惊奇的。
键对象表示了一种特定的注册关系。当应该终结这种关系的时候,可以调用SelectionKey对象的cancel( )方法。可以通过调用isValid( )方法来检查它是否仍然表示一种有效的关系。当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效(参见4.3节)。当再次调用select( )方法时(或者一个正在进行的select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销,而新的SelectionKey将被返回。
当通道关闭时,所有相关的键会自动取消(记住,一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。一旦键被无效化,调用它的与选择相关的方法就将抛出CancelledKeyException。
一个SelectionKey对象包含两个以整数形式进行编码的比特掩码:一个用于指示那些通道/选择器组合体所关心的操作(instrest集合),另一个表示通道准备好要执行的操作(ready集合)。当前的interest集合可以通过调用键对象的interestOps( )方法来获取。最初,这应该是通道被注册时传进来的值。这个interset集合永远不会被选择器改变,但您可以通过调用interestOps( )方法并传入一个新的比特掩码参数来改变它。interest集合也可以通过将通道注册到选择器上来改变(实际上使用一种迂回的方式调用interestOps( )),就像4.1.2小节中描的那样。当相关的Selector上的select( )操作正在进行时改变键的interest集合,不会影响那个正在进行的选择操作。所有更改将会在select( )的下一个调用中体现出来。
可以通过调用键的readyOps( )方法来获取相关的通道的已经就绪的操作。ready集合是interest集合的子集,并且表示了interest集合中从上次调用select( )以来已经就绪的那些操作。例如,下面的代码测试了与键关联的通道是否就绪。如果就绪,就将数据读取出来,写入一个缓冲区,并将它送到一个consumer(消费者)方法中。
if ((key.readyOps( ) & SelectionKey.OP_READ) != 0) { myBuffer.clear( ); key.channel( ).read (myBuffer); doSomethingWithBuffer (myBuffer.flip( ));}
就像之前提到过的那样,有四个通道操作可以被用于测试就绪状态。您可以像上面的代码那样,通过测试比特掩码来检查这些状态,但SelectionKey类定义了四个便于使用的布尔方法来为您测试这些比特值:isReadable( ),isWritable( ),isConnectable( ), 和isAcceptable( )。每一个方法都与使用特定掩码来测试readyOps( )方法的结果的效果相同。例如:
if (key.isWritable( ))
等价于:
if ((key.readyOps( ) & SelectionKey.OP_WRITE) != 0)
这四个方法在任意一个SelectionKey对象上都能安全地调用。不能在一个通道上注册一个它不支持的操作,这种操作也永远不会出现在ready集合中。调用一个不支持的操作将总是返回false,因为这种操作在该通道上永远不会准备好。需要注意的是,通过相关的选择键的readyOps( )方法返回的就绪状态指示只是一个提示,不是保证。底层的通道在任何时候都会不断改变。其他线程可能在通道上执行操作并影响它的就绪状态。同时,操作系统的特点也总是需要考虑的。
SelectionKey对象包含的ready集合与最近一次选择器对所注册的通道所作的检查相同。而每个单独的通道的就绪状态会同时改变。
您可能会从SelectionKey的API中注意到尽管有获取ready集合的方法,但没有重新设置那个集合的成员方法。事实上,您不能直接改变键的ready集合。在下一节里,也就是描述选择过程时,我们将会看到选择器和键是如何进行交互,以提供实时更新的就绪指示的。
让我们试验一下SelectionKey的API中剩下的两个方法:
public abstract class SelectionKey { // This is a partial API listing public final Object attach (Object ob) public final Object attachment( ) }
选择器,可选择通道和选择键类