7 Java NIO Selector-翻译

296 阅读6分钟

Selector是一个Java NIO中能够检测一到多个Channel,并且决定哪个通道已经准备好读或写。通过这种方式可以使用一个线程管理多个通道,因此可以用来管理多个网络连接。

为什么要使用Selector

使用一个线程来管理多个通道的优势是你只需要更少的线程来管理通道。事实上,你可以只使用一个线程来管理所有的通道。对操作系统来说,线程间的切换的开销是很大的,并且每一个线程都需要消耗操作系统的资源(内存)。因此,线程的使用是越少越好。

但是,需要记住,现在的操作系统和CPU在多任务处理方面变得越来越好。所以多线程的开销随着时间的推移变得越来越小。事实上,如果一个CPU是多核的,如果不进行多任务处理,反而在浪费CPU的能力。不管怎么说,关于那种设计的讨论应该放在另一篇的文章里。在这里,只要知道,通过Selector,只需要一个线程来管理多个通道就可以了。

下图是通过Selector使用一个线程来处理三个Channel的例子。

image

创建Selector

创建一个Selector只需要通过Selector.open()方法即可,像这样:

Selector selector = Selector.open();

向通道注册Selector

为了让Channel使用Selector,必须让Channel注册Selector。这可以通过SelectableChannel.register()方法来实现,像这样:

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

当使用Selector时,channel必须是非阻塞模式。这意味着FileChannel不能使用Selector,因为FileChannel是阻塞的。Socket Channel都可以。

注意register方法的第二个参数,这是一个“interest set”,意思是在Selector监听Channel时对哪些事件感兴趣。共有 四种不同的事件类型可以监听。

  • Connect
  • Accept
  • Read
  • Write

一个通道触发一个事件意味着通道已经准备好这个事件了。因此,一个通道成功连接到另一台服务器是一个“Connect ready”事件,一个服务器接受新进来的连接是一个“accept"事件。一个通道已经准备好数据是一个”ready“事件。一个能将准备写数据是一个”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;    

下面还会对interest set作进一步简单介绍。

Interest Set

如“向通道注册选择器”一节中那样,Interest Set是你所选择的感兴趣的集合。可以通过SelectionKey来读写Interest Set,像这样:

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;    

正如你所看到的,你可以通过AND与指定的SelectionKey常量进行运算来判断特定的事件是否在关注兴趣集合中。

Ready Set

Ready Set是一系列的已经准备好的操作集合。在一次选择(Selection)之后,你会首先访问这个ready set。Selection将会在下一小节中进行介绍。可以通过如下方式访问ready set。

int readySet = selectionKey.readOps();

可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel + Selector

从SelectionKey访问Channel和Selector很简单。如下:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();

Attaching Objects

可以将一个对象或更多信息附加到SelectionKey上。例如,你可以附加一个与通道一起使用的Buffer或者包含聚集数据的某个对象。如下所示:

selectionKey.attach(theObject);
Object attacheObj = selectionKey.attachment();

你也可以在Channel注册Selector时附加对象。如下所示:

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

Selecting Channels via a Selector

一旦将一个或多个通道注册到选择器上,就可以调用select方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。

下面是select()方法。

int select();
int select(long timeout);
int selectNow();

select()方法会阻塞直到所注册的事件准备好。

select( lont timeout)方法跟select()方法一样阻塞,但它最大阻塞时间为timeout。

selectNow()方法并不会阻塞,它立即返回无论是否有通道准备好。

方法返回的值表明有多少通道已经准备好。那就是说,从调用select()方法开始共有多少通道准备好。如果你调用select()方法并且返回1,说明有一个Channel已经准备好。如果你再次调用select方法,又一个通道准备好了,它又会返回1。如果对第一个已经准备好的通道没有进行任何处理,第二次将返回2。但在每次调用之间,只有一个通道准备就绪。

selectedKeys()

一旦你调用select方法,它的返回值表明有一个或多个通道已经准备好了。已经准备好了的channel可以通过selected key set进行访问,具体是通过调用 selectedKeys()方法。如下所示。

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

当向Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。

可以遍历这个已选择的键集合来访问就绪的通道。如下:

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();
}

这个循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件。

注意每次迭代末尾的keyIterator.remove()方法调用。Selector并不会自己主动删除selected key。当处理完成之后,必须进行手动处理。当下次channel准备好以后,它将会再次加入到Selected keys集合中。

SelectionKey.channel()方法返回的Channel必须强制转换成对应的Channel。例如ServerSocketChannel或SocketChannel。

wakeUp()

某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。

如果另外一个线程调用了wakeup方法,但此时并没有任何线程阻塞了。下个调用select方法的线程将会立刻wakeup。

close()

用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。

完整的Selector例子

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();
  }
}