从jdk1.4开始,java就提供了NIO,所以再用io是多么不知耻啊。
Channel和Buffer是NIO中的两个核心对象。
Channel是通道的意思,NIO中所有数据都需要通过Channel传输,
Buffer可以理解为一个容器,它的本质是一个数组,发送到Channel中的数据以及从Channel中读取的数据,都必须先放到Buffer中。
Buffer是个抽象类,常用子类有ByteBuffer,此外,除boolean之外的六个原始类型都有对应的Buffer类,如ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer。这些Buffer子类内部都有一个相应类型的数组,如ByteBuffer内部有一个byte数组,CharBuffer内部有一个char数组。Buffer子类都没有提供public的构造方法,只可以通过allocate开头的静态方法或者wrap静态方法生成Buffer实例。
public static ByteBuffer allocate(int capacity):返回一个HeapByteBuffer实例,capacity值为capacity,limit值也为capacity,position值为0。
ByteBuffer还有一个MappedByteBuffer子类,表示Channel将文件的部分或全部内容映射到内存中后得到的结果,通常由Channel的map()方法返回。
在Buffer中有三个重要的概念:容量(capacity)、界限(limit)、位置(position)。
capacity:Buffer的容量表示该Buffer最多能存储多少数据。容量在Buffer实例创建后不可改变。可以通过Buffer的capacity()实例方法查看capacity。
limit:位于界限之后的数据既不可读,也不可写。可以通过Buffer的limit()实例方法查看limit。
position:记录指针。可以通过Buffer的position()实例方法查看position。
Buffer的主要作用就是装入数据,然后输出数据。Buffer有两个重要的实例方法,flip()和clear()。
flip方法实现是:
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
flip方法会将limit值置为position,将position值置为0,将mark置为-1。
clear方法实现是:
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
clear方法会将position值置为0,limit值置为capacity,mark置为-1。这三个参数的状态和调用allocate(capacity)生成一个Buffer实例时的状态一样,所以说clear方法是为向Buffer中放入数据做准备。
往Buffer中放入数据时,limit值如何变化呢???答案是不变。从Buffer中取数据时,limit值如何变化呢???答案还是不变。
Channel
Channel是个接口,常用实现类有FileChannel、SelectableChannel、ServerSocketChannel、SocketChannel、DatagramChannel。FileChannel是操作文件的Channel实现类,ServerSocketChannel、SocketChannel是用于支持TCP通信的Channel实现类,DatagramChannel是用于支持UDP通信的Channel实现类。
如果要从Channel中取数据,必须先用Buffer从Channel中取数据,然后再从Buffer中取数据。反之亦然,如果想往Channel中写数据,则必须先将数据写入Buffer中,再将Buffer中的数据写入Channel中。
示例:
public static void main(String[] args) { File srcFile = new File("d:/CentOS-7-3.iso"); File destFile = new File("d:/CentOS-7-20.iso"); try (FileInputStream inputStream = new FileInputStream(srcFile); FileChannel inChannel = inputStream.getChannel(); FileOutputStream outputStream = new FileOutputStream(destFile); FileChannel outChannel = outputStream.getChannel()) { long size = srcFile.length(); // 一次映射1G到内存中 long pageSize = 1020 * 1024 * 1024; long time = size / pageSize; time = time * pageSize == size ? time : time + 1; MappedByteBuffer mappedByteBuffer; for (int i = 0; i < time; i++) { long currentPageSize = i < time - 1 ? pageSize : size - (time - 1) * pageSize; mappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, i * pageSize, currentPageSize); outChannel.write(mappedByteBuffer, i * pageSize); mappedByteBuffer.clear(); } } catch (Exception e) { e.printStackTrace(); } }
InputStream实例生成的Channel实例调用map方法时,MapMode只能是MapMode.READ_ONLY,否则会抛NonWritableChannelException异常。
FileUtils工具类的copyFile方法也用于文件复制,查看其源码,核心是:
try (FileInputStream fis = new FileInputStream(srcFile); FileChannel input = fis.getChannel(); FileOutputStream fos = new FileOutputStream(destFile); FileChannel output = fos.getChannel()) { final long size = input.size(); long pos = 0; long count = 0; while (pos < size) { final long remain = size - pos; count = remain > FILE_COPY_BUFFER_SIZE ? FILE_COPY_BUFFER_SIZE : remain; final long bytesCopied = output.transferFrom(input, pos, count); if (bytesCopied == 0) { break; } pos += bytesCopied; } }
可以看出FileUtils的copyFile方法用的是Channel的transferFrom方法,方法注释上说,这个方法不用经过Buffer,直接在input channel和output channel之间transfer数据,比output channel write (input channel map的Buffer)高效,FileChannel还有一个类似的transferTo方法可以使用。
通信
NIO为非阻塞式Socket通信提供了几个特殊的类:
1、Selector:它是SelectableChannel的多路复用器,所有SelectableChannel实例都要注册到Selector实例上。调用Selector的open()静态方法可以生成一个Selector实例。
2、SelectableChannel:可以被多路复用的Channel,被注册到Selector上,常用子类有:描述tcp通信的ServerSocketChannel、SocketChannel,以及描述udp通信的DatagramChannel。
3、SelectionKey:表示SelectableChannel与Selector的注册关系。
调用SelectableChannel实例的register()方法可将其注册到Selector实例上,当该Selector实例上的某些SelectableChannel有需要处理的IO操作时,可调用Selector实例的select()方法获取这些SelectableChannel实例的数量,并可通过selectedKeys()方法返回对应的SelectionKey集合,通过该集合就可以获取所有需要进行IO处理的SelectableChannel了。