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的READ
和WRITE
状态。只有当这些状态被触发时,"监管者"才会进行处理。在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的网络应用框架,我们对其的掌握将在未来的编程道路上发挥重要作用。
相关文章:

Netty入门指南之NIO Selector监管
作者简介:☕️大家好,我是Aomsir,一个爱折腾的开发者! 个人主页:Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客 当前专栏:Netty应用专栏_Aomsir的博客-CSDN博客 文章目录 参考文献前言问题解…...

Spring Cloud学习(六)【统一网关 Gateway】
文章目录 网关的功能搭建网关服务路由断言工厂Route Predicate Factory路由过滤器 GatewayFilter过滤器执行顺序跨域问题处理 网关的功能 网关功能: 身份认证和权限校验服务路由、负载均衡请求限流 在SpringCloud中网关的实现包括两种: gatewayzuul …...

基于单片机的空调智能控制器的设计
**单片机设计介绍,基于单片机的空调智能控制器的设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的空调智能控制器需要具备输入输出端口、定时器、计数器等模块,以便对空调进行精确控制。下…...

Spring Boot自动配置原理、实战、手撕自动装配源码
Spring Boot自动配置原理 相比较于传统的 Spring 应用,搭建一个 SpringBoot 应用,我们只需要引入一个注解 SpringBootApplication,就可以成功运行。 前面四个不用说,是定义一个注解所必须的,关键就在于后面三个注解&a…...

111111111111111
全局锁 就是对整个数据库进行加锁,加锁之后整个数据库就处于只读状态,后续的DML写语句,DDL语句,以及对更新事务的提交操作都会被阻塞,典型地使用场景就是做整个数据库的逻辑备份,对所有的表进行锁定&#x…...

React动态生成二维码和毫米(mm)单位转像素(px)单位
一、使用qrcode.react生成二维码,qrcode.react - npm 很简单,安装依赖包,然后引用就行了 npm install qrcode.react或者 yarn add qrcode.react直接上写好的代码 import React, {useEffect, useState} from react; import QRCode from qr…...
SpringMvc 常见面试题
1、SpringMvc概述 1.1、什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦&am…...

jmeter接口自动化测试工具在企业开展实际的操作
在企业使用jmeter开展实际的接口自动化测试工具,建议按如下操作流程, 可以使整个接口测试过程更规范,更有效。 接口自动化的流程: 1、获取到接口文档:swagger、word、excel ... 2、熟悉接口文档然后设计测试用例&am…...

第17章 反射机制
通过本章需要理解反射机制操作的意义以及Class类的作用,掌握反射对象实例化操作,并且可以深刻理解反射机制与工厂模式结合意义。掌握类结构反射操作的实现,并且可以通过反射实现类中构造方法、普通方法、成员属性的操作。掌握反射机制与简单J…...

如何在在线Excel文档中对数据进行统计
本次我们将用zOffice表格的公式与数据透视表分析样例(三个班级的学生成绩)。zOffice表格内置了大量和Excel相同的统计公式,可以进行各种常见的统计分析,如平均值、标准差、相关性等。同时,zOffice也有数据透视表功能&a…...

redis配置文件详解
一、配置文件位置 以配置文件启动 Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf ( Windows名为redis.windows. conf) 例: # 这里要改成你自己的安装目录 cd ./redis-6.0.8 vim redis.conf redis对配置文件对大小写不敏感 二、配置文件 1、获取当前服务的…...
前端设计模式之【工厂模式】
文章目录 前言什么时候不用介绍工厂模式的流程例子优点缺陷后言 前言 hello world欢迎来到前端的新世界 😜当前文章系列专栏:前端设计模式 🐱👓博主在前端领域还有很多知识和技术需要掌握,正在不断努力填补技术短板。…...

Python与ArcGIS系列(一)ArcGIS中使用Python
目录 0 简述1 arcgis中的python窗口2 开始编写代码 0 简述 按照惯例,作为本系列专栏的第一篇,先简单地介绍下本系列文章的内容:通过python语言创建arcgis环境脚本、将脚本以工具箱形式存放在arcgis中、通过脚本自动执行地理处理、数据修复、…...

LeetCode(2)移除元素【数组/字符串】【简单】
目录 1.题目2.答案3.提交结果截图 链接: 27. 移除元素 1.题目 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原…...

原型模式(创建型)
一、前言 原型模式是一种创建型设计模式,它允许在运行时通过克隆现有对象来创建新对象,而不是通过常规的构造函数创建。在原型模式中,一个原型对象可以克隆自身来创建新的对象,这个过程可以通过深度克隆或浅克隆来实现。简单说原型…...
Linux命令(118)之paste
linux命令之paste 1.paste介绍 linux命令paste命令是把每个文件以列对列的方式,一列列地加以合并 2.paste用法 paste [参数] filename... paste参数 参数说明-d使用指定的分隔符进行合并-s以行来指定文件 3.实例 3.1.使用冒号(:)合并文件 命令: …...
使用零拷贝技术实现消息转发功能
零拷贝技术介绍:史上最全零拷贝总结-CSDN博客 这是一个简单的基于epoll的Linux TCP代理程序,通过匿名管道和零拷贝技术的splice函数,将两个TCP端口相互连接,并转发数据。 #define _GNU_SOURCE 1 #include <sys/socket.h> …...

【编程语言发展史】SQL的发展历史
目录 目录 SQL概述 SQL发展历史 SQL特点 SQL基本语句 SQL是结构化查询语言(Structure Query Language)的缩写,它是使用关系模型的数据库应用语言,由IBM在70年代开发出来,作为IBM关系数据库原型System R的原型关系语言,实现了…...
2023NOIP A层联测28-小猫吃火龙果
给你一个长为 n n n 的序列,每个位置是 A , B , C A,B,C A,B,C 三个中的一个物品。 A A A 吃 B B B, B B B 吃 C C C, C C C 吃 A A A。 现在有 m m m 次操作,每次操作有两种: 区间修改:给出 l , r…...

C# Dictionary与List的用法区别与联系
C#是一门广泛应用于软件开发的编程语言,其中Dictionary和List是两种常用的集合类型。它们在存储和操作数据时有着不同的特点和用途。本文将详细探讨C# Dictionary和List的用法区别与联系,并通过代码示例进行对比,以帮助读者更好地选择适合自己…...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...
生成 Git SSH 证书
🔑 1. 生成 SSH 密钥对 在终端(Windows 使用 Git Bash,Mac/Linux 使用 Terminal)执行命令: ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 参数说明: -t rsa&#x…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...
Android第十三次面试总结(四大 组件基础)
Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成,用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机: onCreate() 调用时机:Activity 首次创建时调用。…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...
Caliper 配置文件解析:fisco-bcos.json
config.yaml 文件 config.yaml 是 Caliper 的主配置文件,通常包含以下内容: test:name: fisco-bcos-test # 测试名称description: Performance test of FISCO-BCOS # 测试描述workers:type: local # 工作进程类型number: 5 # 工作进程数量monitor:type: - docker- pro…...

【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅
目录 前言 操作系统与驱动程序 是什么,为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中,我们在使用电子设备时,我们所输入执行的每一条指令最终大多都会作用到硬件上,比如下载一款软件最终会下载到硬盘上&am…...