NIO:Selector 详解

来源:互联网 发布:加油站收入 知乎 编辑:程序博客网 时间:2024/05/16 16:20

示例程序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) != 0key.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异常。
 
 
0 0
原创粉丝点击