首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

NIO的三大核心组件详解,充分说明为什么NIO在网络IO中拥有高性能!

编程知识
2024年07月22日 18:31

一、写在开头

我们在上一篇博文中提到了Java IO中常见得三大模型(BIO,NIO,AIO),其中NIO是我们在日常开发中使用比较多的一种IO模型,我们今天就一起来详细的学习一下。

在传统的IO中,多以这种同步阻塞的IO模型为主,程序发起IO请求后,处理线程处于阻塞状态,直到请求的IO数据从内核空间拷贝到用户空间。如下图可以直观的体现整个流程(图源:沉默王二)。

image

如果发起IO的应用程序并发量不高的情况下,这种模型是没问题的。但很明显,当前的互联网中,很多应用都有高并发IO请求的情况,这时就迫切的需要一款高效的IO模型啦。

NIO中的这个N既可以命名为NEW代表一种新型的IO模型,又可以理解为Non-Blocking,非阻塞之意。Java NIO 是 Java 1.4 版本引入的,基于通道(Channel)和缓冲区(Buffer)进行操作,采用非阻塞式 IO 操作,允许线程在等待 IO 时执行其他任务。常见的 NIO 类有 ByteBuffer、FileChannel、SocketChannel、ServerSocketChannel 等。(图源:深入拆解Tomcat & Jetty)

image

虽然在应用发起IO请求时,之多多次发起,无须阻塞。但在内核将数据拷贝到用户空间时,还是会阻塞的,为了保证数据的准确性和系统的安全稳定。

二、NIO的三大组件

在计算机与外部通信过程中,并非所有场景下NIO的性能都会好,对于连接少,并发地的应用系统中传统的BIO性能反而更好,因为在NIO中应用程序需要不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

image

为了更好的熟悉和掌握NIO,我们这里从NIO的三大组件入手,这也是很多大厂面试官在面试时会问到的点,虽然频率不高,但一定得会!

三个核心组件:

  • Selector(选择器): 一种基于事件驱动的I/O多路复用模型,允许一个线程处理多个Channel,多个Channel注册到一个Selector上,然后由Selector进行轮询监听每一个Channel的变化。
  • Channel(通道): 是一个双向的,可读可写的数据传输管道,通过它来实现数据的输入与输出工作,它只负责运输数据,不负责处理数据,处理数据在Buffer中。一般将管道分为文件通道套接字通道
  • Buffer(缓冲区): NIO中数据的操作都是在缓冲区中完成的。读操作是将Channel中运输过来的数据填充到Buffer中;写操作是将Buffer中的数据写入到Channel中。

为了更好的理解NIO基于三大核心组件的运行流程,画了一个思维导图,如下:

image

三、组件详解

下面,我们针对上一章总结的三大组件,进行一个个的详细介绍。

3.1 Buffer(缓冲区)

在传统的BIO中,数据的读写操作是基于流的,写入采用输入字节流或字符流,而写出采用都的是输出字节流或者字符流,本质上都是基于字节的数据操作。而NIO库中,采用的是缓冲区,无论是写入还是写出数据,都不会进入到缓冲区里,由缓冲区进行下一步的操作。

image

上图是Buffer子类的继承关系结构图,我们可以看到,在Buffer中命名是基于基本数据类型的,而我们在日常使用中,ByteBuffer缓冲类最多,它是基于字节存储的,这一点和流一样。

而进入到这些缓冲类的内部够,我们可以发现,其实它们就相当于一个数组容器。在Buffer的源码中,有这样的几个参数:

public abstract class Buffer {
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
}

这四个成员变量的具体含义如下:

  1. 容量(capacity):Buffer可以存储的最大数据量,Buffer创建时设置且不可改变;
  2. 界限(limit):Buffer 中可以读/写数据的边界。写模式下,limit 代表最多能写入的数据,一般等于 capacity(可以通过limit(int newLimit)方法设置);读模式下,limit 等于 Buffer 中实际写入的数据大小;
  3. 位置(position):下一个可以被读写的数据的位置(索引)。从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了;
  4. 标记(mark):Buffer允许将位置直接定位到该标记处,这是一个可选属性。

并且,上述变量满足如下的关系:0 <= mark <= position <= limit <= capacity

这里我们需要注意一点,Buffer拥有读和写两种模式。Buffer被创建后,默认是写模式 ,调用flip()可以切换到读模式,再调用clear()或者compact()方法切换为写模式。

