Java NIO 核心知识介绍

news/2024/12/15 12:14:17/文章来源:https://www.cnblogs.com/hld123/p/18607826

Java NIO 核心知识介绍

   概要

   在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。

   为了解决这个问题,在 Java1.4 版本引入了一种新的 I/O 模型 — NIO (New IO,也称为 Non-blocking IO) 。NIO 弥补了同步阻塞 I/O 的不足,它在标准 Java 代码中提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个连接,大大提高了 I/O 效率和并发。

   BIO、NIO 和 AIO 处理客户端请求的简单对比,如下图:

   需要注意:使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。

   一、NIO 核心组件

   NIO 主要包括以下三个核心组件:

  • Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
  • Channel(通道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。
  • Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。

    三者的关系如下图所示:

    1. Buffer(缓冲区)

   在传统的 BIO 中,数据的读写是面向流的, 分为字节流和字符流。

   在 Java 1.4 的 NIO 库中,所有数据都是用缓冲区处理的,这是新库和之前的 BIO 的一个重要区别,有点类似于 BIO 中的缓冲流。NIO 在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。 使用 NIO 在读写数据时,都是通过缓冲区进行操作。

   Buffer 的子类如下图所示。其中,最常用的是 ByteBuffer,它可以用来存储和操作字节数据。


   2. Channel(通道)

   Channel 是一个通道,它建立了与数据源(如文件、网络套接字等)之间的连接。我们可以利用它来读取和写入数据,就像打开了一条自来水管,让数据在 Channel 中自由流动。

   BIO 中的流是单向的,分为各种 InputStream(输入流)和 OutputStream(输出流),数据只是在一个方向上传输。通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写。

   Channel 与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。Channel和Buffer之间的关系如下图:

   另外,因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。Channel 的子类如下图所示。 

   其中,最常用的是以下几种类型的通道:

  • FileChannel:文件访问通道
  • SocketChannel、ServerSocketChannel:TCP 通信通道
  • DatagramChannel:UDP 通信通道

   如下图:

  Channel 最核心的两个方法:

  • read :读取数据并写入到 Buffer 中
  • write :将 Buffer 中的数据写入到 Channel 中。

  这里我们以 FileChannel 为例演示一下是读取文件数据的。

RandomAccessFile reader = new RandomAccessFile("/Users/guide/Documents/test_read.in", "r"))
FileChannel channel = reader.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);

   3. Selector(选择器)

   Selector(选择器) 是 NIO 中的一个关键组件,它允许一个线程处理多个 Channel。

   1)工作原理

   Selector 是基于事件驱动的 I/O 多路复用模型,主要运作原理是:通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。如下图:

 

 

    一个多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK 使用了 epoll() 代替传统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制。这也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。

    2)监听的四种事件类型

  • SelectionKey.OP_ACCEPT:表示通道接受连接的事件,这通常用于 ServerSocketChannel。
  • SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于 SocketChannel。
  • SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读。
  • SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。

   Selector是抽象类,可以通过调用此类的 open() 静态方法来创建 Selector 实例。Selector 可以同时监控多个 SelectableChannel 的 IO 状况,是非阻塞 IO 的核心。

   3)Selector实例的SelectionKey集合

   一个 Selector 实例有三个 SelectionKey 集合:

  • 所有的 SelectionKey 集合:代表了注册在该 Selector 上的 Channel,这个集合可以通过 keys() 方法返回。
  • 被选择的 SelectionKey 集合:代表了所有可通过 select() 方法获取的、需要进行 IO 处理的 Channel,这个集合可以通过 selectedKeys() 返回。
  • 被取消的 SelectionKey 集合:代表了所有被取消注册关系的 Channel,在下一次执行 select() 方法时,这些 Channel 对应的 SelectionKey 会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。

   简单演示一下如何遍历被选择的 SelectionKey 集合并进行处理:   

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if (key != null) {if (key.isAcceptable()) {// ServerSocketChannel 接收了一个新连接} else if (key.isConnectable()) {// 表示一个新连接建立} else if (key.isReadable()) {// Channel 有准备好的数据,可以读取} else if (key.isWritable()) {// Channel 有空闲的 Buffer,可以写入数据
        }}keyIterator.remove();
}

   Selector 还提供了一系列和 select() 相关的方法:

  • int select():监控所有注册的 Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应的 SelectionKey 加入被选择的 SelectionKey 集合中,该方法返回这些 Channel 的数量。
  • int select(long timeout):可以设置超时时长的 select() 操作。
  • int selectNow():执行一个立即返回的 select() 操作,相对于无参数的 select() 方法而言,该方法不会阻塞线程。
  • Selector wakeup():使一个还未返回的 select() 方法立刻返回。
  • ……

   使用 Selector 实现网络读写的简单示例:

 1 import java.io.IOException;
 2 import java.net.InetSocketAddress;
 3 import java.nio.ByteBuffer;
 4 import java.nio.channels.SelectionKey;
 5 import java.nio.channels.Selector;
 6 import java.nio.channels.ServerSocketChannel;
 7 import java.nio.channels.SocketChannel;
 8 import java.util.Iterator;
 9 import java.util.Set;
