Java NIO 学习笔记(三)----Selector

目录:
Java NIO 学习笔记(一)----概述,Channel/Buffer
Java NIO 学习笔记(二)----汇集和分散,通道到通道
Java NIO 学习笔记(三)----Selector
Java NIO 学习笔记(四)----文件通道和网络通道
Java NIO 学习笔记(五)----路径、文件和管道 Path/Files/Pipe
Java NIO 学习笔记(六)----异步文件通道 AsynchronousFileChannel
Java NIO 学习笔记(七)----NIO/IO 的对比和总结html

选择器是一个 NIO 组件,它能够检测一个或多个 NIO 通道,并肯定哪些通道能够用于读或写了。 这样,单个线程能够管理多个通道,从而管理多个网络链接。java

摘要:一个选择器可对应多个通道,选择器是经过 SelectionKey 这个关键对象完成对多个通道的选择的。注册选择器的时候会返回此对象,调用选择器的 selectedKeys() 方法也会返回此对象。每个 SelectionKey 都包含了一些必要信息,好比关联的通道和选择器,获取到 SelectionKey 后就能够从中取出对应通道进行操做。服务器

为何使用选择器?

仅使用单个线程来处理多个通道的优势是,只须要更少的线程来处理通道。 实际上只需使用一个线程来处理全部通道。 对于操做系统而言,在线程之间切换是昂贵的,而且每一个线程也占用操做系统中的一些资源(存储器)。 所以,使用的线程越少越好。网络

但请记住,现代操做系统和 CPU 在多任务处理中变得愈来愈好,所以随着时间的推移,多线程的开销会变得愈来愈小。 事实上,若是一个 CPU 有多个内核,你可能会因多任务而浪费 CPU 能力。 不管如何,这里知道可使用选择器使用单个线程处理多个通道就能够。多线程

如下是使用 1 个 Selector 处理 3 个 Channel 的线程图示:异步

image

使用选择器注册通道

首先建立一个选择器,它是经过这种方式建立的:学习

Selector selector = Selector.open();

要使用带选择器的通道,必须使用选择器来注册通道。 这是使用关联 Channel 对象的 register() 方法完成的,以下所示:测试

channel.configureBlocking(false); //不阻塞
SelectionKey key = channel.register(selector, SelectionKey.OP_READ); // 使用通道注册一个选择器

通道必须处于非阻塞模式才能与选择器一块儿使用。 这意味着没法将 FileChannel 与 Selector一 起使用,由于 FileChannel 没法切换到非阻塞模式。 套接字通道则支持。操作系统

注意 register() 方法的第二个参数。 这是一个“ interest 集合”,意味着经过 Selector 在 Channel 中监听哪些事件。能够收听四种不一样的事件:线程

  • Connect 链接
  • Accept 接收
  • Read 读
  • Write 写

一个“发起事件”的通道也被称为“已就绪”事件。 所以,已成功链接到另外一台服务器的通道是“链接就绪”。 接受传入链接的服务器套接字通道是“接收就绪”。 准备好要读取的数据的通道“读就绪”。 准备好写入数据的通道称为“写就绪”。

这四个事件由四个 SelectionKey 常量表示:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

若是要监听多个事件,那么能够用“|”位或操做符将常量链接起来,以下所示:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

本文后面再进一步回顾 interest 集合。

register() 方法返回的 SelectionKey 对象

正如在上一节中看到的,当使用 Selector 注册 Channel 时,register() 方法返回一个 SelectionKey 对象。 这个 SelectionKey 对象包含一些有趣的属性:

  • interest 集合
  • ready 集合
  • 对应 Channel
  • 对应 Selector
  • 附加对象(可选)
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;

可使用给定的 SelectionKey 常量和 interest 集合进行“&”位与操做,以查明某个事件是否在 interest 集合中。

ready 集合

就绪集是通道准备好的一组操做。 将在 Selector 后访问就绪集,能够像这样访问 ready set:

int readySet = selectionKey.readOps();

可使用与上面 interest 集合相同的方式,使用位与操做进行检测频道已准备好的事件/操做。 可是,也可使用下面这四种方法,它们都会返回一个布尔值:

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

对应 Channel + Selector

从 SelectionKey 访问通道和选择器很是简单:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();

附加对象(可选)

能够将对象或者更多信息附加到 SelectionKey ,这是识别某个通道的便捷方式。 例如,能够将正在使用的缓冲区与通道或其余对象相关联。 如下是使用方法:

// 将 theObject 对象附加到 SelectionKey 
selectionKey.attach(theObject);
// 从 SelectionKey 中取出附加的对象
Object attachedObj = selectionKey.attachment();

还能够在 register() 方法中添加参数,在使用 Selector 注册 Channel 时就附加对象。以下:

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

经过选择器选择通道

使用 Selector 注册一个或多个通道后,能够调用其中一个 select() 方法。 这些方法返回咱们感兴趣的,已就绪的事件(链接,接受,读写)的通道。 换句话说,若是对读就绪通道感兴趣,select() 方法会返回读事件已经就绪的那些通道

