一、为什么要使用selector
Selector类可用于避免使用非阻塞式客户端中浪费资源忙等方法,考虑一个即时消息服务器,可能有上千个客户端连接到了服务器,在任何时刻都只有少量的消息需要读取和分发,这需要一种方法阻塞等待,直到至少有一个信道可以进行i/o操作。
一个selector实例可以同时检查一组信道的i/o状态,选择器就是一个多路开关选择器,一个选择器能够选择多组信道的i/o操作使用选择器,需要创建一个selector实例,并将其注册到想要监控的信道上(通过信道的方法实现),调用选择器的select方法,改方法会阻塞等待,直到有一个或更多的信道准备好了i/o操作。
在一个单独的线程中,通过调用select方法能检查多个信道是否准备好进行i/o操作,和可进行i/o操作的信道的数量,没有返回0
二、selector的创建和关闭
static Selector open();
boolean isOpen()
void close();
调用Selector的open工厂方法可以创建一个选择器实例,选择器的状态是打开或者关闭的,创建选择器的状态是打开的,并保存该状态,直到调用close方法关闭,isOpen方法检查选择器是否关闭
三、将selector注册到信道channel上
每个选择器都有一组关联的信道,选择器对这些信道上感兴趣的i/o操作进行监听。
Selector和channel的关联,可以视作是桥梁,由一个selectionkey实例表示。
(一个selector可以注册到多个channel上,因而一个selector可以有多个selectionkey)
Selectionkey维护了一个信道上感兴趣的操纵类型信息,并将这些信息存放在一个int的位图中(bitmap),该int型数据的每一位都有相应的意义
SelectionKey类中的常量定义了信道上可能感兴趣的操作类型(称之为key的兴趣集),每个这种常量都是只有一位设置为1的位掩码
表示如下:SelectionKey的兴趣操作集
OP_READ:信道数据可读 OP_WRITE:可写
通过对OP_ACCEPT,OP_CONNECT,OP_READ,OP_WRITE中常量进行按位OR,构造出一个位向量指定一组操作
例如:一个读写操作的兴趣集表示为: OP_READ|OP_WRITE
InterestOps() :返回一个int型的位图,该位图中设置为1的每一位都指示了信道上需要监听的一种操作。
(例如OP_READ,OP_WRITE.....)
interestOps(int ops) 一个位图为参数,指定了要监听信道上的那些操作。
需要注意的是,任何对key兴趣集的改变,都在下次调用select方法时候生效
SocketChannel,ServerSocketChannel:注册selector
SelectionKey register(selector, int ops);
ops:指的是key的兴趣集
SelectionKey register(selector,int ops,Object attachment);
ops:指的是兴趣集,attachment指的是key携带的附件对象
int Validops():返回的是信道的兴趣集
IsRegistered方法检查信道是否注册了选择器
Keyfor方法与第一次调用register方法返回的是同一个selectionKey实例,除非信道没有注册给定的选择器。
通过register将选择器注册到信道上,注册过程中存储的int类型数据,指定了信道的初始兴趣集
对于,ServerSocketChannel来说, accept是唯一有效的操作,而socketChannel,有效操作包括读,写,和连接。
一个信道可能只会注册一个选择器,一个信道对同一个selector的register操作,是对信道兴趣集的修改
下面注册一个信道,支持读写操作。
SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);
键selectionKey的获取和取消
Selector selector()
SelectableChannel channel()
Void cancel()
键selectionKey关联的选择器和信道channel的实例,可以通过selector(),channel()获取,
selectionKey的cancel将键注销,放入到选择器的注销集中,并在下次调用select方法的时候从键集中移除,同时这个key关联的信道也不会再被监听。
这个图,直观的看到,键集(key set)中有 选择键集(selected key set),和注销键集(cancelled key set)
interset sets表示兴趣集(write,read,accept,connect)
选取和识别就绪的信道
在信道注册选择器后,并标识了信道的兴趣操作集,选择器那端,只要等待有合适的i/o操作,就可以操作信道
int select()
int select(long timeout)
int selectNow()
Selector wakeup()
无参的select方法,返回已注册信道中准备就绪的信道总数,和其他select方法的区别是:
1.无参数的select方法会阻塞等待,直到至少有一个注册信道中兴趣级的操作准备就绪,
或有别的线程调用了该选择器的wakeup方法(这种情况下select返回0)
2. 有参数的select方法会阻塞等待,直到至少有一个信道准备就绪,或等待时间超过了指定的毫秒数(正数),或者有另一个线程调用其 wakeup()方法
3. selectNow方法是一个非阻塞版本,总是立即返回,如果没有信道准备就绪,则返回0
4. wakeup方法使得当前阻塞的任何一种select方法立即返回;如果当前没有select方法阻塞,下一次调用这三种方法的任何一个都将立即返回。
选取键集
Keys() :返回所有注册的所有键,返回的键都是不可修改的selectedKeys():返回上次调用select方法时,被选中的已准备好进行I/O操作的键。,返回的键是可以修改的。
Iterator<SelectionKey> iterator = selector.keys().iterator();
while(iterator.hasNext()){
SelectionKey next = iterator.next();
if(next.isValid()){
//do i/o ....
}
}
查找可用的信道
SelectionKey:查找就绪的I/O操作
Int readOps() :以位图的形式返回所有准备就绪的操作集
//检查各种操作是否可用
Boolean isAcceptable()
Boolean isConnectable()
Boolean isReadable()
Boolean isWritable()
例如:查看键关联的信道上是否有正在等待的读操作
if((next.readyOps()&SelectionKey.OP_READ)!=0){} 或者使用isReadable()
Note:
选择器中的已选键集中的键,以及每个键中准备就绪的操作,都是由select()方法确定的。
随着时间的推进,这些信息可能会过时,其他线程可能会处理准备就绪的I/O操作,同时,键也不是永远存在的。当其关联的信道或选择器关闭时候,键也将失效。
调用cancel也可以将键设置为无效,isvalid方法可以检测一个键的有效性,无效的键将添加到选择器的注销键集中,并在下次调用select方法或close方法时候,从键集中移除。
信道附件
当一个信道准备好进行I/O操作时候,通常还需要额外的信息来处理请求。
例如,在回显协议中,当客户端信道准备好写操作时,就需要有数据可写。当然,我们所需要的可写数据是由之前同一个信道上的读操作收集的,但是在其可写之前,这些数据存放在位置
如果一个消息一次传来了多个字节,需要保存已接收的部分消息,知道整个消息接收完成,这两种情况都需要维护每个信道的状态信息。
Selectionkey使用附件保存每个信道的状态。
SelectionKey; 查找准备就绪的I/O操作
Attach(object ob): 每个键可以有一个附件,数据类型只能是object类型,附件可以在信道第一次调用register方法时候与之关联,或者后来使用attach方法直接添加到键上。
Attachment(): 直接访问键的附件。