Java NIO 系列学习 07 - Selector

Java NIO Selector 是一个可以选择一个或多个 Channel 实例、确定哪个 Channel 处于可写或可读状态的组件。
通过这种方式,一个线程可以管理多个 Channel、多个网络连接。

为什么使用 Selector

只使用一个线程去处理多个channel, 相较于使用多个线程来处理是有优势的。事实上,我们可以只使用一个线程去处理所有的channels
在操作系统中,切换线程的开销是昂贵的,且每一个线程都会占用操作系统的资源(内存等)。因此使用更少的线程性能也就更好。

需要注意的是,现代操作系统和CPU,在多任务处理的支持上是越来越好了,因此随着时间的推移多线程的开销会变得更小。
事实上,如果一个CPU有多个核心,不使用多任务会是一种浪费。在这里不讨论这个问题,只需要说明,可以使用selector通过一个线程来管理多个channel

下面是一个示例图,使用selector管理3个channel:

创建 Selector

通过调用Selector.open()方法来创建一个Selector:

Selector selector = Selector.open();

注册 Channel 到 Selector

为了通过Selector来使用Channel, 就必须注册Channel. 通过调用SelectableChannel.register()方法来实现:

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

使用Selector有一个前提, Channel 必须是非阻塞模式(non-blocking)。也就是说,FileChannel及其继承类就不可以使用(因为其不能切换到非阻塞模式),SocketChannel 是可以使用的。
需要注意下register()方法的第二个参数,需要声明这个channel的事件,总共有四个不同的事件可供选择:
1. SelectionKey.OP_READ (read operations.) 2. SelectionKey.OP_WRITE (write operations.) 3. SelectionKey.OP_CONNECT (socket-connect operations.) 4. SelectionKey.OP_ACCEPT (socket-accept operations.)

每一个事件都相当于是ready状态, 一些解释如下:
1. 当channel准备好读数据时,状态是read ready 2. 当channel准备好写数据时,状态是write ready 3. 当channel连接到另一个server成功时,状态是connect ready 4. 当server socket chennel 接收到连接请求时,状态是accept ready

如果想需要同时多个事件,可以这样传参:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

SelectionKey

注册Channel时调用的register()方法会返回一个SelectionKey对象,这个对象的一些属性我们简单来看下:

Interest Set

interest set 是一组事件组合。我们可以通过SelectionKey来读写inserset 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;

可以通过&操作符来判断所需操作是否在inserset set

Ready Set

ready set 是一组channel准备进行的操作集合。

int readySet = selectionKey.readyOps();

// 可以通过下面几个方法来测试 操作是否可以执行
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel + Selector

如何从SelectionKey中获取Channel和Selector呢?

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();

Attaching Objects

我们可以把一个对象附加到SelectionKey,这样就可以方便的识别给定的channel。举个例子:

// 入
selectionKey.attach(theObject);
// 出
Object attachedObj = selectionKey.attachment();

当然也可以在channel注册时进行附加

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

从Selector查询Channels

一旦你通过selector注册了一个或多个channel,就可以调用select()方法,这些方法返回处在所定义的ready状态的channels。
换句话说,如果你希望读状态就绪的channel,你就会通过select()方法拿到所有已经处在read ready状态的channels.

下面列举了一些select()方法:

  • int select()
  • int select(long timeout)
  • int selectNow()

select() 是阻塞的,一直等到至少有一个channel是所需事件ready状态时才会返回。
select(long timeout)同样是阻塞的,但是允许给定最长等待时间,毫秒级。
selectNow()非阻塞,会立即返回已经ready的channels。

select()方法返回的是有多少个channel是ready状态。也就是说,自上次调用select()以来又有多少个channel是ready状态。
如果调用select()返回了1则是因为有1个已是ready状态。当调用select()多于1次时,且又一个channel已ready,则仍返回1. 如果在两次调用之间没有对ready的channel执行任何操作,现在就有两个channel是ready的,但是只有一个channel变为ready在两次调用select()之间。

selectedKeys()

当调用select()方法时,返回的结果表示有几个channel是ready状态,可以通过”selected key set”来获取ready状态的channel,方法是selectedKeys():

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

通过channel.register()注册channel到一个Selector后,方法返回了一个SelectionKey对象。
可以从SelectionKey通过selectedKeySet()来获取这些channels。

可以通过迭代这个keySet来获取ready的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();
}

主要注意的是,keyIterator.remove();方法要在最后被调用。Selector自己是不会进行移除的。
SelectionKey.channel()返回的channel实例,需要强制转换为需要的Channel类。比如ServerSocketChannel.

wakeUp()

当线程调用阻塞的select()方法时,即时没有channel是ready状态也可以让线程退出select()方法。
怎么做到呢?需要另外一个线程去调用被阻塞线程使用的Selectorwakeup()方法,之后被select()阻塞的线程就会立即返回。

注意 如果另外一个线程调用wakeUp()时没有其它线程被select()阻塞,则下一个线程调用select()时会立即返回.

close()

当使用完Selector时需要调用close()方法,它会销毁Selector及所有使用SelectorKey的实例。Channel并不会被销毁。

本例代码

Selector selector = Selector.open();

channel.configureBlocking(false);

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


while(true) {

  int readyChannels = selector.selectNow();

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

参考

  1. Java NIO Selector