image

1️⃣ Buffer的实例化

Buffer无法通过调用构造方法来创建对象,而是需要通过静态方法进行实例化。我们以ByteBuffer为例:

// 分配堆内存,将缓冲区建立在JVM的内存中
public static ByteBuffer allocate(int capacity);
// 分配直接内存,将缓冲区建立在物理内存中,可以提交效率。但这里的数据不会被垃圾回收,容易导致内存溢出。
public static ByteBuffer allocateDirect(int capacity);

2️⃣ Buffer的核心方法

Buffer中我们常用的方法有:

  1. get : 读取缓冲区的数据;
  2. put :向缓冲区写入数据;
  3. flip :将缓冲区从写模式切换到读模式,它会将 limit 的值设置为当前 position 的值,将 position 的值设置为 0;
  4. clear: 清空缓冲区,将缓冲区从读模式切换到写模式,并将 position 的值设置为 0,将 limit 的值设置为 capacity 的值。

image

3️⃣ Buffer的测试用例

基于以上的理论知识学习后,我们写一个小的测试demo,来感受一下Buffer的使用。

【测试案例】

public class TestBuffer {
        public static void main(String[] args) {

            // 分配一个容量为8的CharBuffer,默认为写模式
            CharBuffer buffer = CharBuffer.allocate(8);
            System.out.println("起始状态:");
            printState(buffer);

            // 向buffer写入3个字符
            buffer.put('a').put('b').put('c');
            System.out.println("写入3个字符后的状态:");
            printState(buffer);

            // 调用flip()方法,切换为读模式,
            // 准备读取buffer中的数据,将 position 置 0,limit 置 3
            buffer.flip();
            System.out.println("调用flip()方法后的状态:");
            printState(buffer);

            // 读取字符
            //hasRemaining()方法用于判断当前位置和限制之间是否有任何元素。
            //当且仅当此缓冲区中至少剩余一个元素时,此方法才会返回true。
            while (buffer.hasRemaining()) {
                System.out.println("读取字符:" + buffer.get());
            }
            // 调用clear()方法,清空缓冲区,将 position 的值置为 0,将 limit 的值置为 capacity 的值
            //调用clear()方法后,由读模式切换为写模式。
            buffer.clear();
            System.out.println("调用clear()方法后的状态:");
            printState(buffer);

        }

        // 打印buffer的capacity、limit、position、mark的位置
        private static void printState(CharBuffer buffer) {
            //容量
            System.out.print("capacity: " + buffer.capacity());
            //界限
            System.out.print(", limit: " + buffer.limit());
            //下一个读写位置
            System.out.print(", position: " + buffer.position());
            //标记
            System.out.print(", mark 开始读取的字符: " + buffer.mark());
            System.out.println("\n");
        }
}

【输出:】

起始状态:
capacity: 8, limit: 8, position: 0, mark 开始读取的字符:         

写入3个字符后的状态:
capacity: 8, limit: 8, position: 3, mark 开始读取的字符:      

调用flip()方法后的状态:
capacity: 8, limit: 3, position: 0, mark 开始读取的字符: abc

读取字符:a
读取字符:b
读取字符:c

调用clear()方法后的状态:
capacity: 8, limit: 8, position: 0, mark 开始读取的字符: abc     

3.2 Channel(通道)

在上面的总结中,我们已经提过了,Channel作为一种双向的数据通道,给外部属于与程序之间搭建了一个传输的桥梁。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。甚至还可以同时读写!

image

Channel 的子类如下图所示。
image

这里虽然有很多通道类,但我们在日常生活中常用的,无非是 FileChannel:文件访问通道;SocketChannel、ServerSocketChannel:TCP 通信通道;DatagramChannel:UDP 通信通道;

FileChannel:用于文件 I/O 的通道,支持文件的读、写和追加操作。FileChannel 允许在文件的任意位置进行数据传输,支持文件锁定以及内存映射文件等高级功能。FileChannel 无法设置为非阻塞模式,因此它只适用于阻塞式文件操作。

SocketChannel:用于 TCP 套接字 I/O 的通道。SocketChannel 支持非阻塞模式,可以与 Selector(下文会讲)一起使用,实现高效的网络通信。SocketChannel 允许连接到远程主机,进行数据传输。

