Java
语言基础
NIO

Java NIO(二):Channel

简介:通道(Channel)是由`java.nio.channels`包定义的。Channel表示IO源与目标打开的连接。Channel类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。

1. Channel简介

通道(Channel)是由java.nio.channels包定义的。Channel表示IO源与目标打开的连接。Channel类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。Java为Channel接口提供的最主要实现类如下:

  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • SocketChannel:通过TCP读写网络中的数据。
  • ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel。
  • DatagramChannel:通过UDP读写网络中的数据通道。

常见的Channel结构类图如下:

1.Channel结构图.png

通过Channel提供的read()write()方法可以将数据在Channel和Buffer之间进行传递,示意图如下:

2.Buffer与Channel.png

注:Channel与Buffer之间的数据读写经常容易搞混,我们应该理解的是,当Channel中有数据时,表示是可读的(对应于Input),我们可以使用read()方法将它的数据读出来,放置在Buffer中。而当我们传入一个有数据的Buffer给Channel,表示此时Channel需要将这些数据写出的(对应于Output),我们可以使用write数据将Buffer中的数据写出(写出的位置视具体实现而定,可以是文件、打印输出等)。JDK中对读和写的注释是:Reads a sequence of bytes from this channel into the given buffer. Writes a sequence of bytes to this channel from the given buffer.

获取通道的一种方式是对支持通道的对象调用getChannel()方法。支持通道的类有FileInputStream、FileOutputStream、RandomAccessFile、DatagramSocket、Socket、ServerSocket等。另外可以使用Files类的静态方法newByteChannel()获取字节通道。或者通过通道的静态方法open()打开并返回指定通道。

接下来分别介绍上面提到的几种常用的Channel类。

2. FileChannel

