java nio 原理 非阻塞IO Netty

一、为什么必须去了解NIO

首先你需要之后Netty的主要实现手段就是Nio,很多人一直学不明白Netty,根本原因是 除了日常开发中很难能够实践,很大一部分原因是不熟悉NIO,事实上真正熟悉了NIO和它背后的原理之后,去查看Netty的源码就有如神助!我们今天就从最基本的IO、以及NIO学起! 

二、操作系统是如何定义I/O的

I/O相关的操作,详细各位从事java的人员并不陌生,顾名思义也就是Input/Output,对应着连个动词,Read/Write 读写两个动作,但是在上层系统应用中无论是读还是写,操作系统都不会直接的操作物理机磁盘数据,而是由系统内核加载磁盘数据!我们以Read为例,当程序中发起了一个Read请求后,操作系统会将数据从内核缓冲区加载到用户缓冲区,如果内核缓冲区内没有数据,内核会将该次读请求追加到请求队列,当内核将磁盘数据读取到内核缓冲区后,再次执行读请求,将内核缓冲区的数据复制到用户缓冲区,继而返回给上层应用系统!

三、网络编程中的IO模型

本文旨在让初学者先大致了解一下基本原理,所以这里并不会涉及到太多代码,具体的实现逻辑,可以关注后续源码分析的时候的文章,这里只做一个铺垫,为日后的学习做一个比较好的铺垫!

1. 同步阻塞I/O

I. 传统的阻塞IO模型

这种模型是单线程应用,服务端监听客户端连接,当监听到客户端的连接后立即去做业务逻辑的处理,该次请求没有处理完成之前,服务端接收到的其他连接全部阻塞不可操作!当然开发中,我们也不会这样写,这种写法只会存在于协议demo中!这种写法的缺陷在哪呢?

我们看图发现,当一个新连接被接入后,其他客户端的连接全部处于阻塞状态,那么当该客户端处理客户端时间过长的时候,会导致阻塞的客户端连接越来越多导致系统崩溃,我们是否能够找到一个办法,使其能够将业务处理与Accept接收新连接分离开来!这样业务处理不影响新连接接入就能够解决该问题!

II. 伪异步阻塞IO模型

这种业务模型是是对上一步单线程模型的一种优化,当一个新连接接入后,获取到这个链接的Socket,交给一条新的线程去处理,主程序继续接收下一个新连接,这样就能够解决同一时间只能处理一个新连接的问题,但是,明眼人都能看出来,这样有一个很致命的问题,这种模型处理小并发短时间可能不会出现问题,但是假设有10w连接接入,我需要开启10w个线程,这样会把系统直接压崩!我们需要限制线程的数量,那么肯定就会想到线程池,我们来优化一下这个模型吧!

III. 优化伪异步阻塞IO模型

这个模型是JDK1.4之前,没有NIO的时候的一个经典Socket模型,服务端接收到客户端新连接会后,将Socket连接以及业务逻辑包装为任务提交到线程池,由线程池开始执行,同时服务端继续接收新连接!这样能够解决上一步因为线程爆炸所引发的问题,但是我们回想下线程池的的提交步骤:当核心线程池满了之后会将任务放置到队列,当队列满了之后,会占用最大线程数的数量继续开启线程,当达到最大线程数的时候开始拒绝策略! 证明我最大的并发数只有1500个,其余的都在队列里面占1024个,假设现在的连接数是1w个,并且使用的是丢弃策略,那么会有近6000的连接任务被丢弃掉,而且1500个线程,线程之间的切换也是一个特别大的开销!这是一个致命的问题!

上述的三种模型除了有上述的问题之外,还有一个特别致命的问题,他是阻塞的!

在哪里阻塞的呢?

  • 连接的时候,当没有客户端连接的时候是阻塞的!没有客户端连接的时候,线程只能傻傻的阻塞在哪里等待新连接接入!
  • 等待数据写入的时候是阻塞的,当一个新连接接入后但是不写入数据,那么线程会一直等待数据写入,直到数据写入完成后才会停止阻塞! 假设我们使用 优化后的伪异步线程模型 ,1000个连接可能只有 100个连接会频繁写入数据,剩余900个连接都很少写入,那么就会有900个线程在傻傻等待客户端写入数据,所以,这也是一个很严重的性能开销!

现在我们总结一下上述模型的问题:

  1. 线程开销浪费严重!
  2. 线程间的切换频繁,效率低下!
  3. read/write执行的时候会进行阻塞!
  4. accept会阻塞等待新连接

那么,我们是否有一种方案,用很少的线程去管理成千上万的连接,read/write会阻塞进程,那么就会进入到下面的模型

