Netty入门指南之NIO Selector监管

作者简介:☕️大家好,我是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从而获取到scServerSocketChannel 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,所以附件为nullSelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);// 监控acceptselectionKey.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和bufferSocketChannel sc = (SocketChannel) key.channel();ByteBuffer buffer = (ByteBuffer) key.attachment();   // 获取附件中的bufferint 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();  // 以免出现一行里面有多个\nByteBuffer 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.rhkb.cn/news/188306.html

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

相关文章

kafka微服务学习

消息中间件对比&#xff1a; 1、吞吐、可靠性、性能 Kafka安装 Kafka对于zookeeper是强依赖&#xff0c;保存kafka相关的节点数据&#xff0c;所以安装Kafka之前必须先安装zookeeper Docker安装zookeeper 下载镜像&#xff1a; docker pull zookeeper:3.4.14创建容器 do…

ObjectArx动态加载及卸载自定义菜单

上节中我们介绍了如何制作自定义菜单即cuix文件&#xff1a;给CAD中添加自定义菜单CUIX-CSDN博客https://blog.csdn.net/qianlixiaomage/article/details/134349794在此基础上&#xff0c;我们开发时通常需要在ObjectArx程序中进行动态的添加或者删除cuix菜单。 创建ObjectArx…

C语言 每日一题 PTA 11.8 day14

1.矩阵A乘以B 给定两个矩阵A和B&#xff0c;要求你计算它们的乘积矩阵AB。需要注意的是&#xff0c;只有规模匹配的矩阵才可以相乘。 即若A有Ra​行、Ca列&#xff0c;B有Rb行、Cb列&#xff0c;则只有Ca与Rb​相等时&#xff0c;两个矩阵才能相乘。 输入格式&#xff1a; 输入…

【树与二叉树的转换,哈夫曼树的基本概念】

文章目录 树与二叉树的转换将二叉树转化为树森林与二叉树的转化&#xff08;二叉树与多棵树之间的关系&#xff09;二叉树转换为森林森林的先序遍历1&#xff09;先序遍历2&#xff09;后序遍历 哈夫曼树的基本概念森林转换成二叉树&#xff08;二叉树与多棵树的关系&#xff0…

【java:牛客每日三十题总结-4】

java:牛客每日三十题总结 总结如下 总结如下 集合相关知识点 元素是否排序和插入顺序无关&#xff0c;取决与集合实现是否考虑了传入对象的java.lang.Comparable接口抽象类和接口相关知识 只能说越来越抽象了 java线程通信的方式 在Java中&#xff0c;常用的线程通信方式有两…

运行npm install卡住不动的几种解决方案

在前端开发经常会遇到运行npm install 来安装工具包一直卡住不动&#xff0c;为此这里提供几种解决方案&#xff0c;供大家参考学习&#xff0c;不足之处还请指正。 第一种方案、首先检查npm代理&#xff0c;是否已经使用国内镜像 // 执行以下命令查看是否为国内镜像 npm con…

【React-Native开发3D应用】React Native加载GLB格式3D模型并打包至Android手机端

【React-Native开发3D应用】React Native加载GLB格式3D模型并打包至Android手机端 【加载3D模型】**React Native上如何加载glb格式的模型**第零步&#xff0c;选择相关模型第一步&#xff0c;导入相关模型加载库第二步&#xff0c;自定义GLB模型加载钩子第三步&#xff0c;借助…

RK3568平台 查看内存的基本命令

一.free命令 free命令显示系统使用和空闲的内存情况&#xff0c;包括物理内存、交互区内存(swap)和内核缓冲区内存。共享内存将被忽略。 Mem 行(第二行)是内存的使用情况。 Swap 行(第三行)是交换空间的使用情况。 total 列显示系统总的可用物理内存和交换空间大小。 used 列显…

k8s、数据存储

数据存储的概念 容器磁盘上的文件的生命周期是短暂的&#xff0c;这就使得在容器中运行重要应用时会出现一些问题。首先&#xff0c;当容器崩溃时&#xff0c;kubelet 会重启它&#xff0c;但是容器中的文件将丢失——容器以干净的状态&#xff08;镜像最初的状态&#xff09;…

