NIO-selector原理

1. 概述

  Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。
  要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。

2.Selector

  Selector是NIO中实现I/O多路复用的关键类。Selector实现了通过一个线程管理多个Channel,从而管理多个网络连接的目的。
  仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
  Selector能够在单个线程中处理多个通道,这样可以减少多个线程造成上下文切换问题。
  Channel代表这一个网络连接通道,我们可以将Channel注册到Selector中以实现Selector对其的管理。一个Channel可以注册到多个不同的Selector中。

  当Channel注册到Selector后会返回一个SelectionKey对象,该SelectionKey对象则代表这这个Channel和它注册的Selector间的关系。并且SelectionKey中维护着两个很重要的属性:interestOps、readyOps
  interestOps是我们希望Selector监听Channel的哪些事件。我们将我们感兴趣的事件设置到该字段,这样在selection操作时,当发现该Channel有我们所感兴趣的事件发生时,就会将我们感兴趣的事件再设置到readyOps中,这样我们就能得知是哪些事件发生了以做相应处理。

  想想一个场景:在一个养鸡场,有这么一个人,每天的工作就是不停检查几个特殊的鸡笼,如果有鸡进来,有鸡出去,有鸡生蛋,有鸡生病等等,就把相应的情况记录下来,如果鸡场的负责人想知道情况,只需要询问那个人即可。

  在这里,这个人就相当Selector,每个鸡笼相当于一个SocketChannel,每个线程通过一个Selector可以管理多个SocketChannel

image

  为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:

1、connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8) 2、accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16) 3、read:读事件,对应值为SelectionKey.OP_READ(1) 4、write:写事件,对应值为SelectionKey.OP_WRITE(4)

  每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。

image.png

过程:
1、创建ServerSocketChannel实例,并绑定制定端口; 2、创建Selector实例; 3、将serverSocketChannel注册到Selector,并指定事件OP_ACCEPT,最底层的socket通过channel和selector建立关联; 4、如果没有准备号的socket,select方法会被阻塞一段时间并返回0; 5、如果底层有socket已经准备好,selector的select方法会返回socket的个数,而且selectedKeys方法会返回socket对应的事件(connect、accept、read or write); 6、根据时间类型,进行不同的处理逻辑。

在步骤3中,selector只注册了serverSocketChannel的OP_ACCEPT事件
1、如果有客户端A连接服务,执行select方法时,可以通过serverSocketChannel获取客户端A的socketChannel,并在selector上注册socketChannel的OP_READ事件。
2、如果客户端A发送数据,会触发read事件,这样下次轮询调用select方法时,就能通过socketChannel读取数据,同时在selector上注册该socketChannel的OP_WRITE事件,实现服务器往客户端写数据

3.Selector(选择器)的使用方法详细描述

image

  NIO中实现非阻塞IO的核心设计Selector,Selector就是注册各种IO事件的地方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件。
  当有读或者写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,同时从SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。
使用NIO中非阻塞IO编写服务器处理程序,有三个步骤
1.向Selector对象注册感兴趣的事件 2.从Selector中获取感兴趣的事件 3.根据不同事件进行相应的处理

1) Selector的创建

  通过调用Selector.open()方法创建一个Selector对象,如下:
  Selector selector = Selector.open();

2)注册Channel到Selector

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

Channel必须是非阻塞的

  所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannelSocket channel可以正常使用。

SelectableChannel抽象类 有一个 configureBlocking() 方法用于使通道处于阻塞模式或非阻塞模式。

abstract SelectableChannel configureBlocking(boolean block)

3)SelectionKey介绍

  一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。
key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel的时候指定。
key.channel(); // 返回该SelectionKey对应的channel。
key.selector(); // 返回该SelectionKey对应的Selector。
key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask
key.readyOps(); // 返回一个bit mask,代表在相应channel上可以进行的IO操作。

4)从Selector中选择channel(Selecting Channels via a Selector)

  选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中.
  Selector维护的三种类型SelectionKey集合:

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

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

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

5)select()方法介绍:

  在刚初始化的Selector对象中,这三个集合都是空的。 通过Selector的select()方法可以选择已经准备就绪的通道 (这些通道包含你感兴趣的的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法:

  select()
  获取就绪的 Channel,阻塞方法,没有就绪的 Channel 就一直阻塞该线程。

  select(long timeout)
  获取就绪的 Channel, 阻塞方法,阻塞 timeout 时间,如果超时还没有就绪的 Channel,返回0,不做任何操作。
  selectNow()
  获取就绪的 Channel,如果没有就绪的就直接返回,不阻塞当前线程。

上面三个 select方法底层都是调用 lockAndDoSelect 方法。 lockAndDoSelect方法的参数值 说明: -1 : 一直阻塞,直到有就绪的 Channel 可处理 0 : 不阻塞 0: 表示阻塞多长时间

  select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。
  之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。
  例如:首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。
  一旦调用select()方法,并且返回值不为0时,则 可以通过调用SelectorselectedKeys()方法来访问已选择键集合 。如下:
Set selectedKeys=selector.selectedKeys();
进而可以放到和某SelectionKey关联的SelectorChannel

欢迎关注公众号


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • # Java NIO # Java NIO属于非阻塞IO,这是与传统IO最本质的区别。传统IO包括socket和文...
    Teddy_b阅读 3,798评论 0 0
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    JackChen1024阅读 12,256评论 1 143
  • 在本章中,我们将探索选择器(selectors)。选择器提供选择执行已经就绪的任务的能力,这使得多元 I/O 成为...
    沉沦2014阅读 3,873评论 0 2
  • Android网络编程 目录 1、Java NIO 介绍 NIO是java New IO的简称,在jdk1.4里提...
    香沙小熊阅读 10,307评论 0 6
  • 背景 java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(...
    ninetyhe_阅读 3,161评论 0 0

友情链接更多精彩内容