Java NIO

一、概述

  从JDK1.4开始,Java提供了一系列改进的输入/输出处理的新特性,被统称为NIO(即New I/O)。新增了许多用于处理输入输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写,新增了满足NIO的功能。NIO采用内存映射文件的方式来处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了。

  

  其中,Channel(通道)、Buffer(缓冲)和Selectors(选择器)是NIO中的两个核心对象。

  Channel是对传统I/O系统的模拟,在NIO中所有数据都需要通过Channel传输。它与传统的I/O最大的区别在于它提供了一个map方法,通过该方法可以直接将“一块数据”映射到内存中,如果说传统I/O是面向流的处理,那么NIO就是面向块的处理。

  Buffer可以理解为一个容器,本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先读到Buffer中。

  服务器端和客户端各自维护一个管理通道的对象,称之为Selector,该对象能检测一个或多个Channel上的事件。以服务器为例,如果服务器上的selector上注册了读事件,某时刻客户端给服务器端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在Selector中添加一个事件,服务器端的处理线程会轮询的访问Selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。

  IO VS NIO

  NIO可以让您只使用一个或几个单线程管理多个通道,但付出的代价是解析数据可能比从一个阻塞流中读取数据更为复杂。

  如果需要管理同时打开的成千上万个连接,这些连接每次只发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。

  如果你有少量的连接使用非常高的带宽,一次发送大量数据,也许典型的I/O服务器实现可能更为契合。

二、缓冲区

  Buffer是特定基本类型元素的线性有限序列。除内容外,Buffer的基本属性还包括容量、限制和位置:

  • 容量:是它所包含的元素的数量。缓冲区的容量不能为负并且不能更改。
  • 限制:是第一个不应该读取或写入的元素的索引。缓冲区的限制不能为负,并且不能大于其容量。
  • 位置:是下一个要读取或写入的元素的索引。缓冲区的位置不能为负,并且不能大于其限制。

  Buffer的主要作用就是装入数据,然后输出数据。开始时,Buffer的position为0,limit为capacity,程序调用put不断向Buffer中放入数据,每放入一些数据,position响应的向后移动。当Buffer装入数据结束后,调用filp方法,该方法将limit设置为position所在的位置,将position设为0,这样使得从Buffer中读数据时总是从0开始,读完刚刚装入的所有数据即结束。

  使用Buffer读写数据一般遵循以下四个步骤: 

  • 写入数据到Buffer
  • 调用flip()方法
  • 从Buffer中读取数据
  • 调用clear()方法或者compact()方法

eg:

public class TestMain
{
    public static void main(String[] args)
    {
        CharBuffer buff = CharBuffer.allocate(5);
        System.out.println("buff.capacity="+buff.capacity());
        System.out.println("buff.position="+buff.position());
        System.out.println("buff.limit="+buff.limit());
        buff.put('a');
        buff.put('b');
        System.out.println("向Buffer中添加元素后--------");
        System.out.println("buff.capacity="+buff.capacity());
        System.out.println("buff.position="+buff.position());
        System.out.println("buff.limit="+buff.limit());
        buff.flip();
        System.out.println("调用filp函数后------------");
        System.out.println("buff.capacity="+buff.capacity());
        System.out.println("buff.position="+buff.position());
        System.out.println("buff.limit="+buff.limit());
        System.out.println(buff.get(0));//使用绝对操作获取0位置上的元素,此时不改变position的值
    }
}

  运行结果:

  其中,filp()的源码如下图,表示缓冲填充数据结束,position回到起点,limit设为position,相当于把Buffer中没有数据的存储空间封印起来,避免读到null值。

 public final Buffer flip()
{ limit
= position; position = 0; mark = -1; return this; }

  还有一点值得提出的是clear()方法,我们看它的源码是:

public final Buffer clear()
 {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
 }

  很明显,这个方法并不会让buffer真正的清空,它只是让position回到起始位置,此时还是可以通过get方法获取指定位置元素,然后将limit置为capacity,仿佛是将buffer清空一样。这样做的目的是不必为了每次读写都创建新的缓冲区,那样做会降低性能。相反,要重用现在的缓冲区,在再次读取之前要清除缓冲区.

  对于每个非 boolean 基本类型,此类都有一个子类与之对应。

  每个子类都定义了两种获取和放置操作:

  相对:从Buffer的当前位置读取或写入数据,然后将位置的值按处理的个数增加。

  绝对:直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer时,不会影响position中的值。