10 
11 public class NioSelectorExample {
12 
13   public static void main(String[] args) {
14     try {
15       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
16       serverSocketChannel.configureBlocking(false);
17       serverSocketChannel.socket().bind(new InetSocketAddress(8080));
18 
19       Selector selector = Selector.open();
20       // 将 ServerSocketChannel 注册到 Selector 并监听 OP_ACCEPT 事件
21       serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
22 
23       while (true) {
24         int readyChannels = selector.select();
25 
26         if (readyChannels == 0) {
27           continue;
28         }
29 
30         Set<SelectionKey> selectedKeys = selector.selectedKeys();
31         Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
32 
33         while (keyIterator.hasNext()) {
34           SelectionKey key = keyIterator.next();
35 
36           if (key.isAcceptable()) {
37             // 处理连接事件
38             ServerSocketChannel server = (ServerSocketChannel) key.channel();
39             SocketChannel client = server.accept();
40             client.configureBlocking(false);
41 
42             // 将客户端通道注册到 Selector 并监听 OP_READ 事件
43             client.register(selector, SelectionKey.OP_READ);
44           } else if (key.isReadable()) {
45             // 处理读事件
46             SocketChannel client = (SocketChannel) key.channel();
47             ByteBuffer buffer = ByteBuffer.allocate(1024);
48             int bytesRead = client.read(buffer);
49 
50             if (bytesRead > 0) {
51               buffer.flip();
52               System.out.println("收到数据:" +new String(buffer.array(), 0, bytesRead));
53               // 将客户端通道注册到 Selector 并监听 OP_WRITE 事件
54               client.register(selector, SelectionKey.OP_WRITE);
55             } else if (bytesRead < 0) {
56               // 客户端断开连接
57               client.close();
58             }
59           } else if (key.isWritable()) {
60             // 处理写事件
61             SocketChannel client = (SocketChannel) key.channel();
62             ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes());
63             client.write(buffer);
64 
65             // 将客户端通道注册到 Selector 并监听 OP_READ 事件
66             client.register(selector, SelectionKey.OP_READ);
67           }
68 
69           keyIterator.remove();
70         }
71       }
72     } catch (IOException e) {
73       e.printStackTrace();
74     }
75   }
76 }

  在示例中,我们创建了一个简单的服务器,监听 8080 端口,使用 Selector 处理连接、读取和写入事件。当接收到客户端的数据时,服务器将读取数据并将其打印到控制台,然后向客户端回复 "Hello, Client!"。

   二、NIO 零拷贝

   零拷贝是提升 IO 操作性能的一个常用手段,像 ActiveMQ、Kafka 、RocketMQ、QMQ、Netty 等顶级开源项目都用到了零拷贝。

   零拷贝是指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷贝时间。也就是说,零拷贝主要解决操作系统在处理 I/O 操作时频繁复制数据的问题。零拷贝的常见实现技术有: mmap+write、sendfile和 sendfile + DMA gather copy 。

   无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA(Direct Memory Access) 拷贝是都少不了的。因为两次 DMA 都是依赖硬件完成的。零拷贝主要是减少了 CPU 拷贝及上下文的切换。

   三、总结

   这篇文章我们主要介绍了 NIO 的核心知识点,包括 NIO 的核心组件和零拷贝。如果我们需要使用 NIO 构建网络程序的话,不建议直接使用原生 NIO,编程复杂且功能性太弱,推荐使用一些成熟的基于 NIO 的网络编程框架比如 Netty。Netty 在 NIO 的基础上进行了一些优化和扩展比如支持多种协议、支持 SSL/TLS 等等。


参考链接:
https://javaguide.cn/java/io/nio-basis.html

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

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

相关文章

#oscp#渗透测试 kioptix level 3靶机getshell及提权教程

声明! 文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关,切勿触碰法律底线,否则后果自负!!!!一、靶机搭建 点击扫描虚拟机选择靶机使在文件夹即可二、信息收集 前言 信息收集阶段,因为这里是靶机,所以不需要做什么,但是实际渗透测试中,大家…

