`
airu
  • 浏览: 267470 次
  • 性别: Icon_minigender_1
  • 来自: 云南
社区版块
存档分类
最新评论

Java-NIO-Selector

 
阅读更多
Selector 的出现,大大改善了多个 Java Socket的效率。在没有NIO的时候,轮询多个socket是通过read阻塞来完成,即使是非阻塞模式,我们在轮询socket是否就绪的时候依然需要使用系统调用。而Selector的出现,把就绪选择交给了操作系统(我们熟知的selec函数),把就绪判断和读取数据分开,不仅性能上大有改善,而且使得代码上更加清晰。

Java NIO的选择器部分,实际上有三个重要的类。
1,Selector 选择器,完成主要的选择功能。select(), 并保存有注册到他上面的通道集合。
2,SelectableChannel 可被注册到Selector上的通道。
3,SelectionKey 描述一个Selector和SelectableChannel的关系。并保存有通道所关心的操作。

接下来,便是一个通用的流程。
首先, 创建选择器,
然后,注册通道,
其次,选择就绪通道,
最后,处理已就绪通道数据。

让我们通过代码来看这些步骤是如何完成的。

Selector selector = Selector.open();

channel1.configureBlocking(false);
channel2.configureBlocking(false);
cahnnel3.configureBlocking(false);

SelectionKey key1 = channel1.register(selector, SelectionKey.OP_READ);
SelectionKey key2 = channel2.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);
SelectionKey key3 = channel3.register(selector, SelectionKey.OP_WRITE);

while(true){
  
 int readyCount = selector.select(1000);
 if( readyCount == 0) continue;
 Iterator<SelectionKey> iter = selector.selectedKeys.iterator();
 while(iter.hasNext()){
      SelectionKey key = iter.next();
      if( key.isReadable()){
           readData(key);
      }
      iter.remove();
    }
}



上面的代码是一个示例。我们可以看到,创建一个Selector使用open方法,这是一个静态工厂模式,注意他的异常处理是IOException。接下来的通道,我们并没有说明是什么通道,一般来说,基本上Socket类通道是可选择的,但是文件类的是不可选择的。
我们可以看到的是,这个通道调用了 configureBlocking(false)这样的方法,在注册到Selector上之前,通道应该保证是非阻塞的,否则异常IllegalBlockingModeException抛出。
之后我们开始注册通道,使用registor方法,主意后面一个参数,如果对一个只读的通道注册写操作,是会抛出异常IllegalArgumentException的。例如SocketChannel不支持accept操作。这里一共有四种操作 read,write,accept,connect。
当然,我们还不能把已经关闭的通道注册到Selector中,而Selector如果调用close,那么试图访问它的大多数操作都会抛出异常。

接下来,我们开始使用select函数更新selectedKey,这里比较复杂,但是从代码看,我们做完select以后,就开始便利selectedKey,找到符合要求的key,进行读数据操作。这里还要注意的是,使用完key以后,需要从selectedKey集合中删除。

下面我们还有更详细的说明,因为我们还不知道这个select到底做了说明,selectedKey又是如何更新的呢?

首先,一个selectionKey 包含了两个集合,一个是 注册的感兴趣的操作集合,一个是已经准备好的集合。第一个集合基本上是注册就确定的,或者通过interestOps(int)来改变。select是不会改变interest集合的。但是select改变的是 ready集合。也就是准备好的感兴趣的操作的集合,这样说,也说明,ready集合实际上是interest集合的子集。

如何使用这些集合呢?
看代码:
if (( key.readyOps() & SelectionKey.OP_READ) != 0)
{
   myBuffer.clear();
   key.channel().read(myBuffer);
   doSomething(myBuffer.flip());

}


从上面的代码看出,这个集合只是一个掩码,需要和操作与,才能得到结果。
当然,也有更方便的用法。

if ( key.isReadable() )


还要注意的是,这样的判断并不是就是一定的,只是一个提示。底层通道随时在改变。

对于SelectionKey, 还可以执行cancel操作,一个被cancel掉的SelectionKey,实际上只是被放到了Selector的cancel键集合里,键马上失效,但是通道依然是注册状态,要等到下一个select时才真正取消注册。

现在,我们再来看看选择器做了什么。选择器是就绪选择的核心,它包含了注册到它上面的通道与操作关系的Key,它维护了三个集合。
1,已经注册的键集合 调用, keys()
2,已经选择的键集合 调用, selectedKeys()
3,已经取消的键集合 私有。

选择器虽然封装了select,poll等底层的系统调用,但是她有自己的一套来管理这些键。
每当select被调用时,她做如下检查:
1,检查已经取消的键的集合。如果非空,从其他两个集合中移除已经取消的键,注销相关通道,清空已经取消的键的集合。
2,已注册的键的集合中的键的interest集合被检查。例如有新的interest的操作注册。但是这一步不会影响后面的操作。这是延时到下一次select调用时才会影响的。
就绪条件确认后,底层系统进行查询。依赖于select方法的参数,如果没有通道准备好,根select带的参数超时设置,可能会阻塞线程。
系统调用完成后,可以对操作系统指示的已经准备好的interest集合中的一种操作的通道,执行以下操作:
a: 如果通道的键还没有在已经选择的键的集合中,那么键的ready集合将被清空。然后表示操作系统发现的当前通道已经准备好的操作的比特掩码将被设置。
b: 否则,一旦通道的键被放入已经选择的键的集合中时,ready集合不会被清除,而是累积。这就是说,如果之前的状态是ready的操作,本次已经不是ready了,但是他的bit位依然表示是ready,不会被清除。
3, 步骤2可能会有很长一段时间的休眠。所以在步骤2完成以后,步骤1继续执行以确保被取消的键正确处理。
4,返回值,select的返回值说明的是从上一次调用到本次调用,就绪选择的个数。如果上一次就已经是就绪的,那么本次不统计。这是是为何返回为0时,我们continue的原因。

这里使用的延迟注销方法,正是为了解决注销键的问题。如果线程在取消键的同时进行通道注销,那么很可能阻塞并与正在进行的选择操作发生冲突。

同样我们有3中select可以选择:
1, select()
2, select(long timeout)
3, selectNow();
select()会阻塞线程知道又一个通道就绪。
而select带timeout的会在特定时间内阻塞,或者至少有一个通道就绪。
而selectNow()如果没有发现就绪,就直接返回。

如何停止中断选择呢?
有三种方法。
1, wakeup()这是一种优雅的方法,同时也是延时的。如果当前没有正进行的选择操作,也就是要等到下一个select才起作用。
2, close()选择器的close被调用,则所有在选择操作中阻塞的线程被唤醒,相关通道被注销,键也被取消。
3, interrupt() 实际上interrupt并不会中断线程。而是设置线程中断标志。
然后依然是调用wakeup()。这是因为 Selector 捕获了interruptedException,然后在异常处理中调用了 wakeup()

根据以上的信息,我们可以了解到,实际上选择器对选择键中的集合的操作,是交给程序员来完成的。如何管理选择键,是很关键的。

这里需要记住的是,ready集合中的比特位,是累积的。根据步骤2,如果一个键是在选择集合中,那么这个键的ready集合是不会被清除的。而如果这个键不在选择集合中,那么就要首先清空这个键的ready集合,然后把就绪信息更新到这个ready集合上,最后,就是把这个键加入到已选择的集合中。

这也是为什么上面的流程中,我们为什么要把处理的键删除,因为如果不删除,下一次的信息是累积的,我们就不能分出本次select中那些操作就绪了。如果清除掉,那么下一次如果就绪,ready集合就是重置后更新的信息。


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics