• 庖丁解牛:NIO核心概念与机制详解 01 _ 入门篇


    在这里插入图片描述


    Pre

    NIO 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。通过定义包含数据的类,以及通过以块的形式处理这些数据,NIO 不用使用本机代码就可以利用低级优化,这是原来的 I/O 包所无法做到的。


    输入/输出

    I/O 或者输入/输出指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。

    在 Java 编程中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。

    NIO 与原来的 I/O 有同样的作用和目的,但是它使用不同的方式 块 I/O。 块 I/O 的效率可以比流 I/O 高许多。


    Why NIO

    NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。


    流与块的比较

    原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的,原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

    面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。

    一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。


    通道和缓冲区

    概述

    通道 和 缓冲区 是 NIO 中的核心对象,几乎在每一个 I/O 操作中都要使用它们。

    • 通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。
    • 一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。

    什么是缓冲区?

    Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream 对象中。

    在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。

    缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程


    缓冲区类型

    最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。

    ByteBuffer 不是 NIO 中唯一的缓冲区类型。

    事实上,对于每一种基本 Java 类型都有一种缓冲区类型:

    • ByteBuffer
    • CharBuffer
    • ShortBuffer
    • IntBuffer
    • LongBuffer
    • FloatBuffer
      DoubleBuffer

    每一个 Buffer 类都是 Buffer 接口的一个实例。 除了 ByteBuffer,每一个 Buffer 类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作


    什么是通道?

    Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流

    正如前面提到的,所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节


    通道类型

    通道与流的不同之处在于**通道是双向的。**而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写

    因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。


    NIO 中的读和写

    概述

    读和写是 I/O 的基本过程。从一个通道中读取很简单:只需创建一个缓冲区,然后让通道将数据读到这个缓冲区中。写入也相当简单:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入操作。


    Demo : 从文件中读取

    从一个文件中读取一些数据。如果使用原来的 I/O,那么我们只需创建一个 FileInputStream 并从它那里读取。而在 NIO 中,情况稍有不同:我们首先从 FileInputStream 获取一个 Channel 对象,然后使用这个通道来读取数据。

    在 NIO 系统中,任何时候执行一个读操作,都是从通道中读取,但不是直接从通道读取。因为所有数据最终都驻留在缓冲区中,所以您是从通道读到缓冲区中。

    因此读取文件涉及三个步骤:

    • (1) 从 FileInputStream 获取 Channel
    • (2) 创建 Buffer
    • (3) 将数据从 Channel 读到 Buffer 中

    1. 从FileInputStream中获取Channel

    第一步是获取通道。我们从 FileInputStream 获取通道:

    FileInputStream fin = new FileInputStream( "readandshow.txt" );
    FileChannel fc = fin.getChannel();
    
    • 1
    • 2

    2. 创建ByteBuffer缓冲区

    下一步是创建缓冲区:

    ByteBuffer buffer = ByteBuffer.allocate( 1024 );
    
    • 1

    3. 将数据从Channle读取到Buffer中

    最后,需要将数据从通道读到缓冲区中,如下所示:

    fc.read( buffer );
    
    • 1

    注意:我们不需要告诉通道要读 多少数据 到缓冲区中。每一个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据。更多请继续往下看关于缓冲区内部细节 中介绍更多关于缓冲区统计机制的内容。


    Demo : 写入文件

    1. 从 FileOutputStream 获取一个通道

    在 NIO 中写入文件类似于从文件中读取。首先从 FileOutputStream 获取一个通道:

    FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
    FileChannel fc = fout.getChannel();
    
    • 1
    • 2

    2. 创建ByteBuffer缓冲区,写入数据

    下一步是创建一个缓冲区并在其中放入一些数据 。

    在这里,数据将从一个名为 message 的数组中取出,这个数组包含字符串 "Some bytes" 的 ASCII 字节(下面会解释 buffer.flip() 和 buffer.put() 调用)。

    ByteBuffer buffer = ByteBuffer.allocate( 1024 );
     
    for (int ii=0; ii<message.length; ++ii) {
         buffer.put( message[ii] );
    }
    buffer.flip();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3. 写入缓冲区

    最后一步是写入缓冲区中:

    fc.write( buffer );
    
    • 1


    Demo : 读写结合

    下面我们将看一下在结合读和写时会有什么情况。

    我们以一个名为 CopyFile.java 的简单程序作为这个练习的基础,它将一个文件的所有内容拷贝到另一个文件中。CopyFile.java 执行三个基本操作:

    • 首先创建一个 Buffer
    • 然后从源文件中将数据读到这个缓冲区中
    • 然后将缓冲区写入目标文件。

    这个程序不断重复 ― 读、写、读、写 ― 直到源文件结束。

    CopyFile 程序我们看看如何检查操作的状态,以及如何使用 clear() 和 flip() 方法重设缓冲区,并准备缓冲区以便将新读取的数据写到另一个通道中。

    package com.artisan.nio;
    
    import java.io.*;
    import java.nio.*;
    import java.nio.channels.*;
    
    /**
     * @author 小工匠
     * @version 1.0
     * @mark: show me the code , change the world
     */
    public class CopyFile {
    
        public static  void main( String args[] ) throws Exception {
    
    
            // 创建文件输入流和文件输出流
            FileInputStream fin = new FileInputStream( "boot-netty/src/main/resources/a.txt" );
            FileOutputStream fout = new FileOutputStream( "boot-netty/src/main/resources/c.txt" );
    
    
            // 创建文件输入流和文件   输出流
            FileChannel fcin = fin.getChannel();
            FileChannel fcout = fout.getChannel();
    
            // 创建文件输入流和文件输出流
            ByteBuffer buffer = ByteBuffer.allocate( 1024 );
    
            // 创建文件输入流和文件输出流
            while (true) {
                // 清空缓冲区
                buffer.clear();
    
                // 清空缓冲区
                int r = fcin.read( buffer );
    
                // 清空缓冲区
                if (r==-1) {
                    break;
                }
                // 反转缓冲区,准备写入数据
                buffer.flip();
    
                // 将缓冲区的数据写入到文件输出流
                fcout.write( buffer );
            }
        }
    }
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    程序解读:

    【运行 CopyFile 例子】

    因为缓冲区会跟踪它自己的数据,所以 CopyFile 程序的内部循环 (inner loop) 非常简单,如下所示:

    fcin.read( buffer );
    fcout.write( buffer );
    
    • 1
    • 2

    第一行将数据从输入通道 fcin 中读入缓冲区,第二行将这些数据写到输出通道 fcout 。


    【检查状态】

    下一步是检查拷贝何时完成。当没有更多的数据时,拷贝就算完成,并且可以在 read() 方法返回 -1 是判断这一点,如下所示:

    int r = fcin.read( buffer );
     
    if (r==-1) {
         break;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    【 重设缓冲区】

    最后,在从输入通道读入缓冲区之前,我们调用 clear() 方法。同样,在将缓冲区写入输出通道之前,我们调用 flip() 方法,如下所示:

    buffer.clear();
    int r = fcin.read( buffer );
     
    if (r==-1) {
         break;
    }
     
    buffer.flip();
    fcout.write( buffer );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • clear() 方法重设缓冲区,使它可以接受读入的数据。
    • flip() 方法让缓冲区可以将新读入的数据写入另一个通道。

  • 相关阅读:
    浅谈斜率优化
    Python程序设计——厄拉多塞素数筛选法的应用
    jdk8u201版本cpu.load过高问题的排查和解决
    vue-cli自定义创建项目-eslint依赖冲突解决方式
    分析kdump(vmcore)
    项目初始化时ApplicationRunner和CommandLineRunner的应用
    针对pycharm中keras问题,求解答
    Yolo算法检测之Anchor Boxes原理详解
    Kotlin委托属性(1)
    python 学习笔记(6)—— Flask 、MySql
  • 原文地址:https://blog.csdn.net/yangshangwei/article/details/134461309