@lemonguge
2015-07-02T08:21:33.000000Z
字数 8799
阅读 391
NIO
在上一篇介绍了有关缓冲区的一些常见知识,在这一篇中我打算讲讲通道。
在JAVA I/O中,曾经说过流的概念“流:代表任何有能力产出数据的数据源对象或者有能力接受数据的接收端对象”。Channel通道是一个对象,可以通过它读取和写入数据。拿nio与标准I/O做个比较,通道就像是流。我们通过缓冲区和通道进行交互,不会直接从通道中读取字节或者将字节直接写入通道中。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者同时用于读写。(RandomAccessFile也可以同时进行读写,是双向的)
由于通道是双向的,因此通道可以比流更好地反映底层操作系统的真实情况
我们对通道的使用主要体现在两方面:操作文件或者操作Socket流。因此在nio中将有四种最重要的通道的实现:
FileChannel从文件中读写数据;DatagramChannel通过UDP读写网络中的数据;SocketChannel通过TCP读写网络中的数据;ServerSocketChannel监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel。如果不懂Socket的话,可以看看我的关于Socket博文,那正是我为了写nio而写的一些文章。
FileChannel是一个连接到文件的通道,可以通过文件通道读写文件。FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。FileInputStream、FileOutputStream和RandomAccessFile都提供了一个getChannel()的方法来获取FileChannel通道。
当我们要从文件进行读取时,如果使用标准I/O,那么我们只需创建一个FileInputStream并从它那里读取。而在nio中,情况稍有不同:我们首先从FileInputStream获取一个FileChannel对象,然后使用这个通道来读取数据。因此读取文件有以下步骤:
FileInputStream获取FileChannel文件通道;ByteBuffer缓冲区;FileChannel读到ByteBuffer中。可以通过getChannel()方法获取此文件关联的唯一FileChannel对象,无论你调用getChannel()方法多次都将获得相同的对象,以下是一个示例:
import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.nio.channels.FileChannel;public class FileChnl {public static void main(String[] args) throws IOException {// 当前项目下有一个file.txt文件File file = new File("file.txt");FileInputStream fis = new FileInputStream(file);verifyChnl(fis);}// 验证获取的通道是否相同public static void verifyChnl(FileInputStream fis) throws IOException {FileChannel chnl1 = fis.getChannel();FileChannel chnl2 = fis.getChannel();System.out.println(chnl1 == chnl2);fis.close();}} /* Output:true*///:~
查看关于FileChannel的API可以发现,里面有些read和write方法,很类似于我们之前所学的标准I/O。如果我们需要将数据从通道读入到缓冲区,可以使用read方法,可以发现:我们不需要告诉通道要读多少数据到缓冲区中,因为每一个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据。以下是一个将文件打印在主控台的示例:
import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class FileChnl {public static void main(String[] args) throws IOException {// 当前项目下有一个file.txt文件,文件中有两行数据// 分别为“abc123”和“你好”File file = new File("file.txt");showFile(file);}public static void showFile(File file) throws IOException {FileInputStream fis = new FileInputStream(file);// 获取通道FileChannel chnl = fis.getChannel();// 缓冲区的大小final int SIZE = 5;// 创建缓冲区ByteBuffer buf = ByteBuffer.allocate(SIZE);// 用于接收缓冲区字节的数组byte[] dst = new byte[SIZE];int length = 0;while ((length = chnl.read(buf)) != -1) { // 通道已到达流的末尾,则返回 -1// 为内存读取缓冲区而做准备buf.flip();buf.get(dst, 0, length);System.out.print(new String(dst, 0, length));// 清空缓冲区buf.clear();}fis.close();}} /* Output:abc123你好*///:~
如果我们需要将数据从缓冲区写入到通道,可以使用write方法。同样不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。此时应该从FileOutputStream获取一个FileChannel对象,以下是一个将内存的数据写入到文件的示例:
import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.UnsupportedEncodingException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class FileChnl {// 根据系统获取换行符private static final String LINE_SEPARATOR = System.getProperty("line.separator");public static void main(String[] args) throws IOException {// 当前项目下有一个file.txt文件,文件中有两行数据// 分别为“abc123”和“你好”File file = new File("file.txt");write2file(file);}public static void write2file(File file) throws FileNotFoundException,UnsupportedEncodingException, IOException {// 续写文件FileOutputStream fos = new FileOutputStream(file, true);// 获取通道FileChannel chnl = fos.getChannel();// 缓冲区的大小final int SIZE = 5;// 创建缓冲区ByteBuffer buf = ByteBuffer.allocate(SIZE);// 文件的编码为UTF-8byte[] src = ("将以下数据输入到文件" + LINE_SEPARATOR + "Hello World").getBytes("UTF-8");// 数据需要几次能被写完int time = getTime(SIZE, src.length);for (int i = 0, offset = 0; i < time; i++) {buf.put(src, offset, src.length - offset > SIZE ? SIZE : src.length - offset);// 为通道读取缓冲区而做准备buf.flip();while(buf.hasRemaining())offset += chnl.write(buf); // 保证将缓冲区的字节全部写入// 清空缓冲区buf.clear();}fos.close();}// 获取写入缓冲区的次数private static int getTime(int size, int length) {int time = length / size;if (length % size != 0)time++;return time;}} ///:OK~
执行以上代码可以打开文件查看到三行文字:“abc123”、“你好将以下数据输入到文件”和“Hello World”。可以看到,当要写入的字节长度大于缓冲区的长度时,代码的复杂度是很高的,我认为如果仅仅只是从内存将数据写入到文件,应该是使用标准I/O,往往通道的write方法和read方法结合使用。
import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class CopyFile {public static void main(String[] args) throws IOException {File src = new File("file.txt");File dst = new File("D:" + File.separator + "copy_file.txt");FileInputStream fis = new FileInputStream(src);FileOutputStream fos = new FileOutputStream(dst);FileChannel srcChnl = fis.getChannel();FileChannel desChnl = fos.getChannel();// 创建缓冲区ByteBuffer buf = ByteBuffer.allocate(5);while (srcChnl.read(buf) != -1) {// 为输出通道读取缓冲区做准备buf.flip();while(buf.hasRemaining())desChnl.write(buf);// 清空缓冲区buf.clear();}fis.close();fos.close();}} ///:OK~
执行以上示例可以在D盘下有一个"copy_file.txt"文件,但是我并不推荐通过allocate()方法来获得缓冲区,之前介绍了两种创建缓冲区的方法,创建了的缓冲区的底层实现为数组,还有一个allocateDirect()方法也可以创建缓冲区,查看API中对该方法的描述“分配新的直接字节缓冲区”。直接缓冲区是为加快I/O速度,而以一种特殊的方式分配其内存的缓冲区,Sun的文档是这样描述直接缓冲区的:
给定一个直接字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作。也就是说,它会在每一次调用底层操作系统的本机I/O操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。
只需要对以上的示例将缓冲区的获取方式进行修改,就可以得到更高效的代码了。
当输出流是通过调用FileOutputStream(File,boolean)构造方法且为第二个参数传入true来创建的,则该文件通道可能处于添加模式。在此模式中,每次调用相关的写入操作都会首先将位置移到文件的末尾,然后再写入请求的数据。
import java.io.FileOutputStream;import java.io.IOException;public class Channel {public static void main(String[] args) throws IOException {// 当前项目下有file.txt文件,有两行内容“abc123”和“你好”// windows平台下换行符占用两个字节FileOutputStream fos = new FileOutputStream("file.txt", true);// 续写文件,文件通道处于添加模式// 在此模式中,每次调用相关的写入操作都会首先将位置移到文件的末尾,然后再写入请求的数据。System.out.println(fos.getChannel().position()); // 到目前为止写入此流的字节数fos.close();}} /* Output:12*///:~
虽然这块知识点并不重要,只是希望通过上面的小示例可以对通道的position位置有个清晰的了解。
通过FileChannel的size()方法将返回该实例所关联文件的大小(以字节为单位),该方法等价与字节输入流的available()方法,如下所示:
import java.io.FileInputStream;import java.io.IOException;public class Channel {public static void main(String[] args) throws IOException {size();}// 返回此通道的文件的当前大小private static void size() throws IOException {// 当前项目下有file.txt文件,有两行内容“abc123”和“你好”FileInputStream fis = new FileInputStream("file.txt");System.out.println(fis.available());System.out.println(fis.getChannel().size());fis.close();}} /* Output:1212*///:~
通过FileChannel的truncate()方法(写入)将此通道的文件截取为给定大小,如下所示:
import java.io.FileInputStream;import java.io.IOException;public class Channel {public static void main(String[] args) throws IOException {truncate();}// 截取文件public static void truncate() throws IOException {// 当前项目下有file.txt文件,有两行内容“abc123”和“你好”RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");FileChannel chnl = raf.getChannel();System.out.println(chnl.size());chnl.truncate(10);System.out.println(chnl.size());raf.close();}} /* Output:1210*///:~
打开file.txt文件可以发现,与原来的文件相比,少了一个“好”。
出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘(本地设备)上。要保证这一点,需要调用force()方法,该方法有一个boolean类型的参数,该参数为true时,强制将所有对此通道的文件更新写入包含该文件的存储设备中。
内存映射文件I/O是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的I/O快得多。通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这最初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会映射(送入)到内存中。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
可以通过FileChannel类中的map方法将一个通道的全部或者部分映射到内存中。查看API了解该方法的使用:
MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)方法 mode根据是按只读、读取/写入或专用来映射文件,分别为FileChannel.MapMode类中所定义的READ_ONLY、READ_WRITE或PRIVATE之一;position文件中的位置,映射区域从此位置开始,必须为非负数;size要映射的区域大小,必须为非负数。对于map方法的第一个参数模式,详细解释如下所示:
READ_ONLY只读:试图修改得到的缓冲区将导致抛出ReadOnlyBufferException;READ_WRITE读/写:对得到的缓冲区的更改最终将传播到文件,该更改对映射到同一文件的其他程序不一定是可见的;PRIVATE专用: 对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其他程序也不是可见的。相反,会创建缓冲区已修改部分的专用副本(写入时的拷贝)。map()方法返回一个MappedByteBuffer,它是ByteBuffer的子类。因此可以像使用ByteBuffer一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。
import java.io.IOException;import java.io.RandomAccessFile;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;import java.nio.channels.FileChannel.MapMode;public class MapChnl {public static void main(String[] args) throws IOException {// 当前项目下有file.txt文件,有两行内容“abc123”和“你好”RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");FileChannel chnl = raf.getChannel();writeMap(chnl);raf.close();}// 读/写模式public static void writeMap(FileChannel chnl) throws IOException {MappedByteBuffer mbb = chnl.map(MapMode.READ_WRITE, 0, 10);mbb.put(0, (byte) 100); // 将第一个字节替换为dSystem.out.println((char) mbb.get(4));}} /* Output:2*///:~
查看文件可以发现原来的"a"被"d"给代替了,接下演示在专用模式的写入:
import java.io.IOException;import java.io.RandomAccessFile;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;import java.nio.channels.FileChannel.MapMode;public class MapChnl {public static void main(String[] args) throws IOException {// 当前项目下有file.txt文件,有两行内容“abc123”和“你好”RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");FileChannel chnl = raf.getChannel();writeMap(chnl);raf.close();}// 专用模式public static void privateMap(FileChannel chnl) throws IOException {MappedByteBuffer mbb = chnl.map(MapMode.PRIVATE, 0, 10);mbb.put(0, (byte) 100); // 绝对写入}} ///:OK~
执行完程序可以发现file.txt并没有发生变化,对FileChannel类的使用大部分就介绍完啦。