Java-NIO篇章(4)——Selector选择器详解

365BET 2025-09-11 09:23:53 admin 865 379
Java-NIO篇章(4)——Selector选择器详解

Selector介绍

选择器(Selector)是什么呢?选择器和通道的关系又是什么?这里详细说明,假设不用选择器,那么一个客户端请求数据传输那就需要建立一个连接,为了避免线程阻塞,那么每个客户端开辟一个线程。而学过JVM的都知道,默认每开一个线程需要栈空间内存1MB大小。如果这时候有大量的客户端连接请求,那么这个内存占用是非常可怕的,而且开辟大量的线程将导致CPU频繁上下文切换,效率非常低。举个例子,我们的服务器就是一家餐厅,客户端就是顾客,餐厅为顾客服务,如果每来一个客人(客户端请求)我们就派一个服务员(线程)那么这样消耗是消耗不起的。最正常的逻辑是,餐厅只招聘一个服务员(一个线程),然后通过一个监控器(Selector)监控所有顾客的需求(监控IO事件),如果哪个顾客需要服务就喊一下(这个信号就是下面的IO事件),然后服务员就跑过去为他服务。这样虽然一个线程很累,但是只需要一个线程就可以处理大量的socket连接,参考Redis单线程模式设计就知道一个线程如果专心处理非阻塞不耗时的业务是非常非常快的。借用一张网图非常清楚地描述了Selector、Channel、Buffer三个核心组件的关系,如下图所示:

来一段专业的介绍:选择器的使命是完成IO的多路复用,其主要工作是通道的注册、监听、事件查询。一个通道代表一条连接通路,通过选择器可以同时监控多个通道的IO(输入输出)状况。选择器和通道的关系,是监控和被监控的关系。 选择器提供了独特的API方法,能够选出(select)所监控的通道已经发生了哪些IO事件,包括读写就绪的IO操作事件。 一般是一个单线程处理一个选择器,一个选择器可以监控很多通道。所以,通过选择器,一个单线程可以处理数百、数千、数万、甚至更多的通道。在极端情况下(数万个连接),只用一个线程就可以处理所有的通道,这样会大量地减少线程之间上下文切换的开销。

先介绍什么是IO事件吧,这里的IO事件不是对通道的IO操作,而是通道处于某个IO操作的就绪状态,表示通道具备执行某个IO操作的条件。 比方说某个SocketChannel传输通道,如果完成了和对端的三次握手过程,则会发生“连接就绪” (OP_CONNECT)的事件。再比方说某个ServerSocketChannel服务器连接监听通道,在监听到一个新连接的到来时,则会发生“接收就绪”(OP_ACCEPT)的事件。还比方说,一个SocketChannel通道有数据可读,则会发生“读就绪”(OP_READ)事件;一个等待写入数据的SocketChannel通道,会发生写就绪(OP_WRITE)事件。这里注意,只有FileChannel文件通道不可用被选择器监控或选择的。其他的三个通道都可以被Selector监控。

通道和选择器之间的关联,通过register(注册)的方式完成。调用通道的Channel.register (Selector selector, int ops)方法,可以将通道实例注册到一个选择器中。 register方法有两个参数:第一个参数,指定通道注册到的选择器实例; 第二个参数,指定选择器要监控的IO事件类型。可供选择器监控的通道IO事件类型,包括以下四种:

可读: SelectionKey.OP_READ

可写:SelectionKey.OP_WRITE

连接:SelectionKey.OP_CONNECT

接收: SelectionKey.OP_ACCEPT

以上的事件类型常量定义在SelectionKey类中。如果选择器要监控通道的多种事件,可以用“按位或”运算符来实现。例如,同时监控可读和可写IO事件:

//监控通道的多种事件,用“按位或”运算符来实现

int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;

SelectionKey选择键

