前言

Selector选择器是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样使得一个单独的线程可以管理多个Channel,从而管理多个网络连接。选择器提供选择执行已经就绪的任务的能力,使得多元I/O成为可能。选择器的执行细节:

  • 创建一个或多个可选择的通道(SelectableChannel)
  • 将这些创建的通道注册到选择器对象中
  • 选择键会记住开发者关心的通道,它们也会追踪对应的通道是否就绪
  • 开发者调用选择器的select()方法,当方法从阻塞状态返回时,选择键会被更新
  • 获取选择键的集合,找到当时已经就绪的通道,通过遍历这些键,开发者可以选择对已经就绪的通道做操作

为什么使用Selector

对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的资源,所以使用Selector单独管理多个Channel

选择器,选择键和可选择通道

选择器(Selector)

选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道和选择器是一起被注册的,并且使用选择器来更新通道的就绪状态

可选择通道(SelectableChannel)

这个抽象类提供了实现通道的可选择性所需的公共方法,它是所有支持就绪检查的通道类的父类,FileChannel对象是不可选择的,它没有继承SelectableChannel类,所有Socket通道都是可选择的,包括从管道(Pipe)对象中获得的通道。SelectableChannel可以被注册到Selector上,同时可以设定对哪个选择器而言哪种操作是感兴趣的。一个通道可以被注册到多个选择器上,而一个选择器只能被注册一次

选择键(SelectionKey)

选择键封装了特定的通道与特定的选择器的注册关系。调用SelectableChannel的register()方法会返回选择键并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数形式进行编码),指示了该注册关系所关心的通道操作以及通道已经准备好的操作

Selector

Selector的创建

Selector selector = Selector.open();

向Selector注册通道

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

与Selector一起使用时,通道必须处于非阻塞模式,这意味着FileChannel不能与Selector一起使用,套接字通道都可以

register()方法的第二个参数是一个“interest集合”,意思是通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件,用四种常量表示:

  • SelectionKey.OP_CONNECT: 连接就绪(某个Channel成功连接到另一个服务器)
  • SelectionKey.OP_ACCEPT: 接收就绪(一个ServerSocketChannel准备好接收新进入的连接)
  • SelectionKey.OP_READ: 读就绪(一个有数据可读的通道)
  • SelectionKey.OP_WRITE: 写就绪(等待写数据的通道)

通道触发了一个事件,就表示该事件已经就绪。如果对不止一种事件感兴趣,可以用位或操作符将常量连接:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

注意: 并非所有的操作都在所有的可选择通道上支持,比如SocketChannel就不支持accept

SelectionKey

public abstract class SelectionKey
{
public static final int OP_READ;
public static final int OP_WRITE;
public static final int OP_CONNECT;
public static final int OP_ACCEPT;
public abstract SelectableChannel channel();
public abstract Selector selector();
public abstract void cancel();
public abstract boolean isValid();
public abstract int interestOps();
public abstract void iterestOps(int ops);
public abstract int readyOps();
public final boolean isReadable();
public final boolean isWritable();
public final boolean isConnectable();
public final boolean isAcceptable();
public final Object attach(Object ob);
public final Object attachment();
}

关于这些API,总结几点:

  1. 一个键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系,channel()和selector()方法反映了这种关系
  2. 开发者可以使用cancel()方法结束这种关系,也可以使用isValid()方法检查这种有效的关系是否仍然存在,可以使用readyOps()方法来获取相关通道已经就绪的操作
  3. 使用isReadable()等四个方法判断通道的就绪状态
  4. 当通道关闭时,所有相关的键会自动取消(一个通道可以被注册到多个选择器上),当选择器关闭时,所有被注册到该选择器的通道将会被注销(通道本身并不会关闭),相关的键被立即取消

这个对象包含了一些属性:

  • interest集合
  • ready集合
  • Channel
  • Selector
  • 附加的对象(可选)

interest集合

interest集合是你所选择的感兴趣的事件集合。可以通过SelectionKey读写interest集合