与之匹配的有ServerSocketChannel:用于监听 TCP 套接字连接的通道。与 SocketChannel 类似,ServerSocketChannel 也支持非阻塞模式,并可以与 Selector 一起使用。ServerSocketChannel 负责监听新的连接请求,接收到连接请求后,可以创建一个新的 SocketChannel 以处理数据传输。

DatagramChannel:用于 UDP 套接字 I/O 的通道。DatagramChannel 支持非阻塞模式,可以发送和接收数据报包,适用于无连接的、不可靠的网络通信。

1️⃣ Channel的核心方法

  1. read :读取数据并写入到 Buffer 中;
  2. write :将 Buffer 中的数据写入到 Channel 中。

2️⃣ Channel的测试案例

RandomAccessFile reader = new RandomAccessFile("E:\\testChannel.txt", "r");
FileChannel channel = reader.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
System.out.println("读取字符:" + new String(buffer.array()));

image

3.3 Selector(选择器)

选择器的概念在上面已经介绍过了,我们现在主要介绍它的运作原理:
通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。

主要监视事件类型:

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

SelectionKey集合:

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

Selector中的select()方法:

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

【测试案例】

public static void main(String[] args) {
     try {
         //1、通过open()方法构建一个服务套接字通道
         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
         serverSocketChannel.configureBlocking(false);
         //封装8080端口
         serverSocketChannel.socket().bind(new InetSocketAddress(8080));

         //2、通过open方法构建一个选择器对象
         Selector selector = Selector.open();
         // 将 ServerSocketChannel 注册到 Selector 并监听 OP_ACCEPT 事件
         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

         while (true) {
             //监听已注册的通道中是否有连接事件,并将对应的 SelectionKey 加入被选择的 SelectionKey 集合中
             int readyChannels = selector.select();
             if (readyChannels == 0) {
                 continue;
             }
             //通过selectedKeys返回所有需要进行 IO 处理的 Channel
             Set<SelectionKey> selectedKeys = selector.selectedKeys();
             Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

             while (keyIterator.hasNext()) {
                 SelectionKey key = keyIterator.next();
                 // 处理连接事件
                 if (key.isAcceptable()) {
                     ServerSocketChannel server = (ServerSocketChannel) key.channel();
                     SocketChannel client = server.accept();
                     client.configureBlocking(false);
                     // 将客户端通道注册到 Selector 并监听 OP_READ 事件
                     client.register(selector, SelectionKey.OP_READ);
                 } else if (key.isReadable()) {
                     // 处理读事件
                     SocketChannel client = (SocketChannel) key.channel();
                     ByteBuffer buffer = ByteBuffer.allocate(1024);
                     int bytesRead = client.read(buffer);
                     if (bytesRead > 0) {
                         buffer.flip();
                         System.out.println("收到数据:" +new String(buffer.array(), 0, bytesRead));
                         // 将客户端通道注册到 Selector 并监听 OP_WRITE 事件
                         client.register(selector, SelectionKey.OP_WRITE);
                     } else if (bytesRead < 0) {
                         // 客户端断开连接
                         client.close();
                     }
                 } else if (key.isWritable()) {
                     // 处理写事件,立刻返回结果
                     SocketChannel client = (SocketChannel) key.channel();
                     ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes());
                     client.write(buffer);

                     // 将客户端通道注册到 Selector 并监听 OP_READ 事件
                     client.register(selector, SelectionKey.OP_READ);
                 }

                 keyIterator.remove();
             }
         }
     } catch (IOException e) {
         e.printStackTrace();
     }
 }

上面的代码创建了一个基于 Java NIO 的简单 TCP 服务器。它使用 ServerSocketChannel 和 Selector 实现了非阻塞 I/O 和 I/O 多路复用。服务器循环监听事件,当有新的连接请求时,接受连接并将新的 SocketChannel 注册到 Selector,关注 OP_READ 事件。当有数据可读时,从 SocketChannel 中读取数据并写入 ByteBuffer,然后将数据从 ByteBuffer 写回到 SocketChannel。

四、总结

到这里基本上就把NIO的几个重要的组件介绍完啦,肯定不能面面俱到,大家想更多了解的,还是要多翻看不同的书籍。同时,后面我们将基于这部分内容,写一个小型的聊天室。