FileChannel主要是用于对文件数据进行操作的通道,通过FileInputStream和FileOutputStream都可以获取FileChannel。下面演示一个使用FileChannel进行文件复制的操作,源码如下:

  • // 1.利用channel完成文件的复制
  • private static void copyUseByteBufferAndChannel() {
  • // 定义流和通道
  • FileInputStream fis = null;
  • FileOutputStream fos = null;
  • FileChannel fisc = null;
  • FileChannel fosc = null;
  • try {
  • // 获取将要拷贝的文件的输入流
  • fis = new FileInputStream("/Users/LennonChin/Desktop/1.png");
  • // 获取生成副本文件的输出流
  • fos = new FileOutputStream("/Users/LennonChin/Desktop/2.png");
  • // 获取输入流和输出流的通道
  • fisc = fis.getChannel();
  • fosc = fos.getChannel();
  • // 分配指定大小的缓冲区
  • ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
  • // 循环将输入流对应的通道中的数据读入缓冲区中
  • while (fisc.read(byteBuffer) != -1) {
  • // 将缓冲区切换成读的模式
  • byteBuffer.flip();
  • // 将缓冲区中的数据写入输出流对应的通道
  • fosc.write(byteBuffer);
  • // 清空缓冲区
  • byteBuffer.clear();
  • }
  • } catch (Exception e) {
  • e.printStackTrace();
  • } finally {
  • // 关闭流和通道
  • if (fosc != null) {
  • try {
  • fosc.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (fisc != null) {
  • try {
  • fisc.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (fos != null) {
  • try {
  • fos.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • if (fis != null) {
  • try {
  • fis.close();
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

上面的代码是通过输入输出流来获取通道,然后在通道中进行数据传输的。我们查看getChannel()方法的源码如下:

  • // FileInputStream类中
  • public FileChannel getChannel() {
  • synchronized (this) {
  • if (channel == null) {
  • channel = FileChannelImpl.open(fd, path, true, false, this);
  • }
  • return channel;
  • }
  • }
  • // FileOutputStream类中
  • public FileChannel getChannel() {
  • synchronized (this) {
  • if (channel == null) {
  • channel = FileChannelImpl.open(fd, path, false, true, append, this);
  • }
  • return channel;
  • }
  • }

可以发现,其实它们内部都是使用FileChannelImpl.open()方法来获取通道的。其实我们可以直接通过FileChannel的open()方法来获取通道,改进后的源码如下:

  • private static void copyUseDirectByteBufferAndChannel() {
  • // 定义所需要使用的通道
  • FileChannel fisc = null;
  • FileChannel fosc = null;
  • try {
  • // 获取输入文件和输出文件的通道
  • fisc = FileChannel.open(Paths.get("/Users/LennonChin/Desktop/", "1.png"), StandardOpenOption.READ);
  • fosc = FileChannel.open(Paths.get("/Users/LennonChin/Desktop/", "2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
  • // 将通道对应的文件直接映射到内存缓冲区
  • MappedByteBuffer inMappedBuf = fisc.map(FileChannel.MapMode.READ_ONLY, 0, fisc.size());
  • MappedByteBuffer outMappedBuf = fosc.map(FileChannel.MapMode.READ_WRITE, 0, fosc.size());
  • // 直接对缓冲区进行数据的读写操作
  • byte[] bytes = new byte[inMappedBuf.limit()];
  • inMappedBuf.get(bytes);
  • outMappedBuf.put(bytes);
  • } catch (IOException e) {
  • e.printStackTrace();
  • } finally {
  • // 关闭通道
  • if (fosc != null) {
  • try {
  • fosc.close();
  • } catch (IOException e) {
  • e.printStackTrace();
  • }
  • }
  • if (fisc != null) {
  • try {
  • fisc.close();
  • } catch (IOException e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

在改进的方法中,还使用了通道的map()方法将通道对应的文件直接映射到内存缓冲区,然后通过两个缓冲区的读取和写入来实现文件的复制。

另外,FileChannel还提供了Transfer快捷方法:transferTo()transferFrom(),分别实现将数据从当前可读通道传输到另一个可写通道以及从一个可读通道传输到当前可写通道。下面使用Transfer相关的快捷方法完成拷贝:

  • private static void copyUseDirectByteBufferTransfer() {
  • // 定义所需要使用的通道
  • FileChannel fisc = null;
  • FileChannel fosc = null;
  • try {
  • // 获取输入文件和输出文件的通道
  • fisc = FileChannel.open(Paths.get("/Users/LennonChin/Desktop/", "1.png"), StandardOpenOption.READ);
  • fosc = FileChannel.open(Paths.get("/Users/LennonChin/Desktop/", "2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
  • // 通道间直接传输数据
  • fisc.transferTo(0, fisc.size(), fosc);
  • // fosc.transferFrom(fisc, 0, fisc.size());
  • } catch (IOException e) {
  • e.printStackTrace();
  • } finally {
  • // 关闭通道
  • if (fosc != null) {
  • try {
  • fosc.close();
  • } catch (IOException e) {
  • e.printStackTrace();
  • }
  • }
  • if (fisc != null) {
  • try {
  • fisc.close();
  • } catch (IOException e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

transferTo()中的三个参数分别代表:写出数据的起始位置,写出数据的长度和用于写出的通道。transferFrom()中的三个参数分别代表:读入数据的通道,读入数据的起始位置,读取数据的长度。

3. 聚集写入和分散读取

聚集写入(Gathering Writes)是指将多个Buffer中的数据“聚集”到Channel。分散读取(Scattering Reads)是指从Channel中读取的数据“分散”到多个Buffer中。它们的示意图如下:

3.ScatteringReads和GatheringWrites.png

下面是一个分散读取和聚集写入的例子,使用多个缓冲区进行文件的拷贝:

  • private static void scatterAndGather() {
  • // 定义所需要的文件和通道
  • RandomAccessFile readRandomAccessFile = null;
  • RandomAccessFile writeRandomAccessFile = null;
  • FileChannel readFileChannel = null;
  • FileChannel writeFileChannel = null;
  • try {
  • // 获取读入文件的通道
  • readRandomAccessFile = new RandomAccessFile("/Users/LennonChin/Desktop/test1.json", "r");
  • readFileChannel = readRandomAccessFile.getChannel();
  • // 准备多个缓冲区
  • ByteBuffer byteBuffer1 = ByteBuffer.allocate(64);
  • ByteBuffer byteBuffer2 = ByteBuffer.allocate(512);
  • ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};
  • // 将数据分散读取到多个缓冲区中
  • readFileChannel.read(byteBuffers);
  • // 对所有的缓冲区进行flip操作转换为读模式
  • for (ByteBuffer byteBuffer : byteBuffers) {
  • byteBuffer.flip();
  • }
  • System.out.println(new String(byteBuffers[0].array(), 0, byteBuffers[0].limit()));
  • System.out.println("-------------------------------------------------");
  • System.out.println(new String(byteBuffers[1].array(), 0, byteBuffers[1].limit()));
  • // 获取写出文件的通道
  • writeRandomAccessFile = new RandomAccessFile("/Users/LennonChin/Desktop/test2.json", "rw");
  • writeFileChannel = writeRandomAccessFile.getChannel();
  • // 将多个缓冲区中的数据聚集写入到通道中
  • writeFileChannel.write(byteBuffers);
  • } catch (IOException e) {
  • e.printStackTrace();
  • } finally {
  • // 关闭文件和通道
  • if (writeFileChannel != null) {
  • try {
  • writeFileChannel.close();
  • } catch (IOException e) {
  • e.printStackTrace();
  • }
  • }
  • if (writeRandomAccessFile != null) {
  • try {
  • writeRandomAccessFile.close();
  • } catch (IOException e) {
  • e.printStackTrace();
  • }
  • }
  • if (readFileChannel != null) {
  • try {
  • readFileChannel.close();
  • } catch (IOException e) {
  • e.printStackTrace();
  • }
  • }
  • if (readRandomAccessFile != null) {
  • try {
  • readRandomAccessFile.close();
  • } catch (IOException e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }

对于/Users/LennonChin/Desktop/test1.json文件有以下的内容:

  • {
  • "name": "中国",
  • "province": [
  • {
  • "name": "广东",
  • "cities":
  • {
  • "city": ["广州", "深圳", "珠海"]
  • }
  • },
  • {
  • "name": "台湾",
  • "cities":
  • {
  • "city": ["台北", "高雄"]
  • }
  • },
  • {
  • "name": "新疆",
  • "cities":
  • {
  • "city": ["乌鲁木齐"]
  • }
  • }
  • ]
  • }

运行上述代码可以得到下面的打印:

  • {
  • "name": "中国",
  • "province": [
  • {
  • -------------------------------------------------
  • "name": "广东",
  • "cities":
  • {
  • "city": ["广州", "深圳", "珠海"]
  • }
  • },
  • {
  • "name": "台湾",
  • "cities":
  • {
  • "city": ["台北", "高雄"]
  • }
  • },
  • {
  • "name": "新疆",
  • "cities":
  • {
  • "city": ["乌鲁木齐"]
  • }
  • }
  • ]
  • }

同时可以得到一份内容完全相同的拷贝文件。