您的位置:首页 > 其它

NIO:Selector 详解

2012-12-30 09:06 260 查看
示例程序TCPEchoServerSelector中展示了Selector的基本用法。在此,我们将对其进行更加详细的介绍。

Selector: 创建和关闭

static Selector open()

boolean isOpen()

void close()

调用Selector的open()工厂方法可以创建一个选择器实例。选择器的状态是"打开"或"关闭"的。创建时选择器的状态是打开的,并保持该状态,直到调用close()方法通知系统其务已经完成。可以调用isOpen()方法来检查选择器是否已经关闭。

5.6.1在信道中注册

我们已经知道,每个选择器都有一组与之关联的信道,选择器对这些信道上"感兴趣的"I/O操作进行监听。Selector与Channel之间的关联由一个SelectionKey实例表示。(注意一个信道可以注册多个Selector实例,因此可以有多个关联的SelectionKey实例)SelectionKey维护了一个信道上感兴趣的操作类型信息,并将这些信息存放在一个int型的位(bitmap)中,该int型数据的每一位都有相应的含义。

SelectionKey类中的常量定义了信道上可能感兴趣的操作类型,每个这种常量都是只有一位设置为1的位掩码(bitmask)(见第3.1.3节)

SelectionKey: 兴趣操作集

static int OP_ACCEPT

static int OP_CONNECT

static int OP_READ

static int OP_WRITE

int interestOps()

SelectionKey interestOps(int ops)

通过对OP_ ACCEPT,OP_CONNECT,OP_READ以及OP_WRITE中适当的常量进行按位OR,我们可以构造一个位向量来指定一组操作。例如,一个包含读和写的操作集由表达式(OP_READ
| OP_WRITE)来指定。不带参数的interestOps()方法将返回一个int型位图,该位图中设置为1的每一位都指示了信道上需要监听的一种操作。另一个方法以一个位图为参数,指示了应该监听信道上的哪些操作。重点提示:任何对key(信道)所关联的兴趣操作集的改变,都只在下次调用了select()方法后才会生效。

SocketChannel, Server SocketChannel:注册Selector

SelectionKey register(Selector sel, int ops)

SelectionKey register(Selector sel, int ops, Object

attachment)

int validOps()

boolean isRegistered()

SelectionKey keyFor(Selector sel)

调用信道的register()方法可以将一个选择器注册到该信道。在注册过程中,通过存储在int型数据中的位图来指定该信道上的初始兴趣操作集(见上文的"SelectionKey:兴趣操作集")。register()方法将返回一个代表了信道和给定选择器之间的关联的SelectionKey实例。validOps()方法用于返回一个指示了该信道上的有效I/O操作集的位图。对于ServerSocketChannel来说,accept是惟一的有效操作,而对于SocketChannel来说,有效操作包括读、写和连接。对于DatagramChannel,只有读写操作是有效的。一个信道可能只与一个选择器注册一次,因此后续对register()方法的调用只是简单地更新该key所关联的兴趣操作集。使用isRegistered()方法可以检查信道是否已经注册了选择器。keyFor()方法与第一次调用register()方法返回的是同一个SelectionKey实例,除非该信道没有注册给定的选择器。

以下代码注册了一个信道,支持读和写操作:

SelectionKey key = clientChannel.register(selector,

SelectionKey.OP_READ | SelectionKey.OP_WRITE);

图5.1展示了一个选择器,其键集中包含了7个代表注册信道的键:两个在端口4000和4001上的服务器信道,以及从服务器信道创建的5个客户端信道:

SelectionKey: 获取和取消

Selector selector()

SelectableChannel channel()

void cancel()

键关联的Selector实例和Channel实例可以分别使用该键的selector()和channel()方法获得。cancel()方法用于(永久性地)注销该键,并将其放入选择器的注销集(canceled
set)中(图5.1)。在下一次调用select()方法时,这些键将从该选择器的所有键集中移除,其关联的信道也将不再被监听(除非它又重新注册)。



(点击查看大图)图5.1:Selector与其关联的键集

Selected Key Set: 选择键集; Cancelled Key Set:注销键集;
Key Set:键集;Interest Sets:

兴趣操作集

5.6.2选取和识别准备就绪的信道

在信道上注册了选择器,并由关联的键指定了感兴趣的I/O操作集后,我们就只需要坐下来等待I/O了。这要使用选择器来完成。

Selector: 等待信道准备就绪

int select()

int select(long timeout)

int selectNow()

Selector wakeup()

select()方法用于从已经注册的信道中返回在感兴趣的I/O操作集上准备就绪的信道总数。(例如,兴趣操作集中包含OP_READ的信道有数据可读,或包含OP_ACCEPT的信道有连接请求待接受。)以上三个select方法的惟一区别在于它们的阻塞行为。无参数的select方法会阻塞等待,直到至少有一个注册信道中有感兴趣的操作准备就绪,或有别的线程调用了该选择器的wakeup()方法(这种情况下select方法将返回0)。以超时时长作为参数的select方法也会阻塞等待,直到至少有一个信道准备就绪,或等待时间超过了指定的毫秒数(正数),或者有另一个线程调用其wakeup()方法。selectNow()方法是一个非阻塞版本:它总是立即返回,如果没有信道准备就绪,则返回0。wakeup()方法可以使当前阻塞(也就是说在另一个线程中阻塞)的任何一种select方法立即返回;如果当前没有select方法阻塞,下一次调用这三种方法的任何一个都将立即返回。