rust学习十四.2、工作空间(workspace)

和大部分语言一样,cargo也可以创建一个工作空间,以便可以包含多个二进制单元和库单元,从而构建较为复杂的工程。 构建这样一个空间空间主要依赖两个手段:Cargo.toml和单元之间的目录结构 从效果上看,rust的Cargo在工作空间上的管理和maven大体相似,但是还不如maven那么的…

08一些元素的使用

一、今日学习内容 1、HTML常见的元素 HTML结构分析,H1到6,p元素,img,a元素,div,span元素 不常用元素,HTML全局属性 2、额外知识点补充 字符实体,URL地址,元素语义化,SEO优化,字符编码tip:这样在编写代码的时候就不用频繁的切换到英文状态下了 这样会提高编写代码的…

零基础科研小白の服务器看这个就够了

服务器 我们上网的时候经常会遇到连不上服务器,那啥是服务器呢。 其实就是一些特化的计算机,是远处有物理实体的设备。 from 挑战全网最硬核服务器基础知识 什么是服务器? 有 高计算 能力,能够通过网络 提供多种服务 的计算机。 服务器啥模样?服务器有什么特点? 计算能力…

2024-2025-1 20241415 《计算机基础与程序设计》第十二周学习总结

2024-2025-1 20241415《计算机基础与程序设计》第十二周学习总结 作业信息这个作业属于哪个课程 2024-2025-1-计算机基础与程序设计这个作业要求在哪里 2024-2025-1计算机基础与程序设计第十二周作业这个作业的目标 复习巩固前面所学的内容作业正文 https://www.cnblogs.com/zh…

Python序列的应用(九):集合以及列表、元组、字典和集合的区别

前言: 一、集合 Python 中的集合同数学中的集合概念类似,也是用于保存不重复元素的。它有可变集合(set)和不可变集合(fozenset)两种。本节所要介绍的可变集合是无序可变序列,而不可变集合在本书中不做介绍。在形式上,集合的所有元素都放在一对{}中,两个相邻元素间使用,分…

jQuery鼠标拖动旋转DOM元素插件

Propeller.js是一款jQuery鼠标拖动旋转DOM元素插件。通过该插件,可以使用鼠标拖动旋转页面中的任意DOM元素。使用方法 通过npm安装插件。npm install Propeller HTML结构 例如要拖动旋转一张图片。<img src="demo.jpg" id="img"> 初始化插件 作为jq…

js消息通知框、对话框、确认框和Loading插件

Notiflix是一款js消息通知框、对话框、确认框和Loading插件。Notiflix通过简单的设置,就可生成非常漂亮的s消息通知框、对话框、确认框和Loading效果。在线预览 下载使用方法 在页面中引入下面的文件。<link rel="stylesheet" href="css/notiflix-1.3.0.mi…

人工智能大语言模型起源篇,低秩微调(LoRA)

上一篇: 《规模法则(Scaling Law)与参数效率的提高》 序言:您在找工作时会不会经常听到LoRA微调,这项技术的来源就是这里了。 (12)Hu、Shen、Wallis、Allen-Zhu、Li、L Wang、S Wang 和 Chen 于2021年发表的《LoRA: Low-Rank Adaptation of Large Language Models》,htt…

群晖Lets Encrypt证书申请

注意本文时效性:2024.9.23 引言 为了保证SSL证书的权威性和安全性,Lets Encrypt 会验证您对域名的控制权。 申请 Lets Encrypt 证书有以下的验证控制权的方式:Web验证:通过在http的有权威的目录下创建一个验证文件以验证对服务器的控制权 Dns验证:通过在 DNS Record 中添加…

《计算机基础与程序设计》第十二周学习总结

学期(如2024-2025-1) 学号(如:20241300) 《计算机基础与程序设计》第十二周学习总结 作业信息这个作业属于哪个课程 <班级的链接> 2024-2025-1-计算机基础与程序设计这个作业要求在哪里 <作业要求的链接> 2024-2025-1计算机基础与程序设计第十二周作业)这个作…

读数据保护:工作负载的可恢复性14备份和恢复数据库

备份和恢复数据库1. 给采用传统方式交付的数据库制作备份 1.1. 某个数据库是新还是旧,跟该数据库是不是传统数据库没有必然的联系,真正的决定因素在于,这个数据库是不是运行在你所管理的服务器或虚拟机里1.1.1. 如果是,那就可以归入按照传统模型来交付的数据库1.1.2. 如果不…