Java NIO Selector

为什么使用Selector

使用一个线程来处理多个Channels的优势在于你只需要使用少量的线程就能处理大量的Channels。实际上,你可以只用一个线程来处理所有的Channels。线程切换对于操作系统来说代价昂贵,而且每个线程也会占用操作系统的一些资源(比如内存),因此你使用的线程数量越少越好。

但请记住,现代操作系统和CPU在多任务处理方面已经变的越来越强,所以随着时间的推移,在多线程方面的开销变的越来越小。实际上,如果一个CPU有多个核心,但不进行多任务处理可能是对CPU处理能力的浪费。不过关于多线程设计方面的讨论属于另外的内容,我们这里要说的是,如何使用Selector实现一个线程管理多个Channels。


Java NIO:一个线程使用Selector处理3个Channel


创建一个Selector

你可以通过调用来创建一个Selector,比如:
Selector selector = Selector.open();

注册Channels到Selector中

为了使用Selector来处理Channel,你必须先将Channel注册到Selector中,可以通过调用SelectableChannel.register() 来完成:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
和Selector一起使用的Channel必须是非阻塞模式的,也就意味着你不能将FileChannel和Selector一起使用,因为FileChannel不能设置为非阻塞模式。Socket Channel可以设置为非阻塞模式。
注意register()方法的第二个参数,这是一个“intrest”集合,这个参数表示你对通过Selector监听的Channel上的哪些事件感兴趣,一共有四种事件可以监听:
1、Connect
2、Accept
3、Read
4、Write
一个Channel触发一个事件也就意味着这个事件“已就绪”,所以一个Channel成功连接到一个服务器端就是“connect”就绪,服务器端接受一个连接请求就是“accept”就绪,可以从一个Channel读数据就是“read”就绪,可以向一个Channel写数据就是“write”就绪。
通过SelectionKey中的常量来描述一下四种事件:
1、SelectionKey.OP_CONNECT
2、SelectionKey.OP_ACCEPT
3、SelectionKey.OP_READ
4、SelectionKey.OP_WRITE
如果你对多个事件感兴趣,可以像下面这样设置:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 

关于SelectionKey

正如你在前面的小节中看到的那样,当你使用register()方法将Channel注册到Selector时,会返回一个SelectionKey对象,这个SelectionKey对象包含以下属性:
1、interest事件集合
2、ready事件集合
3、Channel
4、Selector
5、附加对象(可选)


下面描述一下这些属性的含义

interest集合
interest集合是一系列你感兴趣的事件,你可以通过SelectionKey对interest集合进行读或者写,比如:
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;  
如上所示,你可以使用&操作符来确定你关心的事件是否在interest集合中。

Ready集合
ready集合是Channel上已经就绪的一系列操作。在处理就绪的channel时你将首先访问ready集合。关于处理就绪的channel将在后面的小节中介绍。你可以像这样访问ready集合:
int readySet = selectionKey.readyOps();
你可以像测试interest集合一样测试ready集合来确定Channel上的哪个事件/操作已经就绪。你也可以使用以下四种方式来代替,返回值都是boolean类型
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel + Selector
从SelectionKey 访问 channel和selector是比较简单的,如下:
Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector(); 

附加对象
用最简单的方式来识别一个channel就是向SelectionKey附加一个对象,也可以附加更多信息到channel中。例如:你可能附加一个和channel一起使用的Buffer,或者一个包含更多数据的对象。
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
你也可以在注册时在register()中附加对象
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

通过Selector选择要处理的Channels

一旦向Selector中注册了一个或多个Channels,就可以调用select()方法。这个方法返回那些已就绪事件(connect, accept, read or write)对应的Channels。换句话说,如果你对“读就绪”的Channel感兴趣,通过select()方法就可以获得“读就绪”对应的通道。
select()方法如下:
int select()
int select(long timeout)
int selectNow()

select()方法会阻塞,直到至少有一个channel的注册事件已就绪。
select(long timeout)和select()一样,但阻塞时间最大为timeout 毫秒。
selectNow()不会阻塞,不管有没有channel就绪,都立刻返回。
select()返回的int值表示有多少channels已就绪。这个值只表示你最后一次调用select()方法时已就绪的Channel数量。如果你调用select()的返回值是1表明只有一个channel已就绪,如果你再次调用select(),又有一个channel就绪,那返回值还是1。如果你对第一次调用就已经就绪的channel未做任何处理,那现在就是两个已就绪的channel,只是在两次调用之间仅有一个channel变成已就绪状态。

selectedKeys()
当你调用select()方法后,它的返回值指明现在有多少个channel已就绪,你可以通过调用selectedKeys()访问已就绪的channel。比如:

Set<SelectionKey> selectedKeys = selector.selectedKeys();    
使用Channel.register()方法注册channel到selector时会返回一个SelectionKey对象。这个对象代表channel在selector中的注册。通过调用Selector的selectedKeys()也可以访问这个对象。
可以通过迭代SelectedKey集合来访问已就绪的Channel。像下面那样:
Set<SelectionKey> selectedKeys = selector.selectedKeys();

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}
遍历selectedKey集合,以确定每个SelectionKey对应Channel上的就绪事件。
注意在循环尾部的keyIterator.remove()调用。Selector不会自己移除SelectionKey实例,当处理完channel时必须手动移除。下次channel就绪的时候Selector会再次将对应的SelectionKey加入到SelectionKey集合中。
调用SelectionKey.channel()方法返回的Channel实例需要通过类型转换转为实际的Channel实例,比如ServerSocketChannel或SocketChannel 等。

wakeUp()方法

如果一个线程调用select()后处于阻塞状态,即使没有channel处于就绪状态,线程也可以退出select()方法。在其它线程上调用Selector.wakeup()后,在select()方法上阻塞的线程会立刻从select()方法中返回。如果一个线程调用wakeup()时没有任何线程在select()方法中阻塞,那么下一个调用select()的线程会立刻被"wake up"。

close()方法

Selector使用完毕要调用它的close()方法,这样会关闭Selector并作废Selector中的所有SelectionKey,但channel本身不会被关闭。

完整示例

以下是一个完整示例,包括打开一个Selector,注册channel到selector(省略channel实例部分),监视Selector上的四个事件(accept, connect, read, write)的就绪状态。
Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);


while(true) {

  int readyChannels = selector.select();

  if(readyChannels == 0) continue;


  Set<SelectionKey> selectedKeys = selector.selectedKeys();

  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}

原文地址:http://tutorials.jenkov.com/java-nio/selectors.html
关于NIO其它章节教程:http://my.oschina.net/leejun2005/blog/136680
阅读更多
个人分类: Java
上一篇HTTP协议详解
下一篇Java 6 JVM参数选项大全(中文版)
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