Netty入门指南之NIO Selector监管

news2025/1/16 8:02:06

作者简介:☕️大家好,我是Aomsir,一个爱折腾的开发者!
个人主页:Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客
当前专栏:Netty应用专栏_Aomsir的博客-CSDN博客

文章目录

  • 参考文献
  • 前言
  • 问题解决
    • 如何解决
    • 实战编码
  • Selector详解
    • keys&selectedKeys
    • select()方法
  • 代码问题及改进
    • 问题
    • 解决
    • 代码演示
  • 拓展知识
  • 总结

参考文献

  • 孙哥suns说Netty
  • Netty官方文档

前言

在我们的上一篇文章中,我们详细讲解了如何使用NIO进行网络通信并成功解决了服务端的两次阻塞问题。这种解决方案有效地改善了通信效率。然而,引入非阻塞机制后,又产生了一个新的问题。我们注意到,在没有客户端请求和IO通信的情况下,上篇文章中的while循环会持续运行,导致CPU资源的浪费。更为复杂的是,我们的程序是单线程运行的,所有的请求接收和IO通信都由这一个线程处理,这无疑进一步拉低了CPU的利用率。

问题解决

如何解决

为了解决这个问题,我们可以引入一个“监管者”,负责监控客户端的请求和IO通信。这个“监管者”会专注于监控ServerSocketChannel的ACCEPT状态,以及SocketChannel的READWRITE状态。只有当这些状态被触发时,"监管者"才会进行处理。在NIO中,我们有一个名为Selector的组件,它可以承担这个监管者的角色。

实战编码

现在,让我们通过实战编码来看看如何实现这个解决方案。通过引入Selector,我们成功地解决了while循环空转的问题,将阻塞的责任转交给了selector。这样,我们的程序就不会再发生阻塞了。我们的selector会监控ServerSocketChannel的ACCEPT事件,监控到了ACCEPT以后就会去获取对应的客户端SocketChannel,监控它的READ和WRITE事件。 请参考以下代码和相关注释进行理解。在接下来的内容中,我们会逐步详细解释这个过程。

注意⚠️:它是一个单线程!

public class MyServer2 {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8000));

        // Selector只在非阻塞下可用
        serverSocketChannel.configureBlocking(false);

        // 引入监管者
        Selector selector = Selector.open();

        // 将ssc注册到selector上,返回一个SelectionKey,用于设置监控ACCEPT状态
        // SelectionKey: 将来事件发生后,通过它可以知道来自哪个Channel和哪个事件
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        // 监控
        while (true) {

            // 开始监控,此处会阻塞,直到监控到有客户端请求和实际的连接或读写操作才会继续往下执行
            // 监控到以后会将实际的ssc或者sc保存至 SelectionKeys(HashSet)里,然后放行
            selector.select();

            // 从监控到的SelectionKeys中获取到实际的
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();

                // 获取到后移除,防止重复处理
                iterator.remove();

                // 判断SelectionKey事件类型
                if (key.isAcceptable()) {
                    // 获取到ssc从而获取到sc
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);

                    // 将获取的sc注册到selector上,返回一个SelectionKey,用于设置监控READ状态
                    SelectionKey scKey = sc.register(selector, 0, null);
                    scKey.interestOps(SelectionKey.OP_READ);

                    System.out.println("accept = " + sc);
                } else if (key.isReadable()) {
                    try {
                        // 通过SelectionKey获取到sc,然后读取数据
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(10);
                        int read = sc.read(buffer);

                        if (-1 == read) {
                            // 客户端处理完毕
                            key.cancel();
                        } else {
                            buffer.flip();
                            System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer));
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        key.cancel();
                    }
                }
            }
        }
    }
}

Selector详解

Selector里面有很多的细节,我会带着一点点的去剖析,方便对整个程序有一个清晰的认知。

keys&selectedKeys

在Selector中,我们主要关注两种keys。

第一种是keys,这是我们在将channel注册到selector的时候获取到的SelectionKey。这个key是在注册过程中获取的,而不是由selector在监控特定事件后获取。一旦channel注册成功,这个key就会被添加到keys列表中。这个key的主要作用是为特定的channel设置需要监控的事件。

第二种是selectedKeys,这是我们在事件触发后,通过调用selectedKeys()方法获取的key,它是在事件触发后从keys列表中复制到selectedKeys列表中去的。这些selectedKeys对应的channel都是实际要发生事件的,例如ACCEPT、READ、WRITE等。所以说当我们从selectedKeys中取出一个key后要将其移出,以免出现异常

总的来说,keys列表包含了所有注册的channel和其事件信息,这是一个较大的范围。而selectedKeys列表则是一个较小的范围,它来自于keys,只包含当前实际发生事件的channel。比如我开了个直播课,有100个人报名,这100个人在keys里,实际直播的时候有80人,这80人同时在selectedKeys里
在这里插入图片描述