三、通道

  Channel类似于传统的流对象,但它们之间有两个主要区别:

  1. Channel可以直接将指定文件中的数据部分或全部直接映射成Buffer。
  2. 程序不能直接访问Channel中的数据,读写都不行,必须先用Buffer从Channel中取得数据,然后让程序从Buffer中取出这些数据。写数据同样要将数据先放入Buffer中,再将Buffer里的输入写入到Channel中。

  Channel(通道)表示到实体如硬件设备、文件、网络套接字或可以执行一个或多个不同I/O操作的程序组件的开放的连接。所有的Channel都不是通过构造器创建的,而是通过传统的节点InputStream、OutputStream的getChannel方法来返回响应的Channel。

  Channel中最常用的三个类方法就是map、read和write,其中map方法用于将Channel对应的部分或全部数据映射成ByteBuffer,而read或write方法有一系列的重载形式,这些方法用于从Buffer中读取数据或向Buffer中写入数据。eg:

public class TestMain
{
    public static void main(String[] args) throws UnknownHostException, IOException
    {
        FileChannel outChannel=null;
        File file=new File("E:\\hello.txt");
        FileInputStream inputStream=new FileInputStream(file);
        FileChannel fileInChannel=inputStream.getChannel();
        //将FileInChannel中的全部数据映射成ByteBuffer
        MappedByteBuffer buffer=fileInChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
        Charset charset=Charset.forName("GBK");
        //以文件输出流形式创建FileBuffer,用以控制输出
        outChannel=new FileOutputStream("E://hello_Copy.txt").getChannel();
        outChannel.write(buffer);
        buffer.clear();
        CharsetDecoder decoder=charset.newDecoder();
        CharBuffer charBuffer=decoder.decode(buffer);
        System.out.println(charBuffer);
        inputStream.close();
        fileInChannel.close();
        outChannel.close();
    }
}

  运行结果:

  

  上面的代码中,从FileInputStream获取的FileChannel只能读,而FileOutStream获取的FileChannel只能写。程序先将指定Channel中的全部数据映射成ByteBuffer,然后直接将整个ByteBuffer的全部数据写入一个输出FileChannel中,这样就完成了文件的复制。

  文件的复制还有一种更为简单的方式,就是直接将数据从一个channel传输到另外一个channel。eg:

public class TestMain
{
    public static void main(String[] args) throws UnknownHostException, IOException
    {
        RandomAccessFile fromFile = new RandomAccessFile("E://hello.txt", "rw");  
        FileChannel      fromChannel = fromFile.getChannel();  
        RandomAccessFile toFile = new RandomAccessFile("E://toFile.txt", "rw");  
        FileChannel      toChannel = toFile.getChannel();  
        long position = 0;  
        long count = fromChannel.size();  
        toChannel.transferFrom(fromChannel, position, count);  
        fromFile.close();
        toFile.close();
    }
}

四、选择器

  Selector是Java NIO中能够检测一到多个NIO通道,并能知晓通道是否为诸如读写事件做好准备的组件。

  可以通过调用此类的open()方法创建选择器,该方法将使用系统的默认选择器提供者创建新的选择器。通过选择器的close()方法关闭选择器之前,它一直保持打开状态。

  通过SelectionKey对象表示可选择通道到选择器的注册,选择器维护了三种选择键集:

  • 键集:包含的键表示当前通道到此选择器的注册,此集合由keys方法返回。
  • 已选择键集:在前一次选择操作期间,检测每个键的通道是否已经至少为该键的相关操作所标识的一个操作准备就绪。此集合由selectorKeys方法返回。
  • 已取消键集:是已被取消但其通道尚未注销的键的集合,不可直接访问此集合。

  通过某个通道的register方法注册该通道时,就向选择器的键集中添加了一个键,在选择操作期间从键集中移除已取消的键。

  与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。 
  register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件: 

  • Connect
  • Accept
  • Read
  • Write

  通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server socket channel准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。 
  这四种事件用SelectionKey的四个常量来表示: 

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

  如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来。

posted @ 2015-12-21 16:13  温布利往事  阅读(612)  评论(1编辑  收藏  举报