springboot中定时任务cron不生效,fixedRate指定间隔失效,只执行一次的问题

在调试计算任务的时候&#xff0c;手动重置任务为初始状态&#xff0c;但是并没有重新开始计算&#xff0c;检查定时任务代码&#xff1a; 从Scheduled(fixedRate 120000)可以看到&#xff0c;应该是间隔120秒执行一次该定时任务&#xff0c;查看后台日志&#xff0c;并没有重…

不可否认程序员的护城河已经越来越浅了

文章目录 那些在冲击程序员护城河低代码/无代码开发平台自动化测试和部署工具AI辅助开发工具在线学习和教育平台 面临冲击&#xff0c;程序员应该怎么做深入专业知识&#xff1a;不断学习全栈技能开发解决问题的能力建立人际网络管理和领导技能 推荐阅读 技术和应用的不断发展对…

《Swin Transformer: Hierarchical Vision Transformer using Shifted Windows》阅读笔记

论文标题 《Swin Transformer: Hierarchical Vision Transformer using Shifted Windows》 Swin 这个词貌似来自后面的 Shifted WindowsShifted Windows&#xff1a;移动窗口Hierarchical&#xff1a;分层 作者 微软亚洲研究院出品 初读 摘要 提出 Swin Transformer 可以…

学习笔记:深度学习(3)——卷积神经网络(CNN)理论篇

学习时间&#xff1a;2022.04.10~2022.04.12 文章目录 3. 卷积神经网络CNN3.1 卷积神经网络的概念3.1.1 什么是CNN&#xff1f;3.1.2 为什么要用CNN&#xff1f;3.1.3 人类的视觉原理 3.2 CNN的基本原理3.2.1 主要结构3.2.2 卷积层&#xff08;Convolution layer&#xff09;1.…

VR全景如何应用在房产行业,VR看房有哪些优势

导语&#xff1a; 在如今的数字时代&#xff0c;虚拟现实&#xff08;VR&#xff09;技术的迅猛发展为许多行业带来了福音&#xff0c;特别是在房产楼盘行业中。通过利用VR全景技术&#xff0c;开发商和销售人员可以为客户提供沉浸式的楼盘浏览体验&#xff0c;从而带来诸多优…

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

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

[云原生案例2.2 ] Kubernetes的部署安装 【单master集群架构 ---- (二进制安装部署)】网络插件部分

文章目录 1. Kubernetes的网络类别2. Kubernetes的接口类型3. CNI网络插件 ---- Flannel的介绍及部署3.1 简介3.2 flannel的三种模式3.3 flannel的UDP模式工作原理3.4 flannel的VXLAN模式工作原理3.5 Flannel CNI 网络插件部署3.5.1 上传flannel镜像文件和插件包到node节点3.5.…

[Linux打怪升级之路]-信号的保存和递达

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、信号的保…

医学影像系统源码(MRI、CT三维重建)

一、MRI概述 核磁共振成像&#xff08;英语&#xff1a;Nuclear Magnetic Resonance Imaging&#xff0c;简称NMRI&#xff09;&#xff0c;又称自旋成像&#xff08;英语&#xff1a;spin imaging&#xff09;&#xff0c;也称磁共振成像&#xff08;Magnetic Resonance Imag…

前端学习基础知识

环境搭建 windows环境 nodejs版本管理工具NVM nvm全英文也叫node.js version management&#xff0c;是一个nodejs的版本管理工具。nvm和n都是node.js版本管理工具&#xff0c;为了解决node.js各种版本存在不兼容现象可以通过它可以安装和切换不同版本的node.js。 安装学习访…

STM32 GPIO

STM32 GPIO GPIO简介 GPIO&#xff08;General Purpose Input Output&#xff09;通用输入输出口&#xff0c;也就是我们俗称的IO口 根据使用场景&#xff0c;可配置为8种输入输出模式 引脚电平&#xff1a;0V~3.3V&#xff0c;部分引脚可容忍5V 数据0就是低电平&#xff0c…