NIO - selector简单介绍

news2024/12/13 4:34:05

一 前言

        selector作为NIO当中三大组件之一,是处理NIO非阻塞模式下的核心组件,它允许一个单个线程管理多个通道。

        NIO下的阻塞模式

        因为对于阻塞模式下的NIO模式,存在很大的问题,即使在单线程下,对应的服务端也会一直进行等待客户端的连接,甚至在建立连接之后读写模式下也会阻塞,这就导致只能让当前访问结束之后才能进行下一个客户端的访问,无法并行访问。这里我们主要介绍

        NIO下的非阻塞模式

        对于NIO下的非阻塞模式,我们只需要对于channel通道关闭阻塞模式即可。

        //1。注册连接 创建连接通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //1.1 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);

                Server层代码:

public class Server {
    private static final Logger log = LoggerFactory.getLogger(Server.class);
    //创建集合,存储对应的客户端信息
    public static ArrayList<SocketChannel> socketChannels = new ArrayList<>();
    public static void main(String[] args) throws IOException {
        //0.设置byteBuffer缓冲区存储数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //1。注册连接 创建连接通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //1.1 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //2.设置监听端口
        serverSocketChannel.bind(new InetSocketAddress(8080));
        while (true) {
            log.debug("connecting");
            //3.创建与客户端之间的连接 每有一个客户端连接,都会从这里进行监听
            //3.1 非阻塞模式下,说客户端与服务端连接的建立在这里不会堵塞,会直接通行,但是这里如果没有对应的客户端访问
            //那么返还值就为NULL,根据这个我们可以加一些判断对于这些空值进行处理
            SocketChannel accept = serverSocketChannel.accept();
            if (accept != null) {
                //添加数据
                socketChannels.add(accept);
            }
            for (SocketChannel socketChannel : socketChannels) {
                //获取数据
                log.debug("准备读取数据了!");
                //在非阻塞模式下,这里的读取也不会再停止,对应的会继续运行,如果没有读取到数据将会返还为空
                int read = socketChannel.read(buffer.flip());
                //读取数据
                buffer.flip();
                if (read!=0){
                    log.debug(String.valueOf(buffer));
                    log.debug("数据读取完毕");
                }
                //清空数据变为读取 清空数据
                buffer.clear();
            }
        }
    }
}

                Client层代码: 


//客户端
public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建连接通道
        SocketChannel socketChannel = SocketChannel.open();
        //2.设置连接服务器的地址
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        System.out.println("waiting for connection");
        socketChannel.write(Charset.defaultCharset().encode("Hello World!"));
    }
}

                Tips: 在非阻塞模式下有两点需要注意, 在服务端与客户端之间创建连接的时候,如果当前没有服务端连接,返还的值变为NULL,如下操作

            //那么返还值就为NULL,根据这个我们可以加一些判断对于这些空值进行处理
            SocketChannel accept = serverSocketChannel.accept();

                另一方面,在进行读取操作的时候也是一样,如果对应的客户端没有发送消息,读取的数据就会为0,因此我们可以在此基础上添加一些判断条件,如下              

  int read = socketChannel.read(buffer.flip());
                //读取数据
                buffer.flip();
                if (read!=0){
                    log.debug(String.valueOf(buffer));
                    log.debug("数据读取完毕");
                }

                缺点:但是NIO非阻塞模式解决了阻塞模式下各种操作执行之间的阻塞关系,不会因为当前没有客户端连接而阻塞,换为一直都在执行当中。但是同时的也带来了一定的问题:一直循环不断的连接(accept)与读(read),如果我们一直都没有客户端连接,那么就会造成CPU资源的浪费,即使没有数据读写,也会让CPU一直处于资源消耗中~

二 Slector模式

        在我之前的博客当中有对于NIO一些基础知识的介绍,也有关Selector这方面的介绍,多家对比,大家可以去看看呦 ^ - ^

Netty - NIO基础学习-CSDN博客

        这里我就直接写一个比较基础的Selector代码,其中先不包含读写,仅仅包含如何使用Selector进行与客户端之间建立连接,以及如何监听客户端,让客户端与服务端之间建立连接

        Server层:

public class Server {
    private static final Logger log = LoggerFactory.getLogger(Server.class);
    //创建集合,存储对应的客户端信息
    public static void main(String[] args) throws IOException {
        //1.创建Selector
        Selector selector = Selector.open();
        //2.创建服务端通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8080));
        //3.创建客户端与Selector之间的连接,将两者之间建立连接
        SelectionKey sscKey = ssc.register(selector, 0, null);
        //4.建立连接之后就需要绑定对应的channel的事件类型,事件类型包括四种:accept connect read write 是哪一种事件需要我们自己进行绑定
        //这里我们这个SelectionKey作为管理员只需要关注对应的客户端是否建立连接即可
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        while (true) {
            //5.使用select进行检查,如果没有事件发生就在这里阻塞,有事件发生才会继续进行 这里类似一个监听器,如果有连接这种事件发生才会执行之后的操作
            selector.select();
            //6.使用迭代器处理发生的事件 selectKeys当中会存储所有的KEY,这里如果我们想要对其进行更多的操作,例如删除。那么就必须使用到迭代器
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                //获取KEY
                SelectionKey key = iterator.next();
                log.debug("Selected key: {}", key);
                //6.使用KEY获取对应的SSC之后创建连接
                ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                channel.accept();
                log.debug("Accepted connection");
            }
        }
    }
}

        Cilent层:

//客户端
public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建连接通道
        SocketChannel socketChannel = SocketChannel.open();
        //2.设置连接服务器的地址
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        System.out.println("waiting for connection");
        socketChannel.write(Charset.defaultCharset().encode("Hello World!"));
    }
}

        在使用Selector的时候需要注意几点:

        1.建立与channel之间的关联

                首先便是建立起Selector跟对应的Channel之间的关联

                我们需要使用到ServerSocketChannel下的注册,用以建立关联

//3.创建客户端与Selector之间的连接,将两者之间建立连接
        SelectionKey sscKey = ssc.register(selector, 0, null);

        2.SelectionKey绑定对应的channel事件

                 这里先简单说一下channel的几个事件类型,主要有:

                        accept: 建立客户端与服务端之间的连接

                        connect: 客户端与服务端连接建立之后自动触发的

                        read: 读操作

                        write: 写操作

                当前的selector只需要作为一个管理员,管理对应其自己的事件即可,所以我们需要设置与对应KEY的关联channel事件类型,如下图:

                设定完成之后,如果触发了对应的事件,选择器就会监听到

        3.触发事件select()

                这里我们需要用到selector的核心方法 - select()方法。

                select方法会处于阻塞状态,除非 :

                        1> 已注册通道好的已经开始发送I/O请求

                        2>线程中断

                        3>当前的选择器Slector已被关闭

                select有一个返回值,代表的是当前选择器当中已经准备好I/O请求的通道个数

                也就是说,客户端向服务端发送请求的时候,非阻塞状态才会被激活。

                select成功激活之后,会将当前检测到的事件的SelectKey放进迭代器当中

                但是迭代器当中的数据是不会自动删除的,这一点很重要

                建立连接之后执行的代码逻辑如下:

        while (true) {
            //5.使用select进行检查,如果没有事件发生就在这里阻塞,有事件发生才会继续进行 这里类似一个监听器,如果有连接这种事件发生才会执行之后的操作
            selector.select();
            //6.使用迭代器处理发生的事件 selectKeys当中会存储所有的KEY,这里如果我们想要对其进行更多的操作,例如删除。那么就必须使用到迭代器
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                //获取KEY
                SelectionKey key = iterator.next();
                log.debug("Selected key: {}", key);
                //6.使用KEY获取对应的SSC之后创建连接
                ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                channel.accept();
                log.debug("Accepted connection");
            }
        }

三 selector处理读写

        我们需要谨记一个概念,一个Selector当中可以存储多个KEY,那么实际上读写操作也就是再创建一个KEY放入Selector当中,并设置对应的事件类型即可!

        上文提到,只要有对应的事件触发,那么select就会将其放置到迭代器的循环当中,也就是说所有事件类型的KEY,都会被存放在其中,但是不同的事件类型实际上执行的代码是不一样的,所以我们需要在迭代循环的时候根据KEY的事件类型不同进行区分

        综上,我们改良之后的Server代码如下:

public class Server {
    private static final Logger log = LoggerFactory.getLogger(Server.class);
    //创建集合,存储对应的客户端信息
    public static void main(String[] args) throws IOException {
        //1.创建Selector
        Selector selector = Selector.open();
        //2.创建服务端通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8080));
        //3.创建客户端与Selector之间的连接,将两者之间建立连接
        SelectionKey sscKey = ssc.register(selector, 0, null);
        //4.建立连接之后就需要绑定对应的channel的事件类型,事件类型包括四种:accept connect read write 是哪一种事件需要我们自己进行绑定
        //这里我们这个SelectionKey作为管理员,只需要关注对应的客户端是否建立连接即可
        //我们设置当前的KEY用来专门管理客户端的连接 accept()
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        while (true) {
            //5.使用select进行检查,如果没有事件发生就在这里阻塞,有事件发生才会继续进行 这里类似一个监听器,如果有连接这种事件发生才会执行之后的操作
            selector.select();
            //6.使用迭代器处理发生的事件 selectKeys当中会存储所有的KEY,这里如果我们想要对其进行更多的操作,例如删除。那么就必须使用到迭代器
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                //获取KEY
                SelectionKey key = iterator.next();
                //判断对应的KEY的类型
                if (key.isAcceptable()) {
                    log.debug("Accept Selected key: {}", key);
                    //6.使用KEY获取对应的SSC之后创建连接
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    //创建连接,返还客户端连接通道
                    SocketChannel sc = serverSocketChannel.accept();
                    sc.configureBlocking(false);
                    SelectionKey ssKey = sc.register(selector, SelectionKey.OP_READ);
                    //绑定事件
                    ssKey.interestOps(SelectionKey.OP_READ);
                    log.debug("Accept connection");
                } else if (key.isReadable()) {
                    //如果对应的KEY是读取类型的
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(buffer);
                    buffer.flip();
                    System.out.println(buffer);
                    buffer.compact();
                }

            }
        }
    }
}

        其实上面的代码没有变化,知识迭代的过程代码发生变化:

        1>对读(Read)操作开创新的KEY

                在连接之后,又注册了当前通道的KEY,设置其事件类型为READ,并且将其交给selector管理

if (key.isAcceptable()) {
                    log.debug("Accept Selected key: {}", key);
                    //6.使用KEY获取对应的SSC之后创建连接
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    //创建连接,返还客户端连接通道
                    SocketChannel sc = serverSocketChannel.accept();
                    sc.configureBlocking(false);
                    SelectionKey ssKey = sc.register(selector, SelectionKey.OP_READ);
                    //绑定事件
                    ssKey.interestOps(SelectionKey.OP_READ);
                    log.debug("Accept connection");
                }

        2>新增有关读取数据的操作

else if (key.isReadable()) {
                    //如果对应的KEY是读取类型的
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(buffer);
                    buffer.flip();
                    System.out.println(buffer);
                    buffer.compact();
                }

                但是以上代码还是存在弊端,运行之后,发现报错

               分析一下案发现场:

               我们上面提到,执行select之后,变为非阻塞状态,==》 之后会将对应的事件的KEY交给下边的迭代器集合。这里我们的客户端发送了连接申请,并且写入了数据。

                1.那么我们的服务器检测到事件之后,将连接申请相关事件的KEY提交给selectKeys的集合当中(也就是迭代器集合)

                2.类型匹配,匹配到了key.isAcceptable(),进入并且创建连接,又新增一个KEY,绑定读操作事件,当前if结束

                3.循环回到accept(),检测到写操作,之后将写操作的KEY提交给selectKeys当中

                4.类型匹配,我们发现,之前已经完成过的事件,也就是连接事件依旧存在于迭代循环当中!但是我们这个事件已经处理结束!因此,accept()之后的数据为NULL

                真相大白,其实就是因为我们没有删除对应在selectKeys集合当中已执行的KEY所导致的。      

                这也就是为什么必须使用iterator.remove()的原因了。

                在一开始迭代就删除当前的元素即可。

while (iterator.hasNext()) {
                //在一开始执行就直接移除这个KEY
                iterator.remove();
                //获取KEY
                SelectionKey key = iterator.next();
                //判断对应的KEY的类型
                if (key.isAcceptable()) {
                    log.debug("Accept Selected key: {}", key);
                    //6.使用KEY获取对应的SSC之后创建连接
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    //创建连接,返还客户端连接通道
                    SocketChannel sc = serverSocketChannel.accept();
                    sc.configureBlocking(false);
                    SelectionKey ssKey = sc.register(selector, SelectionKey.OP_READ);
                    //绑定事件
                    ssKey.interestOps(SelectionKey.OP_READ);
                    log.debug("Accept connection");
                } else if (key.isReadable()) {
                    //如果对应的KEY是读取类型的
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(buffer);
                    buffer.flip();
                    System.out.println(buffer);
                    buffer.compact();
                }

            }