2. 同步非阻塞I/O

同步非阻塞I/O模型就必须使用java NIO来实现了,看一段简单的代码:

public static void main(String[] args) throws IOException {//新接连池List<SocketChannel> socketChannelList = new ArrayList<>(8);//开启服务端SocketServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(8098));//设置为非阻塞serverSocketChannel.configureBlocking(false);while (true) {//探测新连接,由于设置了非阻塞,这里即使没有新连接也不会阻塞,而是直接返回nullSocketChannel socketChannel = serverSocketChannel.accept();//当返回值不为null的时候,证明存在新连接if(socketChannel!=null){System.out.println("新连接接入");//将客户端设置为非阻塞  这样read/write不会阻塞socketChannel.configureBlocking(false);//将新连接加入到线程池socketChannelList.add(socketChannel);}//迭代器遍历连接池Iterator<SocketChannel> iterator = socketChannelList.iterator();while (iterator.hasNext()) {ByteBuffer byteBuffer = ByteBuffer.allocate(128);SocketChannel channel = iterator.next();//读取客户端数据 当客户端数据没有写入完成的时候也不会阻塞,长度为0int read = channel.read(byteBuffer);if(read > 0) {//当存在数据的时候打印数据System.out.println(new String(byteBuffer.array()));}else if(read == -1) {//客户端退出的时候删除该连接iterator.remove();System.out.println("断开连接");}}}
}

上述代码我们可以看到一个关键的逻辑:serverSocketChannel.configureBlocking(false); 这里被设置为非阻塞的时候无论是 accept还是read/write都不会阻塞!具体的为什么会非阻塞,我放到文章后面说,我们看一下这种的实现逻辑有什么问题!

看这里,我们似乎的确使用了一条线程处理了所有的连接以及读写操作,但是假设我们有10w连接,活跃连接(经常read/write)只有1000,但是我们这个线程需要每次否轮询10w条数据处理,极大的消耗了CPU!

我们期待什么? 期待的是,每次轮询值轮询有数据的Channel, 没有数据的就不管他,比如刚刚的例子,只有1000个活跃连接,那么每次就只轮询这1000个,其他的有读写了有数据就轮询,没读写就不轮询!

3. 多路复用模型

多路复用模型是JAVA NIO 推荐使用的经典模型,内部通过 Selector进行事件选择,Selector事件选择通过系统实现,具体流程看一段代码:

public static void main(String[] args) throws IOException {//开启服务端SocketServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(8098));//设置为非阻塞serverSocketChannel.configureBlocking(false);//开启一个选择器Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {// 阻塞等待需要处理的事件发生selector.select();// 获取selector中注册的全部事件的 SelectionKey 实例Set<SelectionKey> selectionKeys = selector.selectedKeys();//获取已经准备完成的keyIterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey next = iterator.next();//当发现连接事件if(next.isAcceptable()) {//获取客户端连接SocketChannel socketChannel = serverSocketChannel.accept();//设置非阻塞socketChannel.configureBlocking(false);//将该客户端连接注册进选择器 并关注读事件socketChannel.register(selector, SelectionKey.OP_READ);//如果是读事件}else if(next.isReadable()){ByteBuffer allocate = ByteBuffer.allocate(128);//获取与此key唯一绑定的channelSocketChannel channel = (SocketChannel) next.channel();//开始读取数据int read = channel.read(allocate);if(read > 0){System.out.println(new String(allocate.array()));}else if(read == -1){System.out.println("断开连接");channel.close();}}//删除这个事件iterator.remove();}}
}

 相比上面的同步非阻塞IO,这里多了一个selector选择器,能够对关注不同事件的Socket进行注册,后续如果关注的事件满足了条件的话,就将该socket放回到到里面,等待客户端轮询!

NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,跟上面的NioServer代码类似,selector每次都会轮询所有的sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了epoll基于事件响应机制来优化NIO,首先我们会将我们的SocketChannel注册到对应的选择器上并选择关注的事件,后续操作系统会根据我们设置的感兴趣的事件将完成的事件SocketChannel放回到选择器中,等待用户的处理!那么它能够解决上述的问题吗?

肯定是可以的,因为上面的一个同步非阻塞I/O痛点在于CPU总是在做很多无用的轮询,在这个模型里被解决了!这个模型从selector中获取到的Channel全部是就绪的,后续只需要也就是说他每次轮询都不会做无用功!

深入 底层概念解析
select模型