如下是 select() 方法:

  • int select() : 将一直阻塞,直到至少有一个频道为注册的事件作好准备。
  • int select(long timeout) :与 select() 相同,但它会最长阻塞 timeout 毫秒。
  • int selectNow() :彻底没有阻塞。 它会当即返回任何已准备好的通道。

select() 方法返回的 int 表示有多少通道准备好了。也就是说,自从你上次调用 select() 以来,有多少频道已经准备好了。

若是调用 select() ,由于一个频道已准备就绪,它会返回 1 ,再次调用 select() ,由于另一个通道已准备就绪,它会再次返回 1 。若是没有对第一个已准备就绪的通道作任何事情,那么如今就有 2 个准备就绪的频道,可是在每次 select() 调用之间,只有一个通道是准备就绪的。

选择器的 selectedKeys() 方法返回的 SelectionKey 集合

一旦调用了其中一个 select() 方法而且其返回值表示有通道已准备就绪,就能够经过调用选择器的 selectedKeys() 方法,由于一个选择器能够注册多个通道,因此这里返回集合。经过“已选择键集(selected key set)”访问就绪通道。 以下:

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

使用 Selector 注册通道时,Channel 对象的 register() 方法返回 SelectionKey 对象。此对象表明了该选择器注册的通道。

能够迭代 selectedKeys() 方法返回的 Set 集合来访问就绪通道。以下:

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

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        //  ServerSocketChannel接受了一个链接。
    } else if (key.isConnectable()) {
        //  与远程服务器创建链接。
    } else if (key.isReadable()) {
        // 一个通道已读就绪
    } else if (key.isWritable()) {
        // 一个通道已写就绪
    }
    keyIterator.remove();
}

此循环迭代 Set ,对于每一个 key ,它测试 key 以肯定 key 引用的通道已准备就绪的事件。

注意选择器不会从 Set 自己中删除 SelectionKey 对象。 完成通道处理后,必须在每次迭代结束时的调用 keyIterator.remove() 来删除集合中已处理过的 SelectionKey 。 下一次通道变为“就绪”时,选择器会再次将其添加到选择键集中。

这里 Set 中的 SelectionKey 和当时使用 Selector 注册 Channel 返回的 SelectionKey 是同样的,请参考上述。

调用其对象方法 selectionKey.channel();就会返回 Channel 对象,这时候咱们应该将其转换为具体须要使用的通道,例如 ServerSocketChannel 或 SocketChannel 等。

wakeUp() 唤醒被阻塞的线程

已调用 select() 方法的线程可能会被阻塞,这是能够经过调用 wakeUp() 方法离开 select() 方法,即便还没有准备好任何通道。其它线程来调用阻塞线程 Selector 对象的 select() 便可让阻塞在 select() 方法上的线程立马返回。

若是另外一个线程调用 wakeup() 而且当前在 select() 中没有阻塞线程,则调用 select() 的下一个线程将当即被“唤醒”。

close() 关闭选择器

调用选择器的 close() 方法将关闭 Selector 并使使用此 Selector 注册的全部 SelectionKey 实例失效。 但通道自己并不会被关闭。

Selector 选择器总结

下面是一个完整的例子,它打开一个 Selector ,用它注册一个通道(由于通道相关在后面,还未学习,这里通道实例化被省略),并继续监视 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;

      // 这里的 SelectionKey 就和注册时候返回的 key 同样,
      // 由于一个选择器能够注册多个通道,因此这里返回集合
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if(key.isAcceptable()) {
            //  ServerSocketChannel接受了一个链接。
        } else if (key.isConnectable()) {
            //  与远程服务器创建链接。
        } else if (key.isReadable()) {
            // 一个通道已读就绪
        } else if (key.isWritable()) {
            // 一个通道已写就绪
        }
        keyIterator.remove();
    }
}

再回顾一下:

  1. Selector.open() 打开选择器,设置通道不阻塞,调用通道的 register() 方法注册选择器,此方法的第二个参数是一个“ interest 集合”(Connect 、Accept 、Read 、Write )
  2. register() 方法返回一个 SelectionKey 对象,此对象包含了一些注册信息(interest 集合,ready 集合,对应 Channel,对应 Selector,附加对象(可选)),能够调用此对象的一些方法返回一些颇有用的信息,例如Channel channel = selectionKey.channel();返回关联的通道。
  3. 使用 Selector 注册一个或多个通道后,能够调用其中一个 select() 方法来选择通道,选择什么通道呢?选择咱们注册时候, interest 集合里面所关注的全部通道,而后返回被选择的已准备就绪的通道数量,若是此方法返回值不为 0 ,表明 selector 对象里面有包含咱们须要的通道了。
  4. 知道有就绪通道后,可使用 selector.selectedKeys() 方法获取 SelectionKey 集合,对于集合中每个 SelectionKey 都包含了一些必要信息,好比关联的通道和选择器,注意一个选择器可对应多个通道。获取到 SelectionKey 后就能够从中取出对应通道进行操做,这也是选择器的做用所在,一个选择器,操做多个通道。
相关文章
相关标签/搜索