int interestSet = selectionKey.interestOps();
boolean isAccept = (interest & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isConnect = (interest & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
boolean isRead = (interest & SelectionKey.OP_READ) == SelectionKey.OP_READ;
boolean isWrite = (interest & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;

用位与操作interest集合和SelectionKey常量可以判断某个确定的事件是否在interest集合中

ready集合

ready集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,会首先访问ready set。

int readySet = selectionKey.readyOps();

可以像检测interest集合一样,检测Channel中有哪些事件或操作已经准备就绪,也可以用以下方法:

boolean isAccept = selectionKey.isAcceptable();  // 这种写法等价于 (selectionKey.readyOps() & SelectionKey.OP_ACCEPT) != 0; 下面的类似
boolean isConnect = selectionKey.isConnectable();
boolean isRead = selectionKey.isReadable();
boolean isWrite = selectionKey.isWriteable();

Channel和Selector

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

附加对象

可以将一个对象或更多信息附加到SelectionKey上,这样能方便识别某个通道。例如,可以附加与通道一起使用的Buffer,或是包含聚集数据的某个对象

selectionKey.attach(object);
Object obj = selectionKey.attachment();

还可以用register()方法向Selector注册Channel时附加对象

SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_READ, object);

通过Selector选择通道

一旦向Selector注册了一个或多个通道,就可以调用几个重载的select()方法。这些方法会返回你所感兴趣的事件已经准备就绪的通道。

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

select()方法会阻塞到至少有一个通道在你注册的事件上就绪了

select(long timeout)和select()一样,除了最长会阻塞timeout毫秒

selectNow()不会阻塞,不管什么通道就绪都立刻返回

select()方法返回的int值表示有多少通道已经就绪,即自从上次调用select()方法后有多少通道变成了就绪状态。如果调用一次select()方法,因为有一个通道变成了就绪状态,返回1,如果再次调用select()方法,如果另一个通道也就绪了,则还会返回1。即使,第一个就绪的通道没有做任何操作,但是每次select()方法调用之间,只有一个通道变成就绪状态

Selector维护的三种键

选择器维护着注册过的通道的集合,并且这些注册关系中的任意一个都是封装在SelectionKey对象中。每个Selector对象维护三种键的集合:

已注册的键的集合(Registered key set)

与选择器关联的已经注册的键的集合,并不是所有注册过的键都有效。这个集合通过key()方法返回,并且可能为空。这些键的集合不可以直接修改,试图这么做将引发java.lang.UnsupportedOperationException

已选择的键的集合(Selected key set)

已注册的键的集合的子集,这个集合的每个成员都是相关的通道被选择器判断为已经准备好的并且包含于键的interest集合中的操作,这个集合通过selectedKeys()方法返回(可能为空)

正常情况下,一旦调用了select()方法,且返回值表明有一个或多个通道就绪了,然后可以调用Selector的selectedKeys()方法,访问“已选择键集“(selected key set)中的就绪通道

Set selectedKeys = selector.selectedKeys();

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

已取消的键的集合(Cancelled key set)

已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,无法直接访问

选择过程

选择器是对select(),poll(),epoll()等本地调用或类似的操作系统特定的系统调用的一个包装,但Selector所做的不仅仅是简单的向本地代码传递参数,每个操作都有特定的过程,对每个过程的理解是合理的管理键和它们所表示的状态信息的基础

选择操作是当三种形式的select()方法任意一种被调用时,由选择器执行的。不管哪一种被调用,以下过程都会被执行:

  1. 已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键都会从另外两个集合中移除,并且相关的通道将被注销。此步骤结束,已取消的键的集合将会为空
  2. 已注册的键的集合中的键的interest集合将被检查,此步骤结束,对interest集合的改动将不会影响剩余的检查过程。一旦就绪条件被定下来,底层操作系统将会进行查询,用来确定每个通道所关心的操作的真实就绪状态,依赖于特定的select()方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值
  3. 步骤二可能会花费很长时间,特别是线程处于阻塞状态时,与该选择器相关的键可能会同时被取消,当步骤二结束时,步骤一将再次被执行,用来完成任意一个在选择进行的过程中,键已经被取消的通道的注册

wakeUp()

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

如果有其他线程调用了wakeUp()方法,且线程调用select()方法没有阻塞,下次调用select()方法的线程会立即醒来

close()

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

代码

public class SelectorServer{
  private static int PORT = 12345;
  public static void main(String[] args) throws Exception{
    int port = PORT;
    // 打开一个ServerSocketChannel
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    // 获取ServerSocketChannel绑定的Socket
    ServerSocket serverSocket = serverChannel.socket();
    // 设置ServerSocket监听的端口
    serverSocket.bind(new InetSocketAddress(port));
    // 设置ServerSocketChannel为非阻塞模式
    serverChannel.configureBlocking(false);
    
    // 打开一个选择器
    Selector selector = Selector.open();
    // 将ServerSocketChannel注册到选择器上,并监听ACCEPT事件
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    while(true){
      // 这里会发生阻塞,等待就绪的通道
      int n = selector.select();
      if(n == 0){
        continue;
      }
      // 获取SelectionKey上已经就绪的通道的集合      
      Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
      // 遍历每一个key
      while(iterator.hasNext()){
        SelectionKey key = iterator.next();
        // 通道上是否有可接收的连接
        if(key.isAcceptable()){
          ServerSocketChannel serverChannel2 = (ServerSocketChannel) key.channel();
          SocketChannel socketChannel = serverChannel2.accept();
          socketChannel.configureBlocking(false);
          socketChannel.register(selector, SelectionKey.OP_READ);
        }
        // 通道上是否有数据可读
        else if(key.isReadable()){
          readDataFromSocket(key);
        }
        iterator.remove(); 
      }
    }
  }
  private static ByteBuffer buf = ByteBuffer.allocate(1024);
  // 从通道中读取数据
  public static void readDataFromSocket(SelectionKey key){
    SocketChannel socketChannel = (SocketChannel) key.channel();
    buf.clear();
    while(socketChannel.read(buf) > 0){
      buf.flip();
      while(buf.hasRemaining()){
        System.out.println((char)buf.get());
      }
      buf.clear();
    }
  }
}

注意一: 严格意义上来说NIO并非是一种非阻塞IO,因为NIO会阻塞在Selector的select()方法上

注意二: 满足isAcceptable()方法表示该通道上有数据到来了,此时我们做的事情不是获取该通道(创建一个线程来读取该通道上的数据),这么做就和前面一直讲的阻塞IO没有区别了,也无法发挥出NIO的优势来。我们做的事情只是简单地将对应的SocketChannel注册到选择器上,通过传入OP_READ标记,告诉选择器我们关心新的Socket通道什么时候可以准备好读数据。满足isReadable()方法则表示新注册的Socket通道已经可以读取数据了,此时调用readDataFromSocket方法读取SocketChannel中的数据

注意三: 将键移除,这一行很重要也是容易忘记的一步操作。加入不remove,将会导致socketChannel.configureBlocking(false)出现空指针异常

选择器客户端的代码没什么要求,只要向服务端发送数据就可以了

Selector可以简化用单线程同时管理多个可选择通道的实现。使用一个线程来为多个通道提供服务,通过消除管理各个线程的额外开销,可能会降低复杂性并可能大幅提升性能。

对单核CPU的系统而言这可能是一个好主意,因为在任何情况下都只有一个线程能够运行。通过消除在线程之间进行上下文切换带来的额外开销,总吞吐量可以提高。但对于一个多核CPU的系统而言呢?在一个有n个CPU的系统上,当一个单一的线程线性轮流地处理每一个线程时,可能有(n-1)个CPU处于空闲状态。

一种可行的解决办法是使用多个选择器。但是请尽量不要这么做,在大量通道上执行就绪选择并不会有很大的开销,大多数工作是由底层操作系统完成的,管理多个选择器并随机地将通道分派给它们当中的一个并不是这个问题的合理的解决方案。

一种更好的解决方案是对所有的可选择通道使用同一个选择器,并将对就绪选择通道的服务委托给其他线程。开发者只使用一个线程监控通道的就绪状态,至于通道处于就绪状态之后又如何做,有两种可行的做法:

1、使用一个协调好的工作线程池来处理接收到的数据,当然线程池的大小是可以调整的

2、通道根据功能由不同的工作线程来处理,它们可能是日志线程、命令/控制线程、状态请求线程等

Java NIO(六)选择器的更多相关文章

  1. 【NIO】Java NIO之选择器

    一.前言 前面已经学习了缓冲和通道,接着学习选择器. 二.选择器 2.1 选择器基础 选择器管理一个被注册的通道集合的信息和它们的就绪状态,通道和选择器一起被注册,并且选择器可更新通道的就绪状态,也可 ...

  2. Java NIO之选择器

    1.简介 前面的文章说了缓冲区,说了通道,本文就来说说 NIO 中另一个重要的实现,即选择器 Selector.在更早的文章中,我简述了几种 IO 模型.如果大家看过之前的文章,并动手写过代码的话.再 ...

  3. java输入输出 -- Java NIO之选择器

    一.简介 前面的文章说了缓冲区,说了通道,本文就来说说 NIO 中另一个重要的实现,即选择器 Selector.在更早的文章中,我简述了几种 IO 模型.如果大家看过之前的文章,并动手写过代码的话.再 ...

  4. Java NIO:选择器

    最近打算把Java网络编程相关的知识深入一下(IO.NIO.Socket编程.Netty) Java NIO主要需要理解缓冲区.通道.选择器三个核心概念,作为对Java I/O的补充, 以提升大批量数 ...

  5. Java NIO Selector选择器

    Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读.可写.如此可以实现单线程管理多个channels,也就是可以管理多个网络链接. 为什么使用S ...

  6. Java NIO之选择器Selector

    在单独的线程中,检查多个通道是否可以进行IO操作. Selector创建:静态工厂方法创建 Selector selector = Selector.open(); 注册通道 channel.conf ...

  7. Java NIO读书笔记

    一.Java IO与NIO区别: (1)Java NIO提供了与标准IO不同的IO工作方式: Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO ...

  8. 海纳百川而来的一篇相当全面的Java NIO教程

    目录 零.NIO包 一.Java NIO Channel通道 Channel的实现(Channel Implementations) Channel的基础示例(Basic Channel Exampl ...

  9. Java NIO系列教程(十一) Java NIO 与 IO

    Java NIO系列教程(十一) Java NIO与IO 当学习了 Java NIO 和 IO 的 API 后,一个问题马上涌入脑海: 我应该何时使用 IO,何时使用 NIO 呢?在本文中,我会尽量清 ...

  10. 【Java nio】java nio笔记

    缓冲区操作:缓冲区,以及缓冲区如何工作,是所有I/O的基础.所谓“输入/输出”讲的无非就是把数据移出货移进缓冲区.进程执行I/O操作,归纳起来也就是向操作系统发出请求,让它要么把缓冲区里的数据排干,要 ...

随机推荐

  1. dotnetnuke 调用第三方dll出错 System.Security.Permissions.SecurityPermission,型的权限已失败。

    在dnn下调用第三方dll的微信sdk ,代码如下: WebClient wc = new WebClient();  wc.Encoding = encoding ?? Encoding.UTF8; ...

  2. eclipse离线安装pydev

    首先,下载去http://pydev.org/下载Python的Eclipse插件PyDev. 目前的最新版是PyDev 2.7.1.zip,将压缩文件解压出来.得到features和plugins两 ...

  3. 【从零开始】【Java】【1】Git和svn

    闲聊 干活快一年了吧,感觉工作中能干的事情也有一点了,但总有种不通透的感觉,查一个问题,能一路查出一堆不明白的东西. 之前新建过文档是记录点点滴滴的知识的,使用上没问题了,但原理什么的还是不懂,想了想 ...

  4. CorelDRAW三十周年庆典暨2019新耀发布会,诚邀您的莅临!

    30年时光荏苒!眨眼风惊雨过. 在1989年的春天,CorelDRAW 1.0正式发布,一经面世就掀起了图形设计行业革命浪潮,这个图形工具不仅给设计师提供了矢量图像.页面设计,更能应用于网站制作.位图 ...

  5. 如何避免命令 rm -rf 的悲剧

    一.root高管用户为例,其他用户类同. https://www.cnblogs.com/eos666/articles/10389179.html [root@jenkins /]# vim /ro ...

  6. Map之HashMap的get与put流程,及hash冲突解决方式

    在java中HashMap作为一种Map的实现,在程序中我们经常会用到,在此记录下其中get与put的执行过程,以及其hash冲突的解决方式: HashMap在存储数据的时候是key-value的键值 ...

  7. JavaScript 原型 原型链

    一. 普通对象与函数对象 JavaScript 中,万物皆对象!但对象也是有区别的.分为普通对象和函数对象,Object .Function 是 JS 自带的函数对象.下面举例说明 var o1 = ...

  8. [51nod1074] 约瑟夫问题 V2

    毫无思路,Orz了一下大佬的思路%%%. 大概就是因为k比n小的多,我们知道约瑟夫环有个公式是fn=(fn-1+k) mod n 可以改一下,改成fn+p=(fn+pk) mod (n+p) 但是这样 ...

  9. Mybatis-Plus的BaseMapper的使用

    Mybatis-Plus 是一款 Mybatis 动态 SQL 自动注入 Mybatis 增删改查 CRUD 操作中间件, 减少你的开发周期优化动态维护 XML 实体字段. 下面简单举例,调用Base ...

  10. Microsoft Dynamics CRM 2013 for Outlook 的硬件要求

    当仅联机或脱机模式下执行 Microsoft Dynamics CRM 2013 for Microsoft Office Outlook 时,下表列出了建议的最低硬件要求 watermark/2/t ...