从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时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。
NIO可以让您只使用一个或几个单线程管理多个通道,但付出的代价是解析数据可能比从一个阻塞流中读取数据更为复杂。
如果需要管理同时打开的成千上万个连接,这些连接每次只发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。
如果你有少量的连接使用非常高的带宽,一次发送大量数据,也许典型的I/O服务器实现可能更为契合。
Buffer是特定基本类型元素的线性有限序列。除内容外,Buffer的基本属性还包括容量、限制和位置:
Buffer的主要作用就是装入数据,然后输出数据。开始时,Buffer的position为0,limit为capacity,程序调用put不断向Buffer中放入数据,每放入一些数据,position响应的向后移动。当Buffer装入数据结束后,调用filp方法,该方法将limit设置为position所在的位置,将position设为0,这样使得从Buffer中读数据时总是从0开始,读完刚刚装入的所有数据即结束。
使用Buffer读写数据一般遵循以下四个步骤:
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类似于传统的流对象,但它们之间有两个主要区别:
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对象表示可选择通道到选择器的注册,选择器维护了三种选择键集:
通过某个通道的register方法注册该通道时,就向选择器的键集中添加了一个键,在选择操作期间从键集中移除已取消的键。
与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。
register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:
通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server socket channel准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。
这四种事件用SelectionKey的四个常量来表示:
如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来。