NIO基础篇

NIO是什么?

云里雾里的简介

1
nio就是new io的缩写,在Java1.4的时候就有了,可以替代标准的IO API,它的使用方式与原来的IO完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文 件的读写操作。

NIO与IO(BIO)的区别

用简洁明了的表格表示吧

IO NIO
面向流的操作 面向缓冲区的操作
阻塞的IO 非阻塞的IO
无选择器概念 有选择器概念

NIO相关概念

缓冲区(buffer)

  1. 网上介绍

    一个用于特定基本数据类型的容器。由java.nio包定义的,所有缓冲区都是Buffer抽象类的子类,主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。

  2. 个人理解

    缓冲区其实可以理解为火车,火车(buffer)可以再铁路线(channel)上双向跑动,并且载有具体的乘客(data)

  3. 实操感悟

    1. 我们先看一下下图内容

      nio-buffer01

      由图中实现树可以知道Buffer类有很多实现类,并且有mark、position、limit、capacity四个属性,这是很重要的四个属性,读者应该弄懂这四个属性的含义。

    2. 接下来,我们代码操作,先来解读后面三个属性

      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
      package com.xk.nio;

      import org.junit.Test;

      import java.nio.Buffer;
      import java.nio.ByteBuffer;

      /**
      * Created by xk on 2018-05-17
      */
      public class BufferDemo {
      @Test
      public void lastThreeAttr(){
      // 通过allocate()获得一个指定大小的缓冲区
      ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
      print(byteBuffer);

      // 向缓冲区写入数据
      String name = "abcdef";
      byteBuffer.put(name.getBytes());
      print(byteBuffer);
      // 切换到读模式
      byteBuffer.flip();
      print(byteBuffer);

      // 从缓冲区读取数据
      byte[] bytes = new byte[byteBuffer.limit()];
      byteBuffer.get(bytes);
      System.out.println("bytes:"+new String(bytes, 0 ,bytes.length));

      print(byteBuffer);

      // 可重复读
      Buffer rewind = byteBuffer.rewind();
      print(rewind);

      // 清空缓冲区(并没有把缓冲区的数据清除掉,只不过后续的写入会覆盖掉原来的数据,也就相当于清空了数据了。)
      byteBuffer.clear();
      print(byteBuffer);
      }

      public void print(Buffer buffer){
      System.out.println(buffer.capacity());
      System.out.println(buffer.limit());
      System.out.println(buffer.position());
      System.out.println("----------------------");
      }

      }

      对应的运行结果为:

      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
      capacity:10
      limit:10
      position:0
      ----------------------
      capacity:10
      limit:10
      position:5
      ----------------------
      capacity:10
      limit:5
      position:0
      ----------------------
      bytes:abcde
      capacity:10
      limit:5
      position:5
      ----------------------
      capacity:10
      limit:5
      position:0
      ----------------------
      capacity:10
      limit:10
      position:0
      ----------------------
    3. 读者可以直接运行我的代码,并自己阅读代码后,可以对照下图和文字进行理解

      image-20190218160825321

      其实缓冲区的初始情况下的position为0,capacity和limit为我们调用allocate()方法里指定的大小(int值),当我们调用put()方法写入数据,此时position会进行移动,而capacity和limit并没有影响,所以position输出为5,而其他属性不变,当我们flip切换到读取数据模式时,那么我们的position和limit都会发生变化,所以输出为limit:5,position:0

    4. 接下来,我们继续代码操作看看mark属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      @Test
      public void markAttr(){
      ByteBuffer byteBuffer = ByteBuffer.allocate(10);
      byteBuffer.put("abcde".getBytes());
      System.out.println("position:"+byteBuffer.position());
      byteBuffer.flip();
      //给position标记一下
      byteBuffer.mark();
      byte[] bytes = new byte[byteBuffer.limit()];
      byteBuffer.get(bytes, 0, 4);
      System.out.println(new String(bytes,0,4));
      System.out.println("position:"+byteBuffer.position());
      byteBuffer.reset();
      System.out.println("position:"+byteBuffer.position());
      }

      对应的运行结果:

      1
      2
      3
      4
      position:5
      abcd
      position:4
      position:0

      这个应该很容易体会到吧???

    5. 总结一下:

      1. 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。

        限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据 不可读写。缓冲区的限制不能为负,并且不能大于其容量。

        位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制。

        标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这 个 position。

      2. 标记、位置、限制、容量遵守以下不变式:0<=mark<=position<=limit<=capacity。

    6. 接下来补充一个知识:

      1. 非直接缓冲区:我们可以通过allocate()在JVM内存中分配一个缓冲区。

        image-20190219203054666

      2. 直接缓冲区:我们可以通过allocateDirect()在物理内存中分配一个缓冲区,可以提高效率,但该模式比较消耗内存资源。

        image-20190219203340662

      3. 总结:

        1. 通过看源码发现,allocate(),allocateDirect()方法并不是Buffer的,只是其部分实现类才有的(比如:ByteBuffer,IntBuffer)
        2. 我们可以通过buffer的isDirect()判断该缓冲区是否为直接缓冲区。