select()方法

Selector的select()方法是一个会产生阻塞的方法。它会定期轮询在Selector中注册的所有SelectionKey(也就是keys),并监控与这些key关联的Channel的状态,如果有对应事件发生(例如有新的连接请求,或者有数据可读/可写),则将对应的key添加到selectedKeys列表中,并放行,让程序处理这些事件。

如果在调用select()方法时没有任何事件发生,那么该方法会阻塞,直到有事件发生为止。这样可以避免程序在没有任何事件发生时不断轮询,浪费CPU资源。

如果服务端的buffer设置得太小,可能会导致服务端一次无法处理所有的数据。在这种情况下,当buffer被填满后,服务端会处理这第一部分数据,然后结束,因为这些未处理的数据会被视为新的事件。简言之就是说如果buffer需要两次才能读完客户端发送的一条数据,那这个channel会被selector监控到两次read事件

代码问题及改进

问题

  • 未处理半包与粘包问题,处理的过程中一段数据被分成了几个事件,但是每个buffer是独属某一个事件的,新的事件就是一个新的buffer,怎么解决?
  • 解决半包粘包后,如果buffer设置的小,从SocketChannel中读取的数据还没遇到\n,那buffer切换写模式压缩去等剩余数据写进来,等于白干,程序会被空转调用,怎么解决?
  • 服务端从SocketChannel已经读取完数据了,后续没有通信了,服务端没有去主动断开连接,那select岂不是每次轮询都得带着这些不会产生通信的keys?
  • 服务端没有处理异常

解决

  • 对于第一个问题,我们可以用先前的doLineSpilt方法处理半包粘包,然后我们可以给每一个SocketChannel设置一个附件(att),在注册到selector的时候进行绑定,在处理其读写事件的时候取出来使用,这样粘包粘包压缩的数据就会一直都在了(只要key没有被删除,即channel没有断开,那就是同一个Channel)
  • 对于第二个问题,我们可以在处理半包粘包后,检查一下buffer的limit和position是否相等,如果在处理半包粘包后两者相等,说明buffer里是满的,这时我们创建新的buffer进行扩容,将新buffer作为附件绑定即可
  • 对于第三个问题,客户端和服务端达成协议,比如客户端不发数据代表通信结束,那服务端从channel读不出来数据(返回值为-1)时则调用SelectionKey的cancle方法,从keys中删除
  • 对于第四个问题:处理异常就可以了

代码演示

public class MyServer4 {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8000));

        // Selector只在非阻塞下可用
        serverSocketChannel.configureBlocking(false);

        // 引入监管者
        Selector selector = Selector.open();

        // 让serverSocketChannel被selector管理,它只处理accept,所以附件为null
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);

        // 监控accept
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        System.out.println("MyServer.main");

        // 监控
        while (true) {
            selector.select();

            System.out.println("------------111-------------");

            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();

                // 取出来就从selectedKeys中删除
                iterator.remove();
                if (key.isAcceptable()) {
                    // ServerSocketChannel、获取的是最开始创建的,可以直接使用上面创建的
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);

                    // 给每个SocketChannel绑定一个buffer,监控sc状态
                    ByteBuffer buffer = ByteBuffer.allocate(7);
                    SelectionKey scKey = sc.register(selector, 0, buffer);
                    scKey.interestOps(SelectionKey.OP_READ);

                    System.out.println("accept = " + sc);
                } else if (key.isReadable()) {
                    try {
                        // 监控到key是读时间,获取到SocketChannel和buffer
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer) key.attachment();   // 获取附件中的buffer
                        int read = sc.read(buffer);

                        if (-1 == read) {
                            // 客户端处理完毕
                            key.cancel();
                        } else {
                            doLineSplit(buffer);

                            // 没有压缩动,需要扩容
                            if (buffer.position() == buffer.limit()) {
                                // 1、空间扩大
                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);

                                // 2、老缓冲区数据复制进新缓冲区
                                buffer.flip();
                                newBuffer.put(buffer);

                                // 3、绑定channel
                                // buffer = newBuffer;
                                key.attach(newBuffer);
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        key.cancel();
                    }
                }
            }
        }
    }

    private static void doLineSplit(ByteBuffer buffer) {
        buffer.flip(); // 读模式
        for (int i = 0; i < buffer.limit(); i++) {
            if (buffer.get(i) == '\n') {
                int length = i + 1 - buffer.position();  // 以免出现一行里面有多个\n
                ByteBuffer target = ByteBuffer.allocate(length);

                for (int j = 0; j < length; j++) {
                    target.put(buffer.get());
                }

                // 截取工作完成
                target.flip();
                System.out.println("StandardCharsets.UTF_8.decode(target) = " + StandardCharsets.UTF_8.decode(target));

                target.clear();
            }
        }

        // 写模式(压缩)
        buffer.compact();
    }
}

