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,管理多个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技术实现的 。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一文说明白 MySQL 的 ACID 和 几种日志的关系

1、简介 我们对于MySQL 很熟悉&#xff0c;关于其特性都有一定的了解&#xff0c;但是关于一些具体的实现原理&#xff0c;有的小伙伴可能不太熟悉&#xff0c;而且这部分知识在我们互联网大厂面试中是经常涉及的&#xff0c;因此&#xff0c;本文将带你深入底层&#xff0c;顺…

实验笔记之——基于TUM-RGBD数据集的SplaTAM测试

之前博客对SplaTAM进行了配置&#xff0c;并对其源码进行解读。 学习笔记之——3D Gaussian SLAM&#xff0c;SplaTAM配置&#xff08;Linux&#xff09;与源码解读-CSDN博客SplaTAM全称是《SplaTAM: Splat, Track & Map 3D Gaussians for Dense RGB-D SLAM》&#xff0c;…

c JPEG 1D DCT

步骤&#xff1a; 1. 对yuv 88 数据 8行分别1D DCT 2, 用8行 1D DCT 得到的数据生成中间88 块 Zj 3,对Zj 的8列再 1D DCT 后生成8列,用这8列组合成8*8的2D DCT 系数 准备用此1D DCT程序代替以前写的2D DCT,看能减少多少编码时间。 看网上文章&#xff0c;ffmpeg用…

【AI预测】破晓未来教育市场:如何精准定位、精选师资并启动高潜力培训项目

在当前全球化和技术快速迭代的背景下&#xff0c;各行业正面临巨大的人才缺口和新的发展机遇。 全球化浪潮&#xff0c;各行业如同搭乘上了一列高速列车&#xff0c;不断深入探索并广泛应用AI技术以提升产业效率、创新服务模式。在智能制造领域&#xff0c;工业4.0时代犹如给…

【Java 设计模式】结构型之适配器模式

文章目录 1. 定义2. 应用场景3. 代码实现结语 适配器模式&#xff08;Adapter Pattern&#xff09;是一种结构型设计模式&#xff0c;用于将一个类的接口转换成客户端期望的另一个接口。这种模式使得原本由于接口不兼容而不能一起工作的类可以一起工作。在本文中&#xff0c;我…

骨传导蓝牙耳机怎么使用?使用骨传导耳机对人体有没有伤害?

骨传导蓝牙耳机的使用方法和传统的入耳式蓝牙耳机使用方法相差无几&#xff0c;都是通过蓝牙来进行连接使用&#xff0c;但骨传导耳机会自带内存&#xff0c;所以在此前提上可以存储音乐独立使用&#xff0c;比传统的入耳式蓝牙耳机使用更方便一些。 那么使用骨传导耳机会不会对…

[一]ffmpeg音视频解码

[一]ffmpeg音视频解码 一.编译ffmpeg1.安装vmware虚拟机2.vmware虚拟机安装linux操作系统3.安装ftp和fshell软件4.在Ubuntu&#xff08;Linux&#xff09;中编译Android平台的FFmpeg&#xff08; arm和x86 &#xff09;5.解压FFmpeg6.Android编译脚本&#xff08;1&#xff09;…

Spring Security工作原理(一)

过滤器 Spring Security的Servlet支持是基于Servlet过滤器的&#xff0c;因此首先了解过滤器的一般作用是很有帮助的。下图显示了单个HTTP请求处理程序的典型分层结构。 处理客户端发送的请求时&#xff0c;容器创建一个FilterChain&#xff0c;其中包含Filter实例和Servlet&a…

Rust-泄漏

在C中&#xff0c;如果引用计数智能指针出现了循环引用&#xff0c;就会导致内存泄漏。而Rust中也一样存在引用计数智能指针Rc,那么Rust中是否可能制造出内存泄漏呢? 内存泄漏 首先&#xff0c;我们设计一个Node类型&#xff0c;它里面包含一个指针&#xff0c;可以指向其他…