通道和选择器的监控关系, 本质是一种多对一的关联关系。 一个选择器Selector可以监控多个通道Channel,那么如何区分不同的Channel呢?很简单,给每个Channel取一个唯一的名字就行,这个名字就是SelectionKey,这样就可以维护不同的Channel了。Selector并不直接去管理Channel,而是直接管理SelectionKey,通过SelectionKey与Channel发生关系。一个Channel最多能向Selector注册一次,注册之后就形成了唯一的SelectionKey, 然后被Selector管理起来。 Selector有一个核心成员keys,专门用于管理注册上来的SelectionKey, Channel注册到Selector后所创建的那一个唯一的SelectionKey,添加在这个keys成员中,这是一个HashSet类型的集合。 除了成员keys之外, Selector还有一个核心成员selectedKeys,用于存放已经发生了IO事件的SelectionKey。怎么样?绕晕了吗?别慌,看下面的图:

SelectionKey是IO事件的记录者(或存储者) , SelectionKey 有三个核心成员,一个是关联的Channel通道,另外两个分别存储着自己关联的Channel上的感兴趣IO事件和已经发生的IO事件。Channel通道上可以发生多种IO事件,比如说读就绪事件、写就绪事件、新连接就绪事件,但是SelectionKey记录事件的成员却是一个整数类型。 这样问题就来了,一个整数如何记录多个事件呢?答案是,通过比特位来完成的。 具体的IO事件所占用的哪一个比特位,通过常量的方式定义在SelectionKey中, 如下:

//读取就绪事件,第 0 位

public static final int OP_READ = 1 << 0;

//写入就绪事件,第 2 位

public static final int OP_WRITE = 1 << 2;

//传输通道建立成功的 IO 事件,第 3 位

public static final int OP_CONNECT = 1 << 3;

//新连接就绪事件,第 4 位

public static final int OP_ACCEPT = 1 << 4;

通过SelectionKey的interestOps成员上相应的比特位,可以设置、查询关联的Channel所感兴趣的IO事件;通过SelectionKey的readyOps上相应的比特位,可以查询关联Channel所已经发生的IO事件。 对于interestOps成员上的比特位, 应用程序是可以设置的;但是对于readyOps上的比特位,应用程序只能查询,不能设置。因为,readyOps上的比特位是已经发生了的IO事件,只能由客户端被动触发,不能主动设置。readyOps发生的IO事件只能是Channel感兴趣的interestOps中的IO事件。通道和选择器的监控关系注册成功后, Selector就可以查询就绪事件。具体的查询操作,是通过调用选择器Selector的select( )系列方法来完成。通过select系列方法,可以不断地查询通道中所发生操作的就绪状态(或者IO事件) , 并且把这些发生了底层IO事件,转换成Java NIO中的IO事件,记录在的通道关联的SelectionKey的readyOps上。除此之外,发生了IO事件的SelectionKey,还会记录在Selector内部selectedKeys集合中。简单来说, 一旦在通道中发生了某些IO事件(就绪状态达成),这个事件就被记录在SelectionKey的readyOps上,并且这个SelectionKey被记录在Selector内部的selectedKeys集合中。(1) 通道必须在Selector注册过;(2) 所发生的事件必须是SelectionKey上interestOps成员记录的事件。

使用Selector选择器

使用选择器,主要有以下三步:

获取选择器实例;

将通道注册到选择器中;

轮询感兴趣的IO就绪事件(选择键集合)。

第一步:获取选择器实例。选择器实例是通过调用静态工厂方法open()来获取的,具体如下:

//调用静态工厂方法 open()来获取 Selector 实例

Selector selector = Selector.open();

第二步:将通道注册到选择器实例。要实现选择器管理通道,需要将通道注册到相应的选择器上,简单的示例代码如下:

// 2.获取通道

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

// 3.设置为

相关推荐

足球365官网是哪个 苹果5s出来多久了

苹果5s出来多久了

📅 08-18 👁️ 5732
足球365官网是哪个 三款百万内大型SUV横评:豪华电动与硬派越野谁更胜一筹

三款百万内大型SUV横评:豪华电动与硬派越野谁更胜一筹

📅 09-02 👁️ 3041
365BET 王者荣耀廉颇铭文攻略 廉颇铭文怎么搭配

王者荣耀廉颇铭文攻略 廉颇铭文怎么搭配

📅 07-26 👁️ 6717