拓展知识

对于网络编程中常见的半包和粘包问题,我们有多种解决策略。一种简单且常用的方法是添加特定的标识符,如换行符\n,用于区分数据包的边界。另一种更为复杂但也更为精确的方法是采用类似HTTP协议的头体分离策略。在这种策略中,我们将数据分为头部和体部两部分。头部包含元数据信息,例如体部数据的大小等关键信息。体部则包含实际的数据内容。通过这种方式,我们可以清晰地区分每个数据包,从而有效解决半包和粘包问题。

总结

在今天的学习中,我们深入探讨了如何利用Java NIO的Selector来高效地监控我们的服务器端程序,从而避免无意义的空转。我们对Selector进行了深入剖析,透彻理解了其工作原理。进一步地,我们逐步优化了我们的程序,提高了其性能和效率。这一系列的学习和实践,为我们接下来的Netty学习铺设了坚实的基础。Netty,作为一个基于Java NIO的网络应用框架,我们对其的掌握将在未来的编程道路上发挥重要作用。

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

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

相关文章

MySQL的索引和复合索引

由于MySQL自动将主键加入到二级索引&#xff08;自行建立的index&#xff09;里&#xff0c;所以当select的是主键或二级索引就会很快&#xff0c;select *就会慢。因为有些列是没在索引里的 假设CA有1kw人咋整&#xff0c;那我这个索引只起了前一半作用。 所以用复合索引&am…

1994-2021年分行业二氧化碳排放量数据

1994-2021年分行业二氧化碳排放量数据 1、时间&#xff1a;1994-2021年 2、来源&#xff1a;原始数据整理自能源年鉴 3、指标&#xff1a;统计年度、行业代码、行业名称、煤炭二氧化碳排放量、焦炭二氧化碳排放量、原油二氧化碳排放量、汽油二氧化碳排放量、煤油二氧化碳排放…

笔记:AI量化策略开发流程-基于BigQuant平台(一)

从本文开始&#xff0c;按照AI策略开发的完整流程&#xff08;共七步&#xff09;&#xff0c;上手在BigQuant平台上快速构建AI策略。本文首先介绍如何使用证券代码模块指定股票范围和数据起止日期。重要的事情说三遍&#xff1a;模块的输入端口有提示需要连线的上游数据类型&a…

第七章 块为结构建模 P4|系统建模语言SysML实用指南学习

仅供个人学习记录 这部分感觉很模糊&#xff0c;理解的不好&#xff0c;后面的图也没画了&#xff0c;用到的时候再来翻书 应用端口实现接口建模 端口port表示了块边界上的一个访问点&#xff0c;也可以是由该块分类的任何组成或引用边界上的可访问点。一个块可以有多个端口规…

odoo16 库存初始化 excel导入问题

最近在为一家公司实施odoo时&#xff0c;发现库存模块实施过程中按用户实际&#xff0c;产品初始化就是个问题。下面一一记录下 一个新公司&#xff0c;产品都有上百种&#xff0c;甚致几千种&#xff0c;如何把现有产品数据录入系统就是个不小的活。odoo16是有导入导出功能不…

链表经典面试题之二

今天我们做一道环形链表的题目力扣141题https://leetcode.cn/problems/linked-list-cycle/ 这道题让我们分析链表中是否存环&#xff0c;存在的话返回true&#xff0c;不存在返回false。首先看到这道题我们要捋顺思路&#xff0c;怎么才能达到它要的效果&#xff1f;要找出是否…

[工业自动化-11]:西门子S7-15xxx编程 - PLC从站 - 分布式IO从站/从机

目录 一、什么是以分布式IO从站/从机 二、分布式IO从站的意义 三、ET200分布式从站系列 一、什么是以分布式IO从站/从机 在工业自动化领域中&#xff0c;分布式 IO 系统是目前应用最为广泛的一种 I/O 系统&#xff0c;其中分布式 IO 从站是一个重要的组成部分。 分布式 IO …

Scrum敏捷开发全流程,3款必备的项目管理工具!

​Scrum是一种敏捷方法&#xff0c;致力于帮助团队高效地协作和完成复杂的项目。它强调迭代和快速迭代、自组织、快速响应变化等原则&#xff0c;使得项目开发变得更加灵活和高效。 在Scrum敏捷开发过程中&#xff0c;项目管理工具是必不可少的。下面介绍3款常用的敏捷开发工具…

面向对象基础(以python语言为例)

1、定义一个类&#xff1b;实例化类的对象&#xff1b;调用类中的方法 #定义一个类 class Student:#类方法&#xff08;即函数&#xff09;def study(self,course_name):print(f学生正在学习{course_name})def play(self):print("xx学生正在玩游戏")#实例化&#xf…

