2016 January 02

Java NIO

NIO优势:

如果说标准IO隐藏了系统层级的IO,进而让Java中的IO操作更加简单优雅,但是同样也隐藏了目前操作系统中的一些功能特性,降低了技术使用门槛的同时也降低了某些使用场景的性能;

而NIO在实现高速IO的同时,让使用者避免使用系统层级的繁杂的Native代码,NIO将IO操作中的大量操作重新依赖操作系统,封装系统操作,将更多的操作还给系统,按照系统级块级操作读写数据,同时更关键的点是利用非阻塞式模式,极大的提升性能以及灵活性;

NIO中缓存区 Buffer对象,NIO的数据读取都是直接读取到缓冲区,写入数据也是依旧先写入到缓冲区,与标准IO中的缓存不一样的是,标准IO的缓存依旧是通过流包装操纵,所以本质还是Stream流形式。

NIO基本:

  • ByteBuffer.allocate(); 函数 分配对应Buffer对象;
  • 数组包装: ByteBuffer.wrap( array );

NIO读写文件:

static void readDataFromFile(String path) throws IOException {
        File file = new File(path);
        if (!file.exists()){
            return;
        }
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();

        ByteBuffer nioBuffer = ByteBuffer.allocate(F_BUFFER_SIZE);

        while(fc.read(nioBuffer) > 0){
            nioBuffer.flip();
            for (int i = 0; i < nioBuffer.limit(); i++)
            {
                System.out.print((char) nioBuffer.get());
            }
            nioBuffer.clear(); // do something with the data and clear/compact it.
        }
        fc.close();
    }


    static void writeDataToFile(String path,String data) throws IOException {
        File file = new File(path);
        if (!file.exists()){
            file.createNewFile();
        }
        FileOutputStream fout = new FileOutputStream(file);
        FileChannel fc = fout.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(F_BUFFER_SIZE);

        byte [] bytes = data.getBytes();
        buffer.clear();
        buffer.put(bytes);

        buffer.flip();
        /**
         * 无法保证write()方法一次能向FileChannel写入多少字节,
         * 因此需要重复调用write()方法,
         * 直到Buffer中已经没有尚未写入通道的字节。
         */
        while (!buffer.hasRemaining()){
            fc.write(buffer);
        }
        fc.close();
    }

NIO针对数据的解析操作,比标准IO要明显复杂,我们很难校验我们需要的数据已经从Chanel中读入到Buffer中,所以需要多次校验,而不像Stream中简单优雅的就知道是否读完整个文本?是否读完真行?所以标准IO是有其适用情景的。

操纵Buffer 的几个状态概念与重点操作:

缓冲区状态指示,由于Buffer的读写模式其状态差异化,需要分开讨论:

position :

读模式:用于跟踪已经读了多少数据,将从 positon 指示的下一位置继续读取数据,

写模式:用于跟踪已写入的数据量,指示下一字节在buffer中存放的位置,将继续position 的下一个位置继续写数据

capacity :

指定缓冲区的读写最大容量上限

limit :

读模式: 代表还有多少数据可以从缓冲中取出,也就是可读取的容量,可以理解 limit 必然小于 capacity

写模式: 代表还有多少空间可以存放数据

flip(): 读写切换,将 limit 设定为当前 position,position 置为 0;

clear(): 为数据写入准备,清除标志位,position 置为 0;

rewind(): 标志位回退,认定limit位置设定正确,仅仅将position 重置为 0;一般为数据重写准备;

分散(Scatter)/聚集(Gather):

分散与聚集都是针对Chanel,分散是将Chanel中数据分散写入到多个buffer中;

分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。

聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。

其他Chanel互相之间的操作

非阻塞IO:

标准IO中,针对每个Socket数据流建立对应线程,根据新数据的到来与否做阻塞操作,有数据则处理数据,无数据阻塞等待;

选择器:其实操作系统底层一直在监听IO请求并通知各线程数据准备情况,而NIO中的Selector本质是对系统操作的一种封装,使我们能用操作Java代码的方式,移植对系统层次的操作;

利用注册的方式:指定合适的选择器,感兴趣的操作,同时,一个通道可以注册到多个选择器;通过Chanel关联数据的直接操纵,Sector关联监听Chanel情况,收取所关心的通知,进而通知其他业务逻辑操作,完成非阻塞IO;

多种事件的联合注册使用 “ “符号组合;

Selector使用:

//校验该返回值,若有值,则表明有通道就绪,进一步进行监听的Chenel集合检验
int result = selector.selectNow();

Set selectedKeys = selector.selectedKeys();  
Iterator keyIterator = selectedKeys.iterator();  
while(keyIterator.hasNext()) {  
    SelectionKey key = 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();
 }

通过检查Key集合的到达事件集合,来完成相应的处理逻辑,这也是Selector监听多个Chanel的根本;

Server Demo:

  private void go() throws IOException {
    // Create a new selector
    Selector selector = Selector.open();

    // Open a listener on each port, and register each one
    // with the selector
    for (int i=0; i<ports.length; ++i) {
      ServerSocketChannel ssc = ServerSocketChannel.open();
      ssc.configureBlocking( false );
      ServerSocket ss = ssc.socket();
      InetSocketAddress address = new InetSocketAddress( ports[i] );
      ss.bind( address );

      SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );

      System.out.println( "Going to listen on "+ports[i] );
    }

    while (true) {
      int num = selector.select();//阻塞 事件等待

      Set selectedKeys = selector.selectedKeys();
      Iterator it = selectedKeys.iterator();

      while (it.hasNext()) {
        SelectionKey key = (SelectionKey)it.next();

        if ((key.readyOps() & SelectionKey.OP_ACCEPT)
          == SelectionKey.OP_ACCEPT) {
          // Accept the new connection
          ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
          SocketChannel sc = ssc.accept();
          sc.configureBlocking( false );

          // Add the new connection to the selector
          SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );
          it.remove();

          System.out.println( "Got connection from "+sc );
        } else if ((key.readyOps() & SelectionKey.OP_READ)
          == SelectionKey.OP_READ) {
          // Read the data
          SocketChannel sc = (SocketChannel)key.channel();

          // Echo data
           //处理数据
          int bytesEchoed = 0;
          while (true) {
            echoBuffer.clear();

            int r = sc.read( echoBuffer );

            if (r<=0) {
              break;
            }

            echoBuffer.flip();

            sc.write( echoBuffer );
            bytesEchoed += r;
          }

          System.out.println( "Echoed "+bytesEchoed+" from "+sc );

          it.remove();
        }

      }

//System.out.println( "going to clear" );
//      selectedKeys.clear();
//System.out.println( "cleared" );
    }
  }

字符处理:

NIO中利用CharSet 构造合适的 encoder 与 decoder编解码处理字符编码问题;

总结:

NIO是一个好东西,但是实事求是的说,其编程逻辑有点复杂,好像在移动端应用场景很少但是这种监听 非阻塞思想还是有必要了解的;


Quote:

NIO 入门-IBM

对比Java.nio 和 Java.io

Java NIO入门与详解

Java NIO 系列教程

Difference between standard IO and NIO

深入分析 Java I/O 的工作机制

上一篇
下一篇
Loading Disqus comments...
Table of Contents