今天写这个写了不少时间,明日继续更新,大家是不是都要考六级了呢 = -- = 艾

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

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

相关文章

二、部署docker

二、安装与部署 2.1 安装环境概述 Docker划分为CE和EE&#xff0c;CE为社区版&#xff08;免费&#xff0c;支持周期三个月&#xff09;&#xff0c;EE为企业版&#xff08;强调安全&#xff0c;付费使用&#xff09;。 Docker CE每月发布一个Edge版本&#xff08;17.03&…

Camp4-L2:LMDeploy 量化部署进阶实践

书生浦语大模型实战营第四期&#xff1a;LMDeploy 量化部署进阶实践 教程链接&#xff1a;https://github.com/InternLM/Tutorial/tree/camp4/docs/L2/LMDeploy视频链接&#xff1a;https://www.bilibili.com/video/BV18aUHY3EEG/?vd_sourceb96c7e6e6d1a48e73edafa36a36f1697…

Qt之第三方库QCustomPlot使用(二)

Qt开发 系列文章 - qcustomplot&#xff08;二&#xff09; 目录 前言 一、Qt开源库 二、QCustomPlot 1.qcustomplot介绍 2.qcustomplot下载 3.qcustomplot移植 4.修改项目文件.pro 5.提升QWidget类‌ 三、技巧讲解 1.拖动缩放功能 2.等待更新 总结 前言 Qt第三方…

python数据分析之爬虫基础:selenium详细讲解

目录 1、selenium介绍 2、selenium的作用&#xff1a; 3、配置浏览器驱动环境及selenium安装 4、selenium基本语法 4.1、selenium元素的定位 4.2、selenium元素的信息 4.3、selenium元素的交互 5、Phantomjs介绍 6、chrome handless模式 1、selenium介绍 &#xff08;1…

LearnOpenGL学习(模型加载 -- Assimp,网格,模型)

完整代码见&#xff1a;zaizai77/Cherno-OpenGL: OpenGL 小白学习之路 Assimp 3D建模工具如Blender、3DS Max在导出模型文件时&#xff0c;会自动生成所有的顶点坐标、顶点法线和纹理坐标。 .obj 格式只包含了模型数据和材质信息&#xff08;颜色、贴图等&#xff09; Assi…

qtcanpool 知 08:Docking

文章目录 前言口味改造后语 前言 很久以前&#xff0c;作者用 Qt 仿照前端 UI 设计了一个 ministack&#xff08;https://gitee.com/icanpool/qtcanpool/blob/release-1.x/src/libs/qcanpool/ministack.h&#xff09; 控件&#xff0c;这个控件可以折叠。部分用户体验后&#…

【Linux】文件管理必备知识和基本指令

【Linux】文件管理必备知识和基本指令 什么是操作系统什么是文件什么是路径01. ls 指令02. pwd命令03. cd 指令04. touch指令05.mkdir指令&#xff08;重要&#xff09;&#xff1a;06.rmdir指令 && rm 指令&#xff08;重要&#xff09;&#xff1a;rmdir指令rm指令 0…

R155 VTA 认证对汽车入侵检测系统(IDS)合规要求

续接上集“浅谈汽车网络安全车辆型式认证&#xff08;VTA&#xff09;的现状和未来发展”&#xff0c;有许多读者小伙伴有联系笔者来确认相关的R155 VTA网络安全审核要求&#xff0c;基于此&#xff0c;笔者将针对 R155 VTA 每一条网络安全审核细则来具体展开。 今天就先从汽车…

【PHP项目实战】活动报名系统

目录 项目介绍 开发语言 后端 前端 项目截图&#xff08;部分&#xff09; 首页 列表 详情 个人中心 后台管理 项目演示 项目介绍 本项目是一款基于手机浏览器的活动报名系统。它提供了一个方便快捷的活动报名解决方案&#xff0c;无需下载和安装任何APP&#xff0c…

【数据分享】1901-2023年我国省市县三级逐年最低气温数据(Shp/Excel格式)

