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.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4.绑定连接
serverSocketChannel.bind(new InetSocketAddress(18899));
// 5.将通道注册到选择器上,并制定监听事件为:“接收连接”事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

这里需要注意:注册到选择器的通道,必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常。还需要注意:一个通道,并不一定要支持所有的四种IO事件。例如服务器监听通道ServerSocketChannel,仅仅支持Accept(接收到新连接) IO事件;而传输通道SocketChannel则不同,该类型通道仅不支持Accept类型的IO事件。

第三步:选出感兴趣的IO就绪事件(选择键集合)。通过Selector选择器的select()方法 ,选出已经注册的、已经就绪的IO事件,并且保存到SelectionKey选择键集合中。 SelectionKey集合保存在选择器实例内部,其元素为SelectionKey类型实例。调用选择器的selectedKeys()方法,可以取得选择键集合。

//轮询,选择感兴趣的 IO 就绪事件(选择键集合)
while (selector.select() > 0) {Set selectedKeys = selector.selectedKeys();Iterator keyIterator = selectedKeys.iterator();while(keyIterator.hasNext()) {SelectionKey key = keyIterator.next();//根据具体的 IO 事件类型,执行对应的业务操作if(key.isAcceptable()) {// IO 事件: ServerSocketChannel 服务器监听通道有新连接} else if (key.isConnectable()) {// IO 事件:传输通道连接成功} else if (key.isReadable()) {// IO 事件:传输通道可读} else if (key.isWritable()) {// IO 事件:传输通道可写}//处理完成后,移除选择键keyIterator.remove();}
}

处理完成后,需要将选择键从这个SelectionKey集合中移除,防止下一次循环的时候,被重复的处理。 SelectionKeys集合不能添加元素。select()方法的返回值的是整数类型(int),表示发生了IO事件的数量。更准确地说,是从上一次select到这一次select之间,有多少通道发生了IO事件,更加准确地说,是指发生了选择器感兴趣(注册过)的IO事件数。

用于选择就绪的IO事件的select()方法,有多个重载的实现版本,具体如下:

  • select():阻塞调用,一直到至少有一个通道发生了注册的IO事件。
  • select(long timeout):和select()一样,但最长阻塞时间为timeout指定的毫秒数。
  • selectNow():非阻塞,不管有没有IO事件,都会立刻返回。

常用的是select():阻塞调用,因为如果没有IO事件发生的话CPU就不用在那儿空旋了,这样大大减少了系统消耗。

客户端连接服务器并发送数据例子

下面将举例将上面介绍的三个核心组件以一个案例的形式综合运用,代码如下:

首先是服务端的代码:

public class SelectorServer {public static void main(String[] args) throws IOException {// 1.创建selector,管理多个channelSelector selector = Selector.open();// ServerSocketChannel 可以获取连接通道和套接字通道ServerSocketChannel ssc = ServerSocketChannel.open();ssc.configureBlocking(false);//开启非阻塞时连接,影响的只是 channel.accept();// 2. 建立selector与channel之间的联系(注册channel)// (事件有四种:accept 有连接请求时触发 、connect 客户端建立后触发的事件、read 可读事件、write 可写事件)// sscKey 代表了 ssc连接通道与selector的关联关系SelectionKey sscKey = ssc.register(selector, SelectionKey.OP_ACCEPT, null);sscKey.interestOps(SelectionKey.OP_ACCEPT); // 表示 ssc通道 只关注 accept 事件log.debug("sscKey:"+sscKey);ssc.bind(new InetSocketAddress(8080)); // 服务器程序的端口号,ip为本机ipwhile (true){//3. select 方法,发生了上述事件才会向下继续执行,否则阻塞// selector 在事假未被处理时会将事件重新加入,因此一个事件要么处理要么取消,不能置之不理// 如果没有事件则阻塞,如果有事件未被处理则不会被阻塞,不处理需要selectorKey.cancel();取消事件selector.select(); //4. 处理事件 , 获取所有发生的事件// 获取所有注册的channel的key,可以拿到key访问channelSet<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectionKeys.iterator();while (iter.hasNext()){SelectionKey selectionKey = iter.next();iter.remove(); // 拿到了立即移除,处理完了只标记不删除,需要remove删除log.debug("Key:"+selectionKey);try{if(selectionKey.isAcceptable()){// 如果是连接就绪事件,那就获取对应的ServerSocketChannel,// 然后在接受获得可以数据传输的SocketChannel// 通过key获取channelServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel(); SocketChannel sc = channel.accept(); // 处理事件、 前面设置了非阻塞,没有连接就返回nullsc.configureBlocking(false); //开启非阻塞读,影响的只是 channel.read(buffer);SelectionKey scKey = sc.register(selector, SelectionKey.OP_READ, null);//其实上面第二个参数已经绑定了感兴趣的IO事件,这行不写也行,或者下面这行保留,上面第二个参数给0就行scKey.interestOps(SelectionKey.OP_READ);log.debug("SocketChannel:"+sc);} else if (selectionKey.isReadable()) {// 如果是可读事件,那么就读取内容SocketChannel channel = (SocketChannel) selectionKey.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int read = channel.read(buffer);if(-1==read){// 如果客户端正常断开则读到的数据无,返回-1System.out.println("客户端主动断开...");selectionKey.cancel();}else {buffer.flip();CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer);String msg = charBuffer.toString();System.out.println("客户端发来的信息:" + msg);}}else {selectionKey.cancel(); // 标记已处理}}catch (IOException e){e.printStackTrace();selectionKey.cancel();}}ssc.close();}}
}

其次是客户端的代码:

public class Client {public static void main(String[] args) throws IOException {SocketChannel sc = SocketChannel.open();sc.connect(new InetSocketAddress("localhost",8080));sc.configureBlocking(false);System.out.println("waiting......");while(!sc.finishConnect()){// 没有连接完成时等待Thread.yield();}System.out.println("客户端连接成功!");ByteBuffer byteBuffer = ByteBuffer.allocate(1024);byteBuffer.put("Hello world".getBytes());// 发送到服务器byteBuffer.flip();sc.write(byteBuffer);// 下面断开操作会触发一读事件sc.shutdownOutput();sc.close(); }
}

注意:客户端突然断开会触发一个IO事件,并且服务器会抛出异常,需要捕获异常并且cancel()取消掉这个事件。如果客户端主动正常close()断开也会让Selector查询到一个读事件,也需要处理取消该事件。 如果不处理则Selector则一直会有事件未处理,则不会被阻塞一直死循环。

在NIO中,服务器接收新连接的工作,是异步进行的。不像Java的OIO那样,服务器监听连接,是同步的、阻塞的。 NIO可以通过选择器(也可以说成:多路复用器),后续不断地轮询选择器的选择键集合,选择新到来的连接。 有了Linux底层的epoll支持,以及Java NIO Selector选择器等等应用层IO复用技术, Java程序从而可以实现IO通信的高TPS、高并发,使服务器具备并发数十万、数百万的连接能力。 Java的NIO技术非常适合用于高性能、高负载的网络服务器。鼎鼎大名的通信服务器中间件Netty,就是基于Java的NIO技术实现的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/243050.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

中仕教育:国考调剂和补录的区别是什么?

国考笔试成绩和进面名单公布之后&#xff0c;考生们就需要关注调剂和补录了&#xff0c;针对二者之间的区别很多考生不太了解&#xff0c;本文为大家解答一下关于国考调剂和补录的区别。 1.补录 补录是在公式环节之后进行的&#xff0c;主要原因是经过面试、体检和考察&#…

了解森林消防灭火泵:为何它是森林安全的关键

在森林火灾中&#xff0c;火势蔓延速度极快&#xff0c;一旦发生火灾&#xff0c;很难及时控制和扑灭。传统的灭火方法主要是利用水扑救&#xff0c;这种方法具有经济、简单、有效等优点。然而&#xff0c;在我国森林火灾中&#xff0c;水资源一直没有得到充分的利用。至今&…

基于Django的计算机编程技术学习与服务平台

临近毕业&#xff0c;又到了赶毕设的时候了&#xff0c;本次介绍分享一下自己的毕业设计项目吧。 项目主题&#xff1a;基于Django的计算机技术编程技术学习与服务平台 实现功能&#xff1a; 1.登入&#xff1a;用户的登陆注册 2.Python教程&#xff1a;实现用户的Python技…

苹果笔记本 macbook 在 office word 中使用 mathtype 的方法

前言 想在 MacBook 中使用 mathtype&#xff0c;去搜索&#xff0c;去 Apple Store 下载也发现没有 解决方法 打开 office Word 的「插入」中的「获取加载项」、「我的加载项」。 在应用商店中下载&#xff0c;需要登录自己的微软账号。 加载成功后就可以使用了。 注意 和…

【IP-Adapter】进阶 - 同款人物【2】 ☑

测试模型&#xff1a;###最爱的模型\flat2DAnimerge_v30_2.safetensors [b2c93e7a89] 原图&#xff1a; 加入 control1 [IP-Adapter] 加入 control 2 [OpenPose] 通过openpose骨骼图修改人物动作。 加入 control 3 lineart 加入cotrol3 …

Labview for循环精讲

本文详细介绍Labview中For循环的使用方法&#xff0c;从所有细节让你透彻的看明白For循环是如何使用的&#xff0c;如果有帮助的话记得点赞加关注~ 1. For循环结构 从最简单的地方讲起&#xff0c;一个常用的for循环结构是由for循环结构框图、循环次数、循环计数(i)三部分组成…

04 单链表

目录 链表的概念和结构单链表OJ练习 1. 链表的概念和结构 1.1 链表的概念 链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的 1.从上图可以看出链式结构在逻辑上是连续的&#xff0c;物理上不一定连续 2.现…

html 会跳舞的时间动画特效

下面是是代码&#xff1a; <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns"http://www.w3.org/1999/xhtml"> <head> <meta h…

文件操作(上)

目录 文件的必要性&#xff1a; 文件分类&#xff1a; 程序文件&#xff1a; 数据文件&#xff1a; 文件的打开与关闭&#xff1a; fopen函数分析: ​编辑 FILE*: char*filename: char*mode: fclose函数&#xff1a; 应用&#xff1a; 文件编译 Fgetc Fputc 应用…

大模型微调实战笔记

大模型三要素 1.算法&#xff1a;模型结构&#xff0c;训练方法 2.数据&#xff1a;数据和模型效果之间的关系&#xff0c;token分词方法 3.算力&#xff1a;英伟达GPU&#xff0c;模型量化 基于大模型对话的系统架构 基于Lora的模型训练最好用&#xff0c;成本低好上手 提…

项目实战————苍穹外卖(DAY11)

苍穹外卖-day11 课程内容 Apache ECharts 营业额统计 用户统计 订单统计 销量排名Top10 功能实现&#xff1a;数据统计 数据统计效果图&#xff1a; 1. Apache ECharts 1.1 介绍 Apache ECharts 是一款基于 Javascript 的数据可视化图表库&#xff0c;提供直观&#x…

什么是车载信息娱乐系统和集成驾驶舱

什么是车载信息娱乐系统(IVI)? “车载信息娱乐(IVI)”通过向驾驶员和乘客提供信息和娱乐&#xff0c;为驾驶提供便利和舒适。为了理解这个概念&#xff0c;有必要知道“信息娱乐”的含义。“信息娱乐”是这个市场中使用的一个词&#xff0c;它结合了“信息”和“娱乐”两个词…

恒悦sunsite博客2023年总结及2024年展望

一、2023年总结 一年如一日的坚持做好一件事并不是容易的事情&#xff0c;但是只要我们坚持下去&#xff0c;乘风破浪会有时&#xff0c;直挂云帆济沧海。   2023年是意义非凡的一年&#xff0c;年初的时候自己定下了两个目标&#xff1a;第一个是完成博客专家认证&#xff1…

Unity中URP下的SimpleLit片元着色器

文章目录 前言一、SimpleLit片元着色器大体框架1、传入 和 返回2、GPU实例化部分3、准备 BlinnPhong 光照模型计算需要的 SurfaceData4、准备 BlinnPhong 光照模型计算需要的 InputData5、进行 BlinnPhong 的计算、雾效颜色混合及透明度计算 二、准备SurfaceData1、SurfaceData…

【华为GAUSS数据库】IDEA连接GAUSS数据库方法

背景&#xff1a;数据库为华为gauss for opengauss 集中式数据库 IDEA提供了丰富的各类型数据库驱动&#xff0c;但暂未提供Gauss数据库。可以通过以下方法进行连接。 连接后&#xff0c; 可以自动检查xml文件中的sql语句是否准确&#xff0c;表名和字段名是否正确还可以直接在…

Spring+SprinMVC+MyBatis配置方式简易模板

SpringSprinMVCMyBatis配置方式简易模板代码Demo GitHub访问 ssm-tpl-cfg 一、SQL数据准备 创建数据库test&#xff0c;执行下方SQL创建表ssm-tpl-cfg /*Navicat Premium Data TransferSource Server : 127.0.0.1Source Server Type : MySQLSource Server Versio…

Docker安装配置OnlyOffice

OnlyOffice 是一款强大的办公套件&#xff0c;你可以通过 Docker 轻松安装和部署它。本文将指导你完成安装过程。 步骤 1&#xff1a;拉取 OnlyOffice Docker 镜像 首先&#xff0c;使用以下命令从 Docker Hub 拉取 OnlyOffice Document Server 镜像&#xff1a; sudo docke…

基于光口的以太网 udp 回环实验

文章目录 前言一、系统框架整体设计二、系统工程及 IP 创建三、UDP回环模块修改说明四、接口讲解五、顶层模块设计六、下载验证前言 本章实验我们通过网络调试助手发送数据给 FPGA,FPGA通过光口接收数据并将数据使用 UDP 协议发送给电脑。 提示:任何文章不要过度深思!万事万…

canvas绘制六芒星

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

正信晟锦:亲戚借了钱一直不还怎么办

在生活中&#xff0c;我们难免会遇到一些经济上的困难&#xff0c;这时候向亲戚借钱可能是一个不错的选择。然而&#xff0c;有些亲戚在借了钱之后却迟迟不还&#xff0c;这让借款人感到非常无奈。那么&#xff0c;面对这种情况&#xff0c;我们应该怎么办呢? 我们要明确自己的…