一、为什么使用selector?
传统的IO网络编程是,一个客户端连接到服务器端,服务器就起一个线程去维护与这个连接。对服务器开销非常大。而NIO引入的Selector模式解决了一个客户端一个线程的问题;选择器提供了一种机制,用于监视一个或多个NIO通道,并识别何时可以使用一个或多个NIO通道进行数据传输。这样,一个线程可以用于管理多个通道,从而管理多个网络连接。线程之间的上下文切换对于操作系统来说非常昂贵,而且每个线程都占用内存。
二、如何使用?
我们可以用selector对象注册多个通道。当I/O活动发生在任何通道上时,选择器通知我们。这就是我们如何从单个线程读取大量数据源的方法。我们向选择器注册的任何通道都必须是SelectableChannel的子类。这是一种特殊类型的通道,可以放在非阻塞模式中。所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。而Socket channel可以正常使用。
三、Selector方法解析
Selector源码API如下:(解析核心的方法意义)
(1)、open():创建一个通道
通过系统默认的要给选择器提供者(provider)或者自定义的privider创建一个新的选择器(selector)。
Selector selector = Selector.open();
注册可选择的通道:
为了让选择器监视任何通道,我们必须向选择器注册这些通道。我们通过调用可选通道的register方法来实现这一点。但是在通道注册到选择器之前,它必须处于非阻塞模式:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
第一个参数是我们前面创建的Selector对象,第二个参数通过选择器在监控通道中侦听哪些事件。
我们可以侦听四个不同的事件,每个事件都由SelectionKey类中的常量表示:
Connect—当客户端试图连接到服务器时。SelectionKey.OP_CONNECT
Accept—服务器接受来自客户机的连接。SelectionKey.OP_ACCEPT。
Read—当服务器准备从通道读取数据时。SelectionKey.OP_READ。
Write—当服务器准备写入通道时。SelectionKey.OP_WRITE。
(2)、select 方法
在刚初始化的Selector对象中,这三个集合都是空的。 通过Selector的select()方法可以选择已经准备就绪的通道 。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。
int select():阻塞到至少有一个通道在你注册的事件上就绪了。
int select(long timeout):和select()一样,最长阻塞时间为timeout毫秒。
int selectNow():非阻塞,只要有通道就绪就立刻返回。
注:返回的键的数量,可能是0,已更新的就绪操作集数量。select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。
(3)、wakeup():唤醒在select()方法中阻塞的线程.
通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回 该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回。
(4)、close():
通过close()方法关闭Selector, 该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭.
(5)、访问已选择键集合:
Set selectedKeys=selector.selectedKeys();
四、SelectionKeys方法解析
一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系.
(0)、分析注册成功返回值SelectionKeys:
key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel的时候指定。
key.channel(); // 返回该SelectionKey对应的channel。
key.selector(); // 返回该SelectionKey对应的Selector。 key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask key.readyOps(); // 返回一个bit mask,代表在相应channel上可以进行的IO操作。
(1)、判断要给事件是否被监视:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
(2)、从SelectionKey对象访问被监视的通道非常简单。我们只调用通道方法:
Channel channel = key.channel();
、获取selector
Selector selector = key.selector();
(3)、attachment
我们可以将对象附加到SelectionKey。有时我们可能想给通道一个自定义ID,或者附加任何我们想要跟踪的Java对象。
附加对象是一种方便的方法。下面是如何从SelectionKey附加和获取对象:
key.attach(Object);
Object object = key.attachment();
或者,我们可以选择在通道注册期间附加一个对象。我们将它作为第三个参数添加到channel的register方法中,如下所示:
SelectionKey key = channel.register(
selector, SelectionKey.OP_ACCEPT, object);
将一个可选择的通道通过selectKey注册到这个selector上,一个selector维护着3种selectKey的集合方式。
a、Key-Set
所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。
b、已选择的键的集合(Selected key set)
Selector会定期向操作系统内核询问是否有事件要通知到SelectableChannel,会将那些对这些事件感兴趣的SelectableChannel当时注册到Selector的SelectionKey复制一份到此集合,要获得这些SelectionKey集合可以调用Selector.selectedKeys(),这个集合是Key-Set的子集。
c、已取消的键的集合(Cancelled key set)
这个集合里记录的是那些取消了对操作系统内核事件关注但并未取消对于Selector注册的SelectableChannel对应的SelectionKey的集合,这个集合不能直接访问,通常这个集合是Key-Set的子集。
注意: 当键被取消( 可以通过isValid( ) 方法来判断)时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用 select( ) 方法时(或者一个正在进行的select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销,而新的SelectionKey将被返回。当通道关闭时,所有相关的键会自动取消(记住,一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。一旦键被无效化,调用它的与选择相关的方法就将抛出CancelledKeyException。