通道(channel)

  1. 网上介绍

    由 java.nio.channels 包定义的。Channel表示IO源与目标打开的连接。Channel类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。

  2. 个人理解

    通道其实类似于一个铁路线,比如上海-合肥的铁路线,这条铁路线是供火车运行的,有了铁路线,火车才能跑呀。

  3. 实操感悟

    1. 先看看如下2张图片

      image-20190219204708924image-20190219204820885

      从上图我们可以发现,通道模式下,并不会向CPU申请控制IO权限,而是自己独立的直接进行IO操作控制,所以当IO操作非常多时,用通道来操作的话,可以让CPU空闲起来做其他事,这样效率比DMA方式效率高。

    2. 通过查看Channel的实现类,我们可以看到有以下主要实现类:

      1
      2
      3
      4
      5
      •FileChannel:用于读取、写入、映射和操作文件的通道。
      •DatagramChannel:通过 UDP 读写网络中的数据通道。
      •SocketChannel:通过 TCP 读写网络中的数据。
      •ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来 的连接都会创建一个SocketChannel。
      可以看出这里的第一个为:本地IO,后三个为:网络IO
    3. 接下来,我们代码操作:

      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
      /**
      * Created by xk on 2018-05-17
      */
      public class ChannelDemo {
      @Test
      public void testChannelDemo() throws IOException {
      FileInputStream fileInputStream = new FileInputStream("xk.jpeg");
      FileOutputStream fileOutputStream = new FileOutputStream("xkRC.jpeg");

      FileChannel inputStreamChannel = fileInputStream.getChannel();
      FileChannel outputStreamChannel = fileOutputStream.getChannel();

      ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
      while (inputStreamChannel.read(byteBuffer) != -1){
      byteBuffer.flip();//切换到读模式,该语句不能放在while外面
      outputStreamChannel.write(byteBuffer);
      byteBuffer.clear();
      }
      outputStreamChannel.close();
      inputStreamChannel.close();
      fileOutputStream.close();
      fileInputStream.close();
      }

      }

      可以看到我们目录下多了一个xkRC.jpeg文件

      image-20190220094908465

    4. 接下来,再学习2个概念:

      1. 分散(scatter):如图image-20190220130423165

      2. 聚集(gather):如图image-20190220130516611

      3. 接下来,我们代码操作:

        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
        @Test
        public void testScatterAndGather(){
        try {
        RandomAccessFile randomAccessFile = new RandomAccessFile("NIO.iml","rw");//我这里直接使用ide里自带的.iml文件做测试
        FileChannel channel = randomAccessFile.getChannel();

        ByteBuffer byteBuffer1 = ByteBuffer.allocate(5);
        ByteBuffer byteBuffer2 = ByteBuffer.allocate(200);
        ByteBuffer byteBuffer3 = ByteBuffer.allocate(1024);
        ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2, byteBuffer3};

        channel.read(byteBuffers);

        for (ByteBuffer byteBuffer : byteBuffers){
        byteBuffer.flip();
        }
        System.out.println(new String(byteBuffers[0].array(), 0 , byteBuffers[0].limit()));
        System.out.println("**************************");
        System.out.println(new String(byteBuffers[1].array(), 0 , byteBuffers[1].limit()));
        System.out.println("**************************");
        System.out.println(new String(byteBuffers[2].array(), 0 , byteBuffers[2].limit()));

        // 测试聚集写入
        RandomAccessFile randomAccessFile2 = new RandomAccessFile("NIO2.iml","rw");
        FileChannel channel1 = randomAccessFile2.getChannel();
        channel1.write(byteBuffers);

        } catch (FileNotFoundException e) {
        e.printStackTrace();
        } catch (IOException e) {
        e.printStackTrace();
        }

        }

        运行结果:

        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
        <?xml
        **************************
        version="1.0" encoding="UTF-8"?>
        <module type="JAVA_MODULE" version="4">
        <component name="NewModuleRootManager" inherit-compiler-output="true">
        <exclude-output />
        <content url="file://$MODU
        **************************
        LE_DIR$">
        <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
        </content>
        <orderEntry type="inheritedJdk" />
        <orderEntry type="sourceFolder" forTests="false" />
        <orderEntry type="module-library">
        <library name="JUnit4">
        <CLASSES>
        <root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.12/junit-4.12.jar!/" />
        <root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar!/" />
        </CLASSES>
        <JAVADOC />
        <SOURCES />
        </library>
        </orderEntry>
        </component>
        </module>

        同时项目里生成NIO2.iml文件。

NIO的非阻塞式网络通信

Xie Kun wechat
觉得不错,请喝奶茶吧😃