C++类与对象【友元】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;C从基础到进阶 &#x1f384;1 友元&#x1f951;1.1 全局函数做友元&#x1f951;1.2 类做友元&#x1f951;1.3 成员函数做友元 &#x1f56e;2 总结 &#x1f384;1 友元 生活中你的家…

xml裁剪标注目标并外扩

import glob import xml.etree.ElementTree as ET import cv2 from PIL import Image import os def change_xmlfile(path)

HCIA-H12-811题目解析(11)

1、下列哪个属性不能作为衡量COST的参数&#xff1f; 2、RSTP协议使用P/A机制加快了上游端口转到Forwarding状态的速度&#xff0c;但是却没有出现临时环路的原因是什么&#xff1f; 3、网络管理员在三层交换机上创建了VLAN10&#xff0c;并在该VLAN的虚拟接口下配置了IP地址…

EtherNet/IP协议开发2:在ubuntu测试

下载源码&#xff1a; git clone https://github.com/EIPStackGroup/OpENer编译 首先进入目录 /big/opener/OpENer/bin/posix 执行脚本&#xff1a; lkmaoubuntu:/big/opener/OpENer/bin/posix$ ./setup_posix.sh 执行make lkmaoubuntu:/big/opener/OpENer/bin/posix$ mak…

基于arcgis js api 4.x开发点聚合效果

一、代码 <html> <head><meta charset"utf-8" /><meta name"viewport"content"initial-scale1,maximum-scale1,user-scalableno" /><title>Build a custom layer view using deck.gl | Sample | ArcGIS API fo…

SPEC CPU 2017 quick start

SPEC CPU 2017 quick start 我这里选择在 linux&#xff08;ubuntu22.04.3&#xff09; 上安装 SPEC CPU 2017&#xff0c;gcc、g、gfortran 均使用 sudo apt install xxx 安装&#xff08;其版本为11.4.0&#xff09; 官方的 SPEC CPU 2017 在 Unix Systems 安装示例&#x…

2024-01-18 在Android Studio中,可以通过修改build.gradle文件(位于你的应用模块目录下)来自定义生成的APK名称

一、在Android Studio中&#xff0c;可以通过修改build.gradle文件&#xff08;位于你的应用模块目录下&#xff09;来自定义生成的APK名称&#xff0c;在build.gradle里面增加下面的代码 applicationVariants.all { variant ->variant.outputs.all {outputFileName "…

349. 两个数组的交集(力扣)(OJ题)

题目链接&#xff1a;349. 两个数组的交集 - 力扣&#xff08;LeetCode&#xff09; 个人博客主页&#xff1a;https://blog.csdn.net/2301_79293429?typeblog 专栏&#xff1a;https://blog.csdn.net/2301_79293429/category_12545690.html 给定两个数组 nums1 和 nums2 &a…

json-server的基础使用

json-server 是什么? 用来快速搭建模拟的 REST API 的工具包 可以30秒内快速为我们搭建一个假的基于 REST API的服务 我们要如何使用呢&#xff1f; 1.先安装 //全局安装 npm i -g json-server 2.创建文件 db.json 我们需要在db.json放入一点内容 放入示例&#xff1a; {/…

用 Python 制作可视化 GUI 界面,一键实现证件照背景颜色的替换

今天&#xff0c;我们来分享一下如何通过Python的十来行代码来替换证件照的背景颜色&#xff0c;那么在最后&#xff0c;小编也会将上述的流程制作成一个GUI界面来方便大家使用。关于界面的大致模样其实和先前的相差不大&#xff0c;大家应该都看过上一篇的内容 界面大体的样子…

Python项目——久坐提醒定时器(PySide6)编写

1、介绍 使用Python编写一个久坐提醒软件。功能&#xff1a; 设置工作时间。设置休息时间。选择休息时是否播放音乐。休息时&#xff0c;软件置顶&#xff0c;且不能关闭。 2、工具 语言&#xff1a;python3.11UI设计工具&#xff1a;Qt designer编译器&#xff1a;PyCharm包…