Java NIO 常用归纳

作者: androidjp | 来源:发表于2017-02-16 10:36 被阅读442次

    前言: 之前的文章《Java文件IO常用归纳》主要写了Java 标准IO要注意的细节和技巧,由于网上各种学习途径,所以并没有详细示例等。本文主要简单看看java的NIO库的用法,并做个小归纳,可以对比标准IO参考一下。

    NIO概述


    (一)背景

    NIO(New IO),在Java 1.4引入的一个新的IO API。【可替代标准IO API】

    (二)工作方式

    • Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
    • Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
    • Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道

    NIO 与 标准IO 的区别


    1. IO面向Stream(流),NIO面向Buffer(缓存)
    • 面向Stream:每次从流中读取一个或多个字节,直到读取所有字节,并没有缓存字节的地方。不能前后移动流中的数据(因为如果要前后移动从流中读取的数据,就需先将其缓存到一个缓存区中)。
    • 面向Buffer【更灵活】:数据读取到一个稍后处理的缓冲区,需要时即可在缓冲区中前后移动(注意:移动前首先需要检查是否该缓冲区中包含你需要处理的数据)。需要确保当更多数据读入缓冲区时,不会覆盖掉区中原有的尚未处理的数据。
    1. IO流都是阻塞的,而NIO有非阻塞模式
    • IO的流:当一个线程threadA使用IO调用read()/write()操作时,threadA被阻塞,直到一些数据被读取或写入完成,此过程中threadA不能做任何事。
    • NIO的非阻塞模式:
      • 【非阻塞读】线程threadA从某channel发送请求读取数据时,threadA仅能得到目前可用的数据,若目前没有可用数据,那么threadA不会获取任何数据并可以先做别的事情,而不是保持阻塞,直到有可用数据在这个通道出现。
      • 【非阻塞写】一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
      • 【应用】线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出channels。
    1. NIO独有选择器(Selector)

    常用API总结


    (一)核心接口与类关系图解与分析

    IO与NIO 接口与类关系图.png
    • 首先,图中有颜色背景的,是NIO中最重要的几个概念:Selector选择器、Channel通道、Buffer缓冲区、Pipe管道,他们几个的良好分工合作,才有了NIO。
    • 这四者之间的关系:
      1. Channel就是管理文件/数据传输出入口的地方【水龙头 / 百姓家】。文件数据/客户端数据/服务端数据【水】 在需要开始传递时【水要从水管出来】,就先通过Channel【水龙头打开】;
      2. Buffer用于暂存从Channel传来的数据【水厂】,从而提供外部选择和调整数据集合的能力【管理桶装水】。当数据量大于Buffer的最大装载量时,Buffer中原有的数据将被覆盖。Channel的实现类通过read(Buffer)等方法将数据保存到Buffer中,又通过write(Buffer)将Buffer中存储的数据写入到通道中【桶装水最终会按需派送到各个百姓家中】。
      3. Selector 用于控制和管理一或多个Channel的数据流动【桶装水管理搬运工,负责自己管理的若干水龙头,哪里有山泉水要出了,就关好别的水龙头,拿水桶去装水并打上备注即立刻回去管理水龙头,待会桶装水会被同事送到水厂】。
      4. Pipe 用于在两个线程之间传输数据。其内部依赖着两个Channel对象,分别用于数据的收和发。
    • Channel的相关实现类:FileChannel、SocketChannel与ServerSocketChannel、DatagramChannel,分别对应:“文件操作通道”、“TCP通信操作通道”、“UDP通信操作通道”。这几个实现类中,除了FileChannel不能进入非阻塞状态,其他实现类都可以进入非阻塞状态。
    • 所谓 阻塞状态(BIO)和非阻塞状态(NBIO),上述已说。
    • 图中的“分散”和“聚集”,分别指:
      1. 分散(scatter):【读】从channel中读取数据到buffer时,一个channel可以将数据缓存到多个buffer中。(read(Buffer[]);:开始存储数据到第一个buffer中,当一个buffer被写满后,channel紧接着向下一个buffer中写(按照缓冲区数组的排列顺序))
      2. 聚合(gather):【写】将buffer中的数据写入到channel中时,可以连续将多个buffer中的数据依次写入。
    • Buffer的相关实现类:
      1. 各种基本数据类型的Buffer,如:ByteBuffer、IntBuffer、ShortBuffer、LongBuffer、DoubleBuffer等,对应初始化时设定的capacity(缓冲区大小)即:最多同时缓存XX个byte、int、short、long、double数据。
      2. MappedByteBuffer:表示内存映射文件
    • Channel的原理:打开文件并构建符合NIO读写规则的通信桥梁口,对于网络TCP和UDP连接则是构建一个连接到特定IP特定端口的桥梁,并准备数据发送与数据接收。Channel与Stream(流)不同的是,Stream是单向传递数据的,而Channel是可读取并可写入的,具有双向性,并且更容易配合缓冲区来灵活获取数据。
    • Buffer的原理:Buffer 实际上是指向一个占N个单位的内存空间的对象,它本身就代表了一块内存区域。Buffer有两个模式:读模式和写模式(初始模式),通过silp()方法可以切换状态。而Buffer内部有三个成员属性用于共同维护这块内存区域,他们分别是:capacity【buffer总大小】、position【写模式:下一个可插入数据的位置,初始为0,最大是cap-1;读模式:下一个可读取数据的位置,初始为0,最大cap-1】、limit【读模式:最多可写入limit=cap个数据;写模式:可读取第limit=position(之前写入)的所有数据】。

    (二)常用API清单

    • Channel通用方法:

      • read(Buffer):int 将数据从channel读取到buffer中【读channel,写buffer】
      • read(Buffer[]):int 将数据从channel读取到buffer数组中
      • write(Buffer):int 将数据从buffer写入到channel中【读buffer,写channel】
      • write(Buffer[]):int 将数据从buffer数组写入到channel中
    • Buffer子类(以下以ByteBuffer为例子)通用方法:

      • 构造方法ByteBuffer(N:int) 设定缓冲区大小为N个byte大小的空间
      • get():byte[]:读取buffer中的所有数据
      • put(byte[]):void:数据写入buffer【功能和从channel中读取数据到buffer中一样】
      • filp():void:切换模式(写模式->读模式)
      • rewind():void:重读buffer中的数据(position重置为0)
      • clear():void:清空。重置所有指针,不删除数据!!(position=0,limit=capacity,重新供写入)
      • compact():void:半清空,保留仍未读取的数据。(position=最后一个未读单元之后的位置,limit=cap,重新供写入)
      • mark():标记时刻A的当前pos【与reset()一起用】
      • reset():回到时刻A时标记的pos位置。
      • close():关闭并释放channel对象。
    • FileChannel

      • 获取FileChannel对象:
        RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
        FileChannel fileChannel = accessFile.getChannel();
        
    • SocketChannel

      • 创建SocketChannel对象:
        SocketChannel sc  = SocketChannel.open();
        
    • 设置非阻塞IO状态:

      sc.configureBlocking(false);
      
    • 开始打开连接

      sc.connect(new InetSocketAddress("http://jianshu.com", 80));
      
    • 非阻塞状态下,成功连接前,干别的事

      sc.configureBlocking(false);
      ///...............
      while(!sc.finishConnect()){   ////do other sth.... }
      
    • 保证非阻塞IO状态下,read()过程不会read 空数据
      sc.configureBlocking(false);
      ///...............

      while((int len = sc.read(buf))==0){   ////do other sth.... }
      
    • 保证非阻塞IO状态下,write()过程不会write空数据
      sc.configureBlocking(false);
      ///...............

      while((int len = sc.write(buf))==0){   ////do other sth.... }
      
    • ServerSocketChannelDatagramChannelSelectorPipe的写法与方法说明,由于篇幅原因,可以参考下文的示例。

    (三)文件IO --- FileChannel

    1. 读文件
     public static byte[] readBytes(String fileName) {
          try {
              ///获取对应文件的FileChannel对象
              RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
              FileChannel fileChannel = accessFile.getChannel();
              /// 创建一个缓冲区(大小为48byte)
              ByteBuffer byteBuffer = ByteBuffer.allocate(48);
              StringBuilder builder = new StringBuilder();
    
              int bytesRead = fileChannel.read(byteBuffer);
              while (bytesRead != -1) {
                  System.out.println("Read " + bytesRead);
                  ///翻转buffer
                  byteBuffer.flip();
                  ///每次读取完之后,输出缓存中的内容
                  while (byteBuffer.hasRemaining()) {
                      System.out.println((char) byteBuffer.get());
                      builder.append((char) byteBuffer.get());
                  }
                  ///然后清空缓存区
                  byteBuffer.clear();
                  ///重新再读数据到缓存区中
                  bytesRead = fileChannel.read(byteBuffer);
              }
    
              accessFile.close();
              return builder.toString().getBytes();
          } catch (IOException e) {
              e.printStackTrace();
              return null;
          }
      }
    
    1. 写入文件
      public static void writeBytes(String fileName, byte[] data) {
          try {
              RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
              FileChannel channel = accessFile.getChannel();
              ByteBuffer buffer = ByteBuffer.allocate(48);
              buffer.put(data);
              channel.write(buffer);
          } catch (FileNotFoundException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
    
    1. 通道间内容传输
      /**
       * channel 间的传输
       *
       * @param sFileName 源文件
       * @param dFileName 目标文件
       */
      public static void channelToChannel(String sFileName, String dFileName) {
          try {
              RandomAccessFile sAccess = new RandomAccessFile(sFileName, "rw");
              RandomAccessFile dAccess = new RandomAccessFile(dFileName, "rw");
              FileChannel sChannel = sAccess.getChannel();
              FileChannel dChannel = dAccess.getChannel();
    
              long pos = 0;
              long sCount = sChannel.size();
              long dCount = dChannel.size();
    //            dChannel.transferFrom(sChannel,pos,sCount);//dChannel 必须是FileChannel
              sChannel.transferTo(pos, dCount, dChannel);///sChannel 是FileChannel
          } catch (FileNotFoundException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
    

    (四)TCP通信 --- SocketChannel

    1. 基本的C/S TCP通信
    • Client客户端写法:

    /**
    * Client SocketChannel 写法:
    */
    public static void client(String fileName) {
    SocketChannel sc = null;
    try {
    // 创建一个SocketChannel 通道
    ////TODO: FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
    sc = SocketChannel.open();
    ///TODO:非阻塞IO状态下,socketChannel就可以异步地执行read()、write()、connect()方法了
    sc.configureBlocking(false);
    sc.connect(new InetSocketAddress("http://jianshu.com", 80));

            while (!sc.finishConnect()) {///保证在connect成功之前,可以做别的事情
                //做点别的事。。。。。
            }
            while((int len  = sc.read(xxx))==0){ ///保证NBIO下,read数据不会read空
                 // 做别的事。。。
            }
            while((int len  = sc.write(xxx))==0){///保证NBIO下,write数据不会write空
                // 做别的事。。。
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (sc != null) {
                    sc.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    ```
    
    • Server服务端写法:

    /**
    * 关于:ServerSocketChannel
    */
    public static void serverSocketChannel() {
    ServerSocketChannel serverSocketChannel = null;
    try {
    ///打开
    serverSocketChannel = ServerSocketChannel.open();
    ///连接并开始监听TCP 9999端口
    serverSocketChannel.socket().bind(new InetSocketAddress(9999));
    ///TODO:可设置非阻塞状态(需要检查accept到的socketChannel是否为null)
    serverSocketChannel.configureBlocking(false);
    while (true) {
    SocketChannel socketChannel =
    serverSocketChannel.accept();
    //TODO: 非阻塞时需要考虑返回的socketChannel对象是否为null
    if(socketChannel != null){
    //do something with socketChannel...
    }
    //do something with socketChannel...
    }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocketChannel != null)
                try {
                    serverSocketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }
    ```
    
    1. 配合Selector,简化SocketChannel在非阻塞IO状态下的Null情况监测逻辑
    /**
       * 关于 选择器 和 SocketChannel 的配合使用
       */
      public static void selectorAndSocketChannel(String fileName) {
          SocketChannel sc1 = null;
          SocketChannel sc2 = null;
          SocketChannel sc3 = null;
          try {
              // 创建几个SocketChannel 通道
              ////TODO: FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
              sc1 = SocketChannel.open();
              sc2 = SocketChannel.open();
              sc3 = SocketChannel.open();
              ///TODO:非阻塞IO状态下,socketChannel就可以异步地执行read()、write()、connect()方法了
              sc1.configureBlocking(false);
              sc2.configureBlocking(false);
              sc3.configureBlocking(false);
              sc1.connect(new InetSocketAddress("http://jenkov.com", 80));
              sc2.connect(new InetSocketAddress("http://jenkov.com", 80));
              sc3.connect(new InetSocketAddress("http://jenkov.com", 80));
    
              // 创建Selector
              Selector selector = Selector.open();
              // 注册channels
              SelectionKey key1 = sc1.register(selector, SelectionKey.OP_READ);
              SelectionKey key2 = sc2.register(selector, SelectionKey.OP_READ);
              SelectionKey key3 = sc3.register(selector, SelectionKey.OP_READ);
              // 持续监控selector的四个事件(接受、连接、读、写)是否就绪
              while (true) {
                  int readyChannels = selector.select();
                  if (readyChannels == 0) continue;
                  Set selectedKeys = selector.selectedKeys();
                  Iterator keyIterator = selectedKeys.iterator();
                  while (keyIterator.hasNext()) {
                      SelectionKey key = (SelectionKey) keyIterator.next();
                      if (key.isAcceptable()) {
                          // a connection was accepted by a ServerSocketChannel.
                          ///我的这个连接请求被服务端接受了
                      } else if (key.isConnectable()) {
                          // a connection was established with a remote server.
                          ///已经连接上
                      } else if (key.isReadable()) {
                          // a channel is ready for reading
                          ///可读数据
                      } else if (key.isWritable()) {
                          // a channel is ready for writing
                          ///可写数据
                      }
                  }
                  keyIterator.remove();
              }
    
          } catch (IOException e) {
              e.printStackTrace();
          } finally {
              try {
                  if (sc1 != null) {
                      sc1.close();
                  }
                  if (sc2 != null) {
                      sc2.close();
                  }
                  if (sc3 != null) {
                      sc3.close();
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }
    

    (五)UDP通信 --- DatagramChannel

    1. 收发UDP数据包 的简单示例
       /**
       * 关于:DatagramChannel
       * UDP 无连接网络协议
       * 发送和接收的是数据包
       */
      public static void datagramChannel() {
          DatagramChannel datagramChannel = null;
          try {
              ///打开
              datagramChannel = DatagramChannel.open();
              ///连接并开始监听UDP 9999端口
              datagramChannel.socket().bind(new InetSocketAddress(9999));
              // 接收数据包(receive()方法会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃。 )
              ByteBuffer buf = ByteBuffer.allocate(48);
              buf.clear();
              datagramChannel.receive(buf);
              // 发送数据 send()
              String sendMsg = "要发送的数据";
              ByteBuffer sendBuf = ByteBuffer.allocate(48);
              sendBuf.clear();
              sendBuf.put(sendMsg.getBytes());
              sendBuf.flip();
              datagramChannel.send(sendBuf,new InetSocketAddress("xxxxx",80));
    
              // TODO: 连接到特定的地址(锁住DatagramChannel ,让其只能从特定地址收发数据 因为UDP无连接,本身没有真正的连接产出)
              datagramChannel.connect(new InetSocketAddress("jenkov.com", 80));
              ///连接后,也可以使用Channal 的read()和write()方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证
    
          } catch (IOException e) {
              e.printStackTrace();
          } finally {
              if (datagramChannel != null)
                  try {
                      datagramChannel.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
          }
      }
    

    (六)NIO管道(Pipe)

    首先,什么是NIO管道,下图可以看出其内部结构和功能特点:

    • NIO Pipe,是两个线程之间的单向连接通道(读下图可知)
    • Pipe类内部有两个成员属性,分别是:
      • Pipe.SinkChannel:数据入口通道
      • Pipe.SourceChannel:数据出口通道
    • 整体原理:ThreadA中获取的数据通过SinkChannel传入(写入)管道,当ThreadB要读取ThreadA的数据,则通过管道的SourceChannel传出(读取)数据。
      NIO Pipe原理图解
    1. 示例: 管道传输数据
    /**
       * 关于NIO管道(Pipe)
       * 定义:2个线程之间的单向数据连接
       */
      public static void aboutPipe(){
          Pipe pipe=null;
          try {
              /// 打开管道
              pipe = Pipe.open();
              ///TODO: 一、 向管道写入数据
              /// 访问Pipe.sinkChannel,向Pipe写入数据
              /// 首先,获取Pipe.sinkChannel
              Pipe.SinkChannel sinkChannel = pipe.sink();
              /// 然后,调用write(),开始写入数据
              String newData = "New String to write to file..." + System.currentTimeMillis();
              ByteBuffer buf = ByteBuffer.allocate(48);
              buf.clear();
              buf.put(newData.getBytes());
              buf.flip();
              while(buf.hasRemaining()){
              sinkChannel.write(buf);
              }
              // TODO: 二、读取管道中的数据
              // 首先,获取Pipe.sourceChannel
              Pipe.SourceChannel sourceChannel = pipe.source();
              /// 读取数据到buffer
              ByteBuffer buf2 = ByteBuffer.allocate(48);
              int bytesRead = sourceChannel.read(buf2);
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
    

    附录:java.nio包相关接口与类图一览


    Buffer类图 java.nio.channels 相关接口关系图 java.nio.channels 相关类图 java.nio.charset 相关类图(这个包主要做不同编码格式的加解密等工作)

    参考文章


    相关文章

      网友评论

        本文标题:Java NIO 常用归纳

        本文链接:https://www.haomeiwen.com/subject/uoonittx.html