NIO学习笔记选择器Selector

选择器基础

 从最基础的层面来看,选择器提供了询问通道是否已经准备好执行每个 I/0 操作的能力。例如,我们需要了解一个 SocketChannel 对象是否还有更多的字节需要读取,或者我们需要知道ServerSocketChannel 是否有需要准备接受的连接。在与SelectableChannel 联合使用时,选择器提供了这种服务,但这里面有更多的事情需要去了解。就绪选择的真正价值在于潜在的大量的通道可以同时进行就绪状态的检查。调用者可以轻松地决定多个通道中的哪一个准备好要运行。有两种方式可以选择:被激发的线程可以处于休眠状态,直到一个或者多个注册到选择器的通道就绪,或者它也可以周期性地轮询选择器,看看从上次检查之后,是否有通道处于就绪状态。如果您考虑一下需要管理大量并发的连接的网络服务器(web server)的实现,就可以很容易地想到如何善加利用这些能力。乍一看,好像只要非阻塞模式就可以模拟就绪检查功能,但实际上还不够。非阻塞模式同时还会执行您请求的任务,或指出它无法执行这项任务。这与检查它是否能够执行某种类型的操作是不同的。举个例子,如果您试图执行非阻塞操作,并且也执行成功了,您将不仅仅发现 read( )是可以执行的,同时您也已经读入了一些数据。就下来您就需要处理这些数据了。效率上的要求使得您不能将检查就绪的代码和处理数据的代码分离开来,至少这么做会很复
杂。

 即使简单地询问每个通道是否已经就绪的方法是可行的,在您的代码或一个类库的包里的某些代码需要遍历每一个候选的通道并按顺序进行检查的时候,仍然是有问题的。这会使得在检查每个通道是否就绪时都至少进行一次系统调用,这种代价是十分昂贵的,但是主要的问题是,这种检查不是原子性的。列表中的一个通道都有可能在它被检查之后就绪,但直到下一次轮询为止,您并不会觉察到这种情况。最糟糕的是,您除了不断地遍历列表之外将别无选择。您无法在某个您感兴趣的通道就绪时得到通知。

 这就是为什么传统的监控多个 socket 的 Java 解决方案是为每个 socket 创建一个线程并使得线程可以在 read( )调用中阻塞,直到数据可用。这事实上将每个被阻塞的线程当作了 socket 监控器,并将 Java 虚拟机的线程调度当作了通知机制。这两者本来都不是为了这种目的而设计的。程序员和 Java 虚拟机都为管理所有这些线程的复杂性和性能损耗付出了代价,这在线程数量的增长失控时表现得更为突出。

 真正的就绪选择必须由操作系统来做。操作系统的一项最重要的功能就是处理 I/O 请求并通知各个线程它们的数据已经准备好了。选择器类提供了这种抽象,使得 Java 代码能够以可移 植的方式,请求底层的操作系统提供就绪选择服务。

选择器(Selector)

 选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。

可选择通道(SelectableChannel)

 这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。FileChannel 对象不是可选择的,因为它们没有继承SelectableChannel。所有 socket 通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。SelectableChannel 可以被注册到 Selector 对象上,同时可以指定对那个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

选择键(SelectionKey)

 选择键封装了特定的通道与特定的选择器的注册关系.选择键对象被SelectableChannel.register( ) 返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
image

图1

 看看 SelectableChannel 的相关 API 方法

public abstract class SelectableChannel
    extends AbstractChannel
    implements Channel
{
    //读操作的标志位
    public static final int OP_READ = 1 << 0;
    //写操作的标志位
    public static final int OP_WRITE = 1 << 2;
    //socket连接的标志位
    public static final int OP_CONNECT = 1 << 3;
    //接受socket连接的标志位
    public static final int OP_ACCEPT = 1 << 4;

    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放到了一个类中。调用可选择通道的 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( )
}
选择器才是提供管理功能的对象,而不是可选择通道对象。选择器对象对注 册到它之上的通道执行就绪选择,并管理选择键。

 一个键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关 系 。 可 以看到前两 个 方 法中 反 映 了这种关系 。channel( ) 方 法 返 回 与该 键 相 关的SelectableChannel 对象,而 selector( )则返回相关的 Selector 对象。对于键的 interest集合和 ready集合的解释是和特定的通道相关的。每个通道的实现,将定义它自己的选择键类。在 register( )方法中构造它并将它传递给所提供的选择器对象。

 键对象表示了一种特定的注册关系。当应该终结这种关系的时候,可以调用 SelectionKey对象的 cancel( )方法。可以通过调用 isValid( )方法来检查它是否仍然表示一种有效的关系。当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用 select( )方法时(或者一个正在进行的 select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销,而新的SelectionKey 将被返回。当通道关闭时,所有相关的键会自动取消(记住,一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化.一个 SelectionKey 对象包含两个以整数形式进行编码的比特掩码:一个用于指示那些通道/选择器组合体所关心的操作(instrest 集合),另一个表示通道准备好要执行的操作(ready 集合)。当前的 interest 集合可以通过调用键对象的 interestOps( )方法来获取。最初,这应该是通道被注册时传进来的值。这个 interset 集合永远不会被选择器改变,但您可以通过调用 interestOps( )方法并传入一个新的比特掩码参数来改变它。

使用选择器

选择过程

 选择器维护着注册过的通道的集合,并且这些注册关系中的任意一个都是封装在SelectionKey 对象中的。每一个 Selector 对象维护三个键的集合:

public abstract class Selector
{
    // This is a partial API listing
    public abstract Set keys( );
    public abstract Set selectedKeys( );
    public abstract int select( ) throws IOException;
    public abstract int select (long timeout) throws IOException;
    public abstract int selectNow( ) throws IOException;
    public abstract void wakeup( );
}
  • 已注册的键的集合(Registered key set)

 与选择器关联的已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys( )方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引 java.lang.UnsupportedOperationException。
- 已选择的键的集合(Selected key set)

 已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的 interest 集合中的操作。这个集合通过 selectedKeys( )方法返回(并有可能是空的)。不要将已选择的键的集合与 ready 集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的 ready 集合,指示了所关联的通道已经准备好的操作。键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。
- 已取消的键的集合(Cancelled key set)

 已注册的键的集合的子集,这个集合包含了 cancel( )方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。在一个刚初始化的 Selector 对象中,这三个集合都是空的。Selector 类的核心是选择过程。这个名词您已经在之前看过多次了——现在应该解释一下了。基本上来说,选择器是对 select( )、poll( )等本地调用(native call)或者类似的操作系统特定的系统调用的一个包装。但是 Selector 所作的不仅仅是简单地向本地代码传送参数。它对每个选择操作应用了特定的过程。对这个过程的理解是合理地管理键和它们所表示的状态信息的基础。

猜你喜欢

转载自blog.csdn.net/Pengjx2014/article/details/79134601