From:https://www.cnblogs.com/JavaBuild/p/18316731
本文地址: http://www.shuzixingkong.net/article/301
0评论
提交 加载更多评论
其他文章 踩坑记录:windows11下使用 VS2022 和 PCL1.14.1 配置点云开发环境
闲话不多说,具体在windows下下载PCL与解压pcl可以看https://www.yuque.com/huangzhongqing/pcl/这位大佬的文章,那我就具体说一下踩过点坑: 踩坑点1: 按照大佬的文章的步骤进行解压与下载,我的PCL环境下在了K盘中,但是最后不知怎么的我的openni2
踩坑记录:windows11下使用 VS2022 和 PCL1.14.1 配置点云开发环境 踩坑记录:windows11下使用 VS2022 和 PCL1.14.1 配置点云开发环境 踩坑记录:windows11下使用 VS2022 和 PCL1.14.1 配置点云开发环境
前端使用 Konva 实现可视化设计器(18)- 素材嵌套 - 加载阶段
本章主要实现素材的嵌套(加载阶段)这意味着可以拖入画布的对象,不只是图片素材,还可以是嵌套的图片和图形。
周边上新,T恤上星:博客园T恤幸运闪系列,一款上架预售,一款上照预览
今天发布2款博客园T恤,一款上架预售,见上图中的前两张照片;一款上照预览,见上图中的后两张照片,未敢上架,先看看大家的评价。在第三版星星款设计被放弃后,我们痴星不改,还是想推出带星星的幸运闪系列T恤
周边上新,T恤上星:博客园T恤幸运闪系列,一款上架预售,一款上照预览 周边上新,T恤上星:博客园T恤幸运闪系列,一款上架预售,一款上照预览 周边上新,T恤上星:博客园T恤幸运闪系列,一款上架预售,一款上照预览
ComfyUI进阶:Comfyroll插件 (七)
前言: 学习ComfyUI是一场持久战,而Comfyroll 是一款功能强大的自定义节点集合,专为 ComfyUI 用户打造,旨在提供更加丰富和专业的图像生成与编辑工具。借助这些节点,用户可以在静态图像的精细调整和动态动画的复杂构建方面进行深入探索。Comfyroll 的节点设计简洁易用,功能强大,
ComfyUI进阶:Comfyroll插件 (七) ComfyUI进阶:Comfyroll插件 (七) ComfyUI进阶:Comfyroll插件 (七)
2024 Selenium10个替代品
随着自动化测试需求的不断增长,Selenium作为广泛使用的自动化测试工具,虽然功能强大,但也存在一些限制和挑战。在2024年, 越来越多的替代工具涌现,它们提供了更高效、更易用的解决方案。那么,哪些替代品值得我们关注呢? 在自动化测试领域,除了Selenium,还有哪些工具能够满足我们的需求,并且
2024 Selenium10个替代品 2024 Selenium10个替代品 2024 Selenium10个替代品
OI-Wiki 学习笔记
算法基础 \(\text{Update: 2024 - 07 - 22}\) 复杂度 定义 衡量一个算法的快慢,一定要考虑数据规模的大小。 一般来说,数据规模越大,算法的用时就越长。 而在算法竞赛中,我们衡量一个算法的效率时,最重要的不是看它在某个数据规模下的用时,而是看它的用时随数据规模而增长的趋
OI-Wiki 学习笔记 OI-Wiki 学习笔记 OI-Wiki 学习笔记
C++如何在main函数开始之前(或结束之后)执行一段逻辑?
1. 问题 2. 考察的要点 3. 解决策略 3.1. 方案一:使用GCC的拓展功能 3.2. 方案二:使用全局变量 3.3. 方案三:atexit 4. Demo测试 4.1. 测试代码 4.2. 执行结果 5. 程序异常退出场景 5.1. 存在的问题 5.2. 解决方案 5.2.1. 原理 5.
新做了一个MySQL 数据库 DDL 差异对比的网站
MySQL 数据库 DDL 差异对比的网站 摘要 新做了个网站,用来对比不同环境下的 DDL 差异,生成变更点和 迁移 DDL 网站地址:https://ddlcompare.com/ 对比过程中如果有问题,可以通过邮箱联系我 huiyuanai709@gmail.com,下班后我会密集的修一波 b
新做了一个MySQL 数据库 DDL 差异对比的网站 新做了一个MySQL 数据库 DDL 差异对比的网站 新做了一个MySQL 数据库 DDL 差异对比的网站