之前我们分享过1901-2023年1km分辨率逐月最低气温栅格数据和Excel和Shp格式的省市县三级逐月最低气温数据&#xff0c;原始的逐月最低气温栅格数据来源于彭守璋学者在国家青藏高原科学数据中心平台上分享的数据&#xff01;基于逐月栅格数据我们采用求年平均值的方法得到逐年最…

使用伪装IP地址和MAC地址进行Nmap扫描

使用伪装IP地址和MAC地址进行Nmap扫描 在某些网络设置中&#xff0c;攻击者可以使用伪装的IP地址甚至伪装的MAC地址进行系统扫描。这种扫描方式只有在可以保证捕获响应的情况下才有意义。如果从某个随机的网络尝试使用伪装的IP地址进行扫描&#xff0c;很可能无法接收到任何响…

【趣题分享】赤壁之战每日演兵(原诸葛亮列传兵法题)求解算法

文章目录 序言1 求解算法代码&#xff08;python&#xff09;2 思路细节2.1 定义拼图与阵型2.2 穷举复杂度2.3 使用缓存进行改进&#xff08;&#xff09;2.3.1 LRU缓存2.3.2 将2.2的solve函数改写为可缓存装饰的 2.4 使用剪枝进行改进&#xff08;&#xff09;2.5 使用更好的状…

Java项目实战II基于微信小程序的私家车位共享系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 随着城市化进程的加速&…

STM32 实现 TCP 服务器与多个设备通信

目录 一、引言 二、硬件准备 三、软件准备 四、LWIP 协议栈的配置与初始化 五、创建 TCP 服务器 1.创建任务以及全局变量 2.创建 TCP 控制块 3.绑定端口 4. 进入监听状态 5.设置接收回调函数 六、处理多个客户端连接 七、总结 一、引言 在嵌入式系统开发中&…

LobeChat-46.6k星!顶级AI工具集,一键部署,界面美观易用,ApiSmart 是你肉身体验学习LLM 最好IDEA 工具

LobeChat LobeChat的开源&#xff0c;把AI功能集合到一起&#xff0c;真的太爽了。 我第一次发现LobeChat的时候&#xff0c;就是看到那炫酷的页面&#xff0c;这么强的前端真的是在秀肌肉啊&#xff01; 看下它的官网&#xff0c;整个网站的动效简直闪瞎我&#xff01; GitH…

计算机键盘的演变 | 键盘键名称及其功能 | 键盘指法

注&#xff1a;本篇为 “键盘的演变及其功能” 相关几篇文章合辑。 英文部分机翻未校。 The Evolution of Keyboards: From Typewriters to Tech Marvels 键盘的演变&#xff1a;从打字机到技术奇迹 Introduction 介绍 The keyboard has journeyed from a humble mechanical…

第三部分:进阶概念 7.数组与对象 --[JavaScript 新手村:开启编程之旅的第一步]

第三部分&#xff1a;进阶概念 7.数组与对象 --[JavaScript 新手村&#xff1a;开启编程之旅的第一步] 在 JavaScript 中&#xff0c;数组和对象是两种非常重要的数据结构&#xff0c;它们用于存储和组织数据。尽管它们都属于引用类型&#xff08;即它们存储的是对数据的引用而…

面试中遇到的一些有关进程的问题(有争议版)

一个进程最多可以创建多少个线程&#xff1f; 这个面经很有问题&#xff0c;没有说明是什么操作系统&#xff0c;以及是多少位操作系统。 因为不同的操作系统和不同位数的操作系统&#xff0c;虚拟内存可能是不一样多。 Windows 系统我不了解&#xff0c;我就说说 Linux 系统…

Excel技巧:如何批量调整excel表格中的图片?

插入到excel表格中的图片大小不一&#xff0c;如何做到每张图片都完美的与单元格大小相同&#xff1f;并且能够根据单元格来改变大小&#xff1f;今天分享&#xff0c;excel表格里的图片如何批量调整大小。 方法如下&#xff1a; 点击表格中的一个图片&#xff0c;然后按住Ct…

Stable Audio Open模型部署教程:用AI打造独家节拍,让声音焕发新活力!

Stable Audio Open 是一个开源的文本到音频模型&#xff0c;允许用户从简单的文本提示中生成长达 47 秒的高质量音频数据。该模型非常适合创建鼓点、乐器即兴演奏、环境声音、拟音录音和其他用于音乐制作和声音设计的音频样本。用户还可以根据他们的自定义音频数据微调模型&…