如果要深入分析NIO的底层我们需要逐步的分析,首先,我们需要了解一种叫做select()函数的模型,它是什么呢?他也是NIO所使用的多路复用的模型之一,是JDK1.4的时候所使用的一种模型,他是epoll模型之前所普遍使用的一种模型,他的效率不高,但是当时被普遍使用,后来才会被人优化为epoll!

他是如何做到多路复用的呢?如图:

  1. 首先我们需要了解操作系统有一个叫做工作队列的概念,由CPU轮流执行工作队列里面的进程,我们平时书写的Socket服务端客户端程序也是存在于工作队列的进程中,只要它存在于工作队列,它就会被CPU调用执行!我们下文将该网络程序称之为进程A

2.他的内部会维护一个 Socket列表,当调用系统函数select(socket[])的时候,操作系统会将进程A加入到Socket列表中的每一个Socket的等待队列中,同时将进程A从工作队列移除,此时,进程A处于阻塞状态!

3.当网卡接收到数据之后,触发操作系统的中断程序,根据该程序的Socket端口取对应的Socket列表中寻找该进程A,并将进程A从所有的Socket列表中的等待队列移除,并加入到操作系统的工作队列! 

4.此时进程A被唤醒,此时知道至少有一个Socket存在数据,开始依次遍历所有的Socket,寻找存在数据的Socket并进行后续的业务操作

该种结构的核心思想是,我先让所有的Socket都持有这个进程A的引用,当操作系统触发Socket中断之后,基于端口寻找到对应的Socket,就能够找到该Socket对应的进程,再基于进程,就能够找到所有被监控的Socket! 要注意,当进程A被唤醒,就证明一件事,操作系统发生了Socket中断,就至少有一个Socket的数据准备就绪,只需要将所有的Socket遍历,就能够找到并处理本次客户端传入的数据!

但是,你会发现,这种操作极为繁琐,中间似乎存在了很多遍历,先将进程A加入的所有的Socket等待队列需要遍历一次,发生中断之后需要遍历一次Socket列表,将所有对于进程A的引用移除,并将进程A的引用加入到工作队列!因为此时进程A并不知道哪一个Socket是有数据的,所以,由需要再次遍历一遍Socket列表,才能真正的处理数据,整个操作总共遍历了3此Socket,为了保证性能,所以1.4版本种,最多只能监控1024个Socket,去掉标准输出输出和错误输出只剩下1021个,因为如果Socket过多势必造成每次遍历消耗性能极大!

epoll模型

epoll总共分为三个比较重要的函数:

  1. epoll_create 对应JDK NIO代码种的Selector.open()
  2. epoll_ctl 对应JDK NIO代码中的socketChannel.register(selector,xxxx);
  3. epoll_wait 对应JDK NIO代码中的 selector.select();

感兴趣的可以下载一个open-jdk-8u的源代码,也可以关注公众号回复openJdk获取源码压缩包!

他是如何优化select的呢?

  1. epoll_create:这些系统调用将返回一个非负文件描述符,他也和Socket一样,存在一个等待队列,但是,他还存在一个就绪队列!

2.epoll_ctl :添加Socket的监视,对应Java中将SocketChannel注册到Selector中,他会将创建的文件描述符的引用添加到Socket的等待队列!这点比较难理解,注意是将EPFD(Epoll文件描述符)放到Socket的等待队列!

3.当操作系统发生中断程序后,基于端口号(客户端的端口号是唯一的)寻找到对应的Socket,获取到EPFD的引用,将该Socket的引用加入到EPFD的就序列表!

4.epoll_wait:查看EPFD的就绪列表是否存在Socket的引用,如果存在就直接返回,不存在就将进程A加入到EPFD的等待队列,并移除进程A再工作队列的引用!

 

 当网卡再次接收到数据,发生中断,进行上述步骤,将该Socket的因引用加入到就序列表,并唤醒进程A,移除该EPFD等待队列的进程A,将进程A加入到工作队列,程序继续执行!

4. 异步非阻塞I/O

异步非阻塞模型是用户应用只需要发出对应的事件,并注册对应的回调函数,由操作系统完成后,回调回调函数,完成具体的约为操作!先看一段代码