选择之后,我们需要知道哪些信道准备好了特定的I/O操作。每个选择器都维护了一个已选键集(selected-key
set),与这些键关联的信道都有即将发生的特定I/O操作。通过调用selectedKeys()方法可以访问已选键集,该方法返回一组SelectionKey。我们可以在这组键上进行迭代,分别处理等待在每个键关联的信道上的I/O操作。

Iterator<SelectionKey> keyIter =

selector.selectedKeys().iterator();

while (keyIter.hasNext()) {

SelectionKey key = keyIter.next();

// ...Handle I/O for key's channel...

keyIter.remove();

}

图5.1中的选择器的已选键集中有两个键:K2和K5。

Selector: 获取键集

Set<SelectionKey> keys()

Set<SelectionKey> selectedKeys()

以上方法返回选择器的不同键集。keys()方法返回当前已注册的所有键。返回的键集是不可修改的:任何对其进行直接修改的尝试(如,调用其remove()方法)都将抛出UnsupportedOperationException异常。selectedKeys()方法用于返回上次调用select()方法时,被"选中"的已准备好进行I/O操作的键。重要提示:selectedKeys()方法返回的键集是可修改的,实际上在两次调用select()方法之间,都必须"手工"将其清空。换句话说,select方法只会在已有的所选键集上添加键,它们不会创建新的键集。

所选键集指示了哪些信道当前可以进行I/O操作。对于选中的每个信道,我们需要知道它们各自准备好的特定I/O操作。除了兴趣操作集外,每个键还维护了一个即将进行的I/O操作集,称为就绪操作集(ready
set)。

SelectionKey: 查找就绪的I/O操作

int readyOps()

boolean isAcceptable()

boolean isConnectable()

boolean isReadable()

boolean isValid()

boolean isWritable()

对于给定的键,可以使用readyOps()方法或其他指示方法来确定兴趣集中的哪些I/O操作可以执行。readyOps()方法以位图的形式返回所有准备就绪的操作集。其他方法用于分别检查各种操作是否可用。

例如,查看键关联的信道上是否有正在等待的读操作,可以使用以下代码:

(key.readyOps() & SelectionKey.OP_READ) != 0或key.isReadable()

选择器的已选键集中的键,以及每个键中准备就绪的操作,都是由 select()方法来确定的。随着时间的推进,这些信息可能会过时。其他线程可能会处理准备就绪的I/O操作。同时,键也不是永远存在的。当其关联的信道或选择器关闭时,键也将失效。通过调用其cancel()方法可以显示地将键设置为无效。调用其isValid()方法可以检测一个键的有效性。无效的键将添加到选择器的注销键集中,并在下次调用任一种形式的 select()方法或 close()方法时从键集中移除。(当然,从键集中移除键意味着与它关联的信道也不再受监听。)

5.6.3信道附件

当一个信道准备好进行I/O操作时,通常还需要额外的信息来处理请求。例如,在前面的回显协议中,当客户端信道准备好写操作时,就需要有数据可写。当然,我们所需要的可写数据是由之前同一信道上的读操作收集的,但是在其可写之前,这些数据存放在什么地方呢?另一个例子是第3章中的成帧过程。如果一个消息一次传来了多个字节,我们需要保存已接收的部分消息,直到完整个消息接收完成。这两种情况都需要维护每个信道的状态信息。然而,我们非常幸运!SelectionKey通过使用附件使保存每个信道的状态变得容易。

SelectionKey: 查找准备就绪的I/O操作

Object attach(Object ob)

Object attachment()

每个键可以有一个附件,数据类型只能是Object类。附件可以在信道第一次调用register()方法时与之关联,或者后来再使用 attach()方法直接添加到键上。通过 SelectionKey 的attachment()方法可以访问键的附件。

5.6.4 Selector小结

总的来说,使用Selector的步骤如下:

I.创建一个Selector实例。

II.将其注册到各种信道,指定每个信道上感兴趣的I/O操作。

III.重复执行:

1.调用一种select方法。

2.获取选取的键列表。

3.对于已选键集中的每个键,

a.获取信道,并从键中获取附件(如果合适的话)

b.确定准备就绪的操作并执行。如果是accept操作,将接受的信道设置为非阻塞模式,

并将其与选择器注册。

c.如果需要,修改键的兴趣操作集

d.从已选键集中移除键

如果选择器告诉了你什么时候I/O操作准备就绪,你还需要非阻塞I/O吗?答案是肯定的。信道在已选键集中的键并不能确保非阻塞I/O,因为调用了select()方法后,键集信息可能会过时。另外,阻塞式写操作会阻塞等待直到写完所有的字节,而就绪集中的OP_WRITE仅表示至少有一个字节可写。实际上,只有非阻塞模式的信道才能与选择器进行注册:如果信道在阻塞模式,SelectableChannel类的register()方法将抛出IllegalBlockingModeException异常。

相关下载:


Java_TCPIP_Socket编程(doc)

http://download.csdn.net/detail/undoner/4940239

文献来源:

LSOFT.CN(琅软中国)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: