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

news2024/9/29 15:24:02

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,管理多个channel
        Selector 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为本机ip
        while (true){
            //3. select 方法,发生了上述事件才会向下继续执行,否则阻塞
            // selector 在事假未被处理时会将事件重新加入,因此一个事件要么处理要么取消,不能置之不理
            selector.select(); // 如果没有事件则阻塞
            //4. 处理事件 , 获取所有发生的事件
            // 获取所有注册的channel的key,可以拿到key访问channel
            Set<SelectionKey> selectionKeys = selector.selectedKeys(); 
            Iterator<SelectionKey> iter = selectionKeys.iterator();
            while (iter.hasNext()){
                SelectionKey selectionKey = iter.next();
                iter.remove(); // 拿到了立即移除
                log.debug("Key:"+selectionKey);

                if(selectionKey.isAcceptable()){
                    // 如果是连接就绪事件,那就获取对应的ServerSocketChannel,然后在接受获得可以数据传输的SocketChannel
                    // 通过key获取channel
                    ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel(); 
                    SocketChannel sc = channel.accept(); // 前面设置了非阻塞,没有连接就返回null
                    sc.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);
                    channel.read(buffer);
                    buffer.flip();
                    CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer);
                    String msg = charBuffer.toString();
                    System.out.println("客户端发来的信息:"+msg);
                }
            }
            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();
    }
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微分方程(1)微分方程的历史

总之就是学习吧~ Introduction Without knowing something about differential equations and methods of solving them, it is difficult to appreciate the history of this important branch of mathematics. Further, the development of differential equations is inti…

如何用GOWIN创建FPGA工程

高云FPGA如何创建工程 第一步&#xff1a;安装Gowin软件&#xff0c;这个在高云的官网是可以下载的 第二步&#xff1a;点开这个软件&#xff0c;点击“New Project…” 点击“Next” 选择自己对应的器件型号&#xff1a; 工程创建成功&#xff0c;如下图&#xff1a; 最后将…

给程序加个进度条吧!1行Python代码,快速搞定~

你在写代码的过程中&#xff0c;有没有遇到过以下问题&#xff1f; 已经写好的程序&#xff0c;想看看程序执行的进度&#xff1f; 在写代码批量处理文件的时候&#xff0c;如何显示现在处理到第几个文件了&#xff1f; &#x1f446;如上图所示的进度条是一个最好的解决方法…

如何解决分支机构无法连入总部采购管理系统的难题

案例背景&#xff1a; 某企业业务规模不断壮大&#xff0c;内部采购流程越发复杂&#xff0c;供应商资质情况各异难以管理&#xff0c;为提高内部采购效率和采购品质&#xff0c;优化供应链管理&#xff0c;确保采购环节公正透明可溯&#xff0c;该企业集中化部署了采购管理系…

高级RAG(八): 自动合并检索(Auto-merging Retrieval)

自动合并检索(Auto-merging Retrieval)是LlamaIndex的另外一种高级RAG技术&#xff0c;它有点类似与我们之间介绍的从小到大的检索&#xff0c;不过自动合并检索要比“从小到大的检索”稍微复杂一些&#xff0c;它首先将文档按一定的层次结构进行切割&#xff0c;然后在检索的时…

北斗卫星:助力社区矫正人员追踪与管理的科技突破

北斗卫星&#xff1a;助力社区矫正人员追踪与管理的科技突破 社区矫正人员是一个重要的社会群体&#xff0c;他们的安全和管理对于社会的和谐稳定至关重要。随着技术的飞跃发展&#xff0c;北斗卫星系统作为我国自主研发的卫星导航系统&#xff0c;正逐渐在社区矫正工作中发挥…

UE5 播放rtsp监控视频

1. 插件下载 https://github.com/inveta/InVideo https://github.com/inveta/InVideo/releases https://download.csdn.net/download/qq_17523181/88760489?spm1001.2014.3001.5501 插件目前支持5.1 / 5.0 2. 建立C UE5项目 重要&#xff1a;此插件支持C项目&#xff0c;不然不…

Windows Server搭建DNS服务器

服务器规划 DNS服务器IP为&#xff1a;192.168.2.82 需要自定义域名解析 192.168.2.82 dns.zhangsan.com 192.168.2.181 www.zhangsan.com 192.168.2.182 test1.zhangsan.com 192.168.2.183 test2.zhangsan.com 0. 服务器系统信息 1. 服务器初始化 1.1 查询IP 在命令行窗口中…

【React基础】– JSX语法

文章目录 认识JSX为什么React选择了JSXJSX的使用 React事件绑定this的绑定问题事件参数传递 React条件渲染React列表渲染列表中的key JSX的本质createElement源码Babel官网查看直接编写jsx代码 虚拟DOM的创建过程jsx – 虚拟DOM – 真实DOM声明式编程 阶段案例练习 认识JSX ◼ …

【重学C语言】四、常量、变量和基本数据类型

【重学C语言】四、常量、变量和基本数据类型 1. 常量和变量1.1 常量1.1.1 直接常量1.1.1.1 整数常量1.1.1.2 浮点常量1.1.1.3 字符常量1.1.1.4字符串常量 1.1.2符号常量 1.2 标识符1.3 变量1.3.1 变量的定义1.3.2 左值&#xff08;Lvalues&#xff09;和右值&#xff08;Rvalue…

a-range-picker获取开始和结束时间

效果图 代码块 <a-range-picker change"onChangeRangeDate" format"YYYY-MM-DD"/>onChangeRangeDate(value,dateString){this.startDatedateString[0]this.endDatedateString[1]}

抓包工具Fiddler的下载、安装、配置、基本使用

文章目录 前言一、Fiddler下载二、Fiddler安装三、打开Fiddler四、Fiddler显示菜单栏及菜单栏功能基本介绍五、Fiddler的作用六、Fiddler的工作原理七、Fiddler基本功能使用1.导包2.规则设置Rules&#xff1a; 八、Fiddler设置抓取HTTPS的包九、抓取APP包必须设置十、Fiddler过…

谷达冠楠科技:抖音开网店新手小白可以卖的产品

随着互联网的发展&#xff0c;越来越多的人选择在网上开设自己的店铺。而抖音作为目前最火的短视频平台&#xff0c;也提供了开店的功能。那么&#xff0c;对于新手小白来说&#xff0c;抖音开网店可以卖哪些产品呢? 我们可以考虑的是服装类商品。抖音上有很多时尚博主&#x…

极致画质与流畅播放的完美结合,只在ProVideoPlayer for Mac!

ProVideoPlayer for Mac 是一款功能强大的专业级视频播放软件&#xff0c;旨在提供出色的用户体验和无与伦比的功能。以下是它的一些主要功能介绍&#xff1a; 多格式兼容&#xff1a;ProVideoPlayer for Mac 支持广泛的视频格式&#xff0c;包括常见的MP4、AVI、MOV&#xff0…

鸿蒙开发(五)鸿蒙UI开发概览

从用户角度来讲&#xff0c;一个软件拥有好看的UI&#xff0c;那是锦上添花的事情。再精确的算法&#xff0c;再厉害的策略&#xff0c;最终都得通过UI展现给用户并且跟用户交互。那么&#xff0c;本篇一起学习下鸿蒙开发UI基础知识&#xff0c;认识下各种基本控件以及使用方式…

基于Java图书商城系统设计与实现(源码+部署文档)

博主介绍&#xff1a; ✌至今服务客户已经1000、专注于Java技术领域、项目定制、技术答疑、开发工具、毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅 &#x1f447;&#x1f3fb; 不然下次找不到 Java项目精品实…

三星Galaxy S24 系列了发布会预热新品:Galaxy Ring 智能戒指亮相

在三星 Galaxy S24 系列发布会上&#xff0c;他们不仅带来了新款手机&#xff0c;还意外地推出了一款智能指环 ——Galaxy Ring&#xff01;没错&#xff0c;就是戴在手指上的那种智能设备&#xff01; 在发布会的尾声&#xff0c;三星放出了一段简短的预告片&#xff0c;让我…

美摄视频SDK的HDR格式编辑方案

在当今的视觉媒体时代&#xff0c;高动态范围&#xff08;HDR&#xff09;技术已成为高质量视频内容的标配。为了满足企业对高效、高质量视频处理的需求&#xff0c;美摄科技推出了业界领先的视频SDK&#xff0c;全面支持多种HDR标准的图像视频进行处理。 一、核心优势 HDR全…

【Docker】安装nacos以及实现负载均衡

&#x1f973;&#x1f973;Welcome 的Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Docker的相关操作吧 目录 &#x1f973;&#x1f973;Welcome 的Huihuis Code World ! !&#x1f973;&#x1f973; 前言 一.nacos单个部署 1.镜像拉取 …

Java实现厦门旅游电子商务预订系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 景点类型模块2.2 景点档案模块2.3 酒店管理模块2.4 美食管理模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 学生表3.2.2 学生表3.2.3 学生表3.2.4 学生表 四、系统展示五、核心代码5.1 新增景点类型5.2 查询推荐的…