OpenCV-Python小应用(九):通过灰度直方图检测图像异常点

OpenCV-Python小应用&#xff08;九&#xff09;&#xff1a;通过灰度直方图检测图像异常点 前言前提条件相关介绍实验环境通过灰度直方图检测图像异常点代码实现输出结果 参考 前言 由于本人水平有限&#xff0c;难免出现错漏&#xff0c;敬请批评改正。更多精彩内容&#xff…

EDA实验----四选一多路选择器设计(QuartusII)

目录 一&#xff0e;实验目的 二&#xff0e;实验仪器设备 三&#xff0e;实验原理&#xff1a; 四&#xff0e;实验要求 五&#xff0e;实验内容及步骤 1.实验内容 2.实验步骤 六&#xff0e;实验报告 七.实验过程 1.创建Verilog文件&#xff0c;写代码 2.波形仿真 …

TMUX命令的基本操作和使用

tmux&#xff1a;是两个单词的缩写&#xff0c;即“Terminal MultipleXer”&#xff0c;意思是“终端复用器”。 TMUX使用场景&#xff1a;假如你需要跑大模型或者数据集特别大的AI任务时&#xff0c;它往往需要花较长时间才能跑完&#xff0c;在跑的过程中&#xff0c;不能断…

苹果手机安装未上架APP应用测试教程

STEP 2&#xff1a;找到下载的描述文件&#xff08;如果没有找到&#xff0c;请到 设置 - 通用 - 描述文件 中查看&#xff09; STEP 3&#xff1a;安装描述文件 STEP 4&#xff1a;输入解锁密码安装描述文件 STEP 5&#xff1a;同意免责声明&#xff0c;安装描述文件 STEP 6…

“目标值排列匹配“和“背包组合问题“的区别和leetcode例题详解

1 目标值排列匹配 1.1 从目标字符串的角度来看&#xff0c;LC139是一个排列问题&#xff0c;因为最终目标子串的各个字符的顺序是固定的&#xff1f; 当我们从目标字符串 s 的角度来看 LC139 “单词拆分” 问题&#xff0c;确实可以认为它涉及到排列的概念&#xff0c;但这种…

puzzle(1612)拼单词、wordlegame

目录 拼单词 wordlegame 拼单词 在线play 找出尽可能多的单词。 如果相邻的话&#xff08;在任何方向上&#xff09;&#xff0c;你可以拖拽鼠标从一个字母&#xff08;方格&#xff09;到另一个字母&#xff08;方格&#xff09;。在一个单词中&#xff0c;你不能多次使用…

Android拖放startDragAndDrop拖拽Glide加载堆叠圆角图,Kotlin(5)

Android拖放startDragAndDrop拖拽Glide加载堆叠圆角图&#xff0c;Kotlin&#xff08;5&#xff09; import android.content.ClipData import android.graphics.Canvas import android.graphics.Point import android.os.Bundle import android.util.Log import android.view.…

macOS Sonoma 14.2beta2(23C5041e)发布(附黑白苹果镜像地址)

系统介绍 黑果魏叔11 月 10 日消息&#xff0c;今日向 Mac 电脑用户推送了 macOS 14.2 开发者预览版 Beta 2 更新&#xff08;内部版本号&#xff1a;23C5041e&#xff09;&#xff0c;本次更新距离上次发布隔了 14 天。 macOS Sonoma 14.2 添加了 Music 收藏夹播放列表&…

python工具HIKVISION视频编码设备接入网关任意文件下载

python工具 构造payload /serverLog/downFile.php?fileName../web/html/serverLog/downFile.php漏洞证明 文笔生疏&#xff0c;措辞浅薄&#xff0c;望各位大佬不吝赐教&#xff0c;万分感谢。 免责声明&#xff1a;由于传播或利用此文所提供的信息、技术或方法而造成的任何…

rasa train nlu详解:1.2-_train_graph()函数

本文使用《使用ResponseSelector实现校园招聘FAQ机器人》中的例子&#xff0c;主要详解介绍_train_graph()函数中变量的具体值。 一.rasa/model_training.py/_train_graph()函数   _train_graph()函数实现&#xff0c;如下所示&#xff1a; def _train_graph(file_importer…

【C语言】【数据结构】【环形链表判断是否带环并返回进环节点】有数学推导加图解

1.判断是否带环&#xff1a; 用快慢指针 slow指针一次走一步&#xff0c;fast指针一次走两步 当两个指针相遇时&#xff0c;链表带环&#xff1b;两个指针不能相遇时&#xff0c;当fast走到倒数第一个节点或为空时&#xff0c;跳出循环返回空指针。 那么slow指针一次走一步&a…