public static void main(String[] args) throws Exception {final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));//监听连接事件,并注册回调serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {@Overridepublic void completed(AsynchronousSocketChannel socketChannel, Object attachment) {try {System.out.println("2--"+Thread.currentThread().getName());// 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端serverChannel.accept(attachment, this);System.out.println(socketChannel.getRemoteAddress());ByteBuffer buffer = ByteBuffer.allocate(1024);//监听read事件并注册回调socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {@Overridepublic void completed(Integer result, ByteBuffer buffer) {System.out.println("3--"+Thread.currentThread().getName());buffer.flip();System.out.println(new String(buffer.array(), 0, result));//向客户端回写一个数据socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));}//发生错误调这个@Overridepublic void failed(Throwable exc, ByteBuffer buffer) {exc.printStackTrace();}});} catch (IOException e) {e.printStackTrace();}}//发生错误调这个@Overridepublic void failed(Throwable exc, Object attachment) {exc.printStackTrace();}});System.out.println("1--"+Thread.currentThread().getName());Thread.sleep(Integer.MAX_VALUE);}
}

AIO客户端

public static void main(String... args) throws Exception {AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));ByteBuffer buffer = ByteBuffer.allocate(512);Integer len = socketChannel.read(buffer).get();if (len != -1) {System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));}
}

整体逻辑就是,告诉系统我要关注一个连接的事件,如果有连接事件就调用我注册的这个回调函数,回调函数中获取到客户端的连接,然后再次注册一个read请求,告诉系统,如果有可读的数据就调用我注册的这个回调函数!当存在数据的时候,执行read回调,并写出数据!

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

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

相关文章

Managed Lustre 和 WEKA:高性能文件系统的对比与应用

Managed Lustre 和 WEKA&#xff1a;高性能文件系统的对比与应用 1. 什么是 Managed Lustre&#xff1f;主要特点&#xff1a;适用场景&#xff1a; 2. 什么是 WEKA&#xff1f;主要特点&#xff1a;适用场景&#xff1a; 3. Managed Lustre 和 WEKA 的对比4. 如何选择 Managed…

el-select 添加icon

Element-ui-plus 新版本&#xff1a;Select 选择器 | Element Plus 要实现如上的效果 &#xff0c;要用到具名插槽 prefix&#xff0c;看代码&#xff1a; <template><el-dialog ref"dialogRef" v-model"dialogVisible" :title"title"…

【Linux】Ubuntu Linux 系统 ——PHP开发环境

ℹ️大家好&#xff0c;我是练小杰&#xff0c;元宵节到了&#xff0c;在此祝大家元宵节快乐&#x1f606; 新的一年里&#xff0c;愿你步步高升&#xff0c;事事如意&#xff0c;心想事成&#xff01;! 本文是关于Linux 操作系统中部署PHP开发环境这部分基础内容&#xff0c;后…

非谓语动词三驾马车

文章目录 1. 不定式基本结构不定式的由来1.不受主语的人称和数的限制2.没有限定时态3.可以在句子中充当不同的成分 常见句子成分1. 作主语2. 作表语3. 作宾语4. 作定语5. 作状语 不定式 vs 动名词 2. 动名词动名词做成分作主语作主语补语作定语作宾语介词宾语 3. 分词(现在、过…

【Apache Paimon】-- 16 -- 利用 paimon-flink-action 同步 kafka 数据到 hive paimon 表中

目录 引言CDC 技术概述 2.1 什么是 CDC2.2 CDC 的应用场景Kafka 作为 CDC 数据源的原理与优势 3.1 Kafka 的基本架构3.2 Kafka 在 CDC 中的角色

利用Python pandas 数据清洗详细教程

文章目录 前言一、环境搭建1. 下载 Python2. 安装 Python3 使用 pip 安装 Pandas 二、使用步骤1.读取数据2.处理缺失值3. 处理重复值4. 处理异常值5. 数据类型转换6. 处理不一致的数据 前言 pandas 是 Python 数据处理与分析的得力工具&#xff0c;功能强大&#xff0c;是数据…

qt的QMainWindow保存窗口和恢复窗口状态

保存窗口状态 QSettings settings("MyCompany", "MyApp"); // 指定存储的应用信息 settings.setValue("mainWindowState", saveState());saveState() 返回一个 QByteArray&#xff0c;包含 所有停靠窗口和工具栏的状态。QSettings 用于存储数据…

haproxy实现MySQL服务器负载均衡

1.环境准备 准备好下面四台台服务器&#xff1a; 主机名IP角色open-Euler1192.168.121.150mysql-server1openEuler-2192.168.121.151mysql-server2openEuler-3192.168.121.152clientRocky8-1192.168.121.160haproxy 2.mysql服务器配置 1.下载mariadb #下载mariadb [rootop…

Uniapp 原生组件层级过高问题及解决方案

文章目录 一、引言&#x1f3c5;二、问题描述&#x1f4cc;三、问题原因❓四、解决方案&#x1f4af;4.1 使用 cover-view 和 cover-image4.2 使用 subNVue 子窗体4.3 动态隐藏原生组件4.4 使用 v-if 或 v-show 控制组件显示4.5 使用 position: fixed 布局 五、总结&#x1f38…

Neo4j集群学习

文章目录 官方指导文档Neo4j因果集群核心服务器只读副本因果一致性 Neo4j集群搭建Neo4j企业版下载集群简介虚拟机准备jdk安装实施搭建访问neo4j Web服务 集群添加Core节点 官方指导文档 Neo4j 5 ClusterNeo4j 5 Basic Cluster Neo4j因果集群 集群是Neo4企业版中所提供的功能…

Ant-Design-Vue:Button按钮SVG图标垂直未居中问题

问题描述 如下图所示&#xff0c;在使用Ant-Design-Vue UI组件库提供的Button按钮Icon组件开发页面时&#xff0c;发现icon图标并没有垂直居中。 icon插槽-图标垂直不居中 再看一个官网的例子&#xff0c; 官网老子 问题排查 首先&#xff0c;考虑是否由于当前的页面布局&#…

OpenMetadata MySQL 数据库使用率提取管道实现解析

目录 架构概述核心组件源码分析使用率指标定义数据提取流程图源码类图配置与扩展指南架构概述 OpenMetadata 通过可插拔的元数据摄取框架实现对 MySQL 使用率数据的采集,核心流程包含三个阶段: 数据采集层:从 MySQL performance_schema 和 sys schema 获取原始指标指标处理…

慧星云+Chatbox :云上部署DeepSeek告别使用卡顿

DeepSeek 官网卡顿 DeepSeek 系列模型的现象级爆火&#xff0c;引发国内外广泛关注与讨论热潮&#xff0c;然而充满热情的使用者也让 DeepSeek 官网崩溃导致无法正常使用。 为了解决用户使用痛点&#xff0c;慧星云推出了云端蒸馏与满血版的 DeepSeek&#xff0c;搭配 Chatbox …

DeepSeek与ChatGPT的全面对比

在人工智能&#xff08;AI&#xff09;领域&#xff0c;生成式预训练模型&#xff08;GPT&#xff09;已成为推动技术革新的核心力量。OpenAI的ChatGPT自发布以来&#xff0c;凭借其卓越的自然语言处理能力&#xff0c;迅速占据市场主导地位。然而&#xff0c;近期中国AI初创公…

LeetCode1706

LeetCode1706 目录 LeetCode1706题目描述示例题目理解问题描述 示例分析思路分析问题核心 代码段代码逐行讲解1. 获取网格的列数2. 初始化结果数组3. 遍历每个球4. 逐行模拟下落过程5. 检查是否卡住6. 记录结果7. 返回结果数组 复杂度分析时间复杂度空间复杂度 总结的知识点1. …

前后端的身份认证

现代Web应用程序中&#xff0c;身份认证是确保用户数据安全的关键环节。无论你是开发一个简单的博客系统还是复杂的企业级应用&#xff0c;正确实现身份验证机制都是至关重要的。本文将探讨如何在前后端分离的架构下&#xff0c;使用Node.js作为后端来实现安全有效的身份认证。…

如何调整 Nginx工作进程数以提升性能

&#x1f3e1;作者主页&#xff1a;点击&#xff01; Nginx-从零开始的服务器之旅专栏&#xff1a;点击&#xff01; &#x1f427;Linux高级管理防护和群集专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2025年2月15日14点20分 Nginx 的工作进程数&#xff0…

学习web数据埋点

什么是埋点&#xff0c;以及为什么需要埋点 通过代码主动收集用户行为数据&#xff08;如点击、浏览、停留时长等&#xff09;&#xff0c;用于数据分析驱动产品优化。 一、前端埋点 在客户端&#xff08;浏览器、移动端应用&#xff09;直接采集用户行为数据&#xff0c;通…

VMware Workstation Pro 下载(无需注册账号)

VMware Workstation Pro 目前对个人用户完全免费。有两种下载方式&#xff0c;分别为 1、Broadcom 官网下载 需要注册账号&#xff0c;我懒得注册 2、VMware 更新服务器下载 如果不想注册账号&#xff0c;可以通过 VMware 更新服务器直接下载。访问以下链接&#xff1a; ht…

04性能监控与调优篇(D1_学习前言)

目录 一、引言 二、基本介绍 三、JVM基础 1. java堆 2. 垃圾回收 3. STW 四、调优层次 五、调优指标 六、JVM调优原则 1. 优先原则 2. 堆设置 3. 垃圾回收器设置 1> GC 发展阶段 2> G1的适用场景 3> 其他收集器适⽤场景 4. 年轻代设置 5. 年⽼代设置 …