Channel 通道的简介

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

 

应用程序与磁盘之间的数据写入或者读出,都需要由用户地址空间和内存地址空间之间来回复制数据,内存地址空间中的数据通过操作系统层面的IO接口,完成与磁盘的数据存取。在应用程序调用这些系统IO接口时,由CPU完成一系列调度、任务分配,早先这些IO接口都是由CPU独立负责。所以当发生大规模读写请求时,CPU的占用率很高。

之后,操作系统为了避免CPU完全被各种IO接口调用占用,引入了DMA(直接存储器存储)。当应用程序对操作系统发出一个读写请求时,会由DMA先向CPU申请权限,申请到权限之后,内存地址空间与磁盘之间的IO操作就全由DMA来负责操作。这样,在读写请求的过程中,CPU不需要再参与,CPU去做其他事情。当然,DMA来独立完成数据在磁盘与内存空间中的来去,需要借助于DMA总线。但是当DMA总线过多时,大量的IO操作也会造成总线冲突,即也会影响最终的读写性能。

为了避免DMA总线冲突对性能的影响,后来便有了通道的方式。通道,它是一个完全独立的处理器。CPU是中央处理器,通道本身也是一个处理器,专门负责IO操作。既然是处理器,通道有自己的IO命令,与CPU无关。它更适用于大型的IO操作,性能更高。

Java的NIO的通道类似流,但是又有一些不同: 

  • 1、既可以从Channel中读数据也可以往Channel里面写数据;但是流的读写一般是单向的。 
  • 2、Channel可以异步的读写。
  • 3、Channel的读写是通过Buffer这个中介实现的。数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

下面是Java的NIO的最重要的几个channel的实现:

  • 1、FileChannel:主要是用于文件的读写。
  • 2、DatagramChannel:主要用于UDP读写网络中的数据。
  • 3、SocketChannel:通过TCP读写网络中的数据。
  • 4、ServerSocketChannel:主要用于服务端,可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

FileChannel 类

FileChannel主要用来对本地文件进行 IO 操作,常见的方法有

  • public int read(ByteBuffer dst):从通道读取数据并放到缓冲区中。
  • public int write(ByteBuffer src):把缓冲区的数据写到通道中。
  • public long transferFrom(ReadableByteChannel src, long position, long count):从目标通道中复制数据到当前通道。
  • public long transferTo(long position, long count, WritableByteChannel target):把数据从当前通道复制给目标通道。

应用实例1——本地文件写数据

使用ByteBuffer(缓冲) 和 FileChannel(通道), 将 "hello,明天" 写入到file01.txt 中

public class NIOFileChannel01 {
    public static void main(String[] args) throws Exception{
        String str = "hello,明天";
        //创建一个输出流->channel
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");

        //通过 fileOutputStream 获取 对应的 FileChannel
        //这个 fileChannel 真实 类型是  FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();

        //创建一个缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //将 str 放入 byteBuffer
        byteBuffer.put(str.getBytes());

        //对byteBuffer 进行flip
        byteBuffer.flip();

        //将byteBuffer 数据写入到 fileChannel
        fileChannel.write(byteBuffer);
        fileOutputStream.close();
    }
}

应用实例2——本地文件读数据

使用ByteBuffer(缓冲) 和 FileChannel(通道), 将 file01.txt 中的数据读入到程序,并显示在控制台屏幕

public class NIOFileChannel02 {
    public static void main(String[] args) throws Exception {
        //创建文件的输入流
        File file = new File("d:\\file01.txt");
        FileInputStream fileInputStream = new FileInputStream(file);

        //通过fileInputStream 获取对应的FileChannel -> 实际类型  FileChannelImpl
        FileChannel fileChannel = fileInputStream.getChannel();

        //创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());

        //将 通道的数据读入到Buffer
        fileChannel.read(byteBuffer);

        //将byteBuffer 的 字节数据 转成String
        System.out.println(new String(byteBuffer.array()));
        fileInputStream.close();
    }
}

应用实例3——使用一个Buffer完成文件读取

使用 FileChannel(通道) 和 方法 read , write,完成文件的拷贝

public class NIOFileChannel03 {
    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("input.txt");
        FileChannel fileChannel01 = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("input2.txt");
        FileChannel fileChannel02 = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        while (fileChannel01.read(byteBuffer) != -1) { //循环读取
            //将buffer中的数据写入到 fileChannel02 -- 2.txt
            byteBuffer.flip();
            fileChannel02.write(byteBuffer);
            byteBuffer.clear(); //清空buffer
        }
        //关闭相关的流
        fileInputStream.close();
        fileOutputStream.close();
    }
}

应用实例4——拷贝文件transferFrom 方法

使用 FileChannel(通道) 和 方法 transferFrom ,完成文件的拷贝

public class NIOFileChannel04 {
    public static void main(String[] args)  throws Exception {
        //创建相关流
        FileInputStream fileInputStream = new FileInputStream("d:\\a.jpg");
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg");

        //获取各个流对应的filechannel
        FileChannel sourceCh = fileInputStream.getChannel();
        FileChannel destCh = fileOutputStream.getChannel();

        //使用transferForm完成拷贝
        destCh.transferFrom(sourceCh,0,sourceCh.size());
        //关闭相关通道和流
        sourceCh.close();
        destCh.close();
        fileInputStream.close();
        fileOutputStream.close();
    }
}

关于Buffer 和 Channel的注意事项和细节

  • 1、ByteBuffer 支持类型化的put 和 get,put 放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。

  • 2、可以将一个普通Buffer 转成只读Buffer。

  • 3、NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO 来完成。

  • 4、前面我们讲的读写操作,都是通过一个Buffer 完成的,NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering。

MappedByteBuffer案例:

MappedByteBuffer修改文件

/**
 * MappedByteBuffer 可让文件直接在内存(堆外内存)修改,
 * 操作系统不需要在内核空间和用户空间来回的拷贝
 */
public class MappedByteBufferTest {

    public static void main(String[] args) throws Exception {
        RandomAccessFile randomAccessFile = new RandomAccessFile("input.txt", "rw");
        //获取对应的通道
        FileChannel channel = randomAccessFile.getChannel();

        /**
         * 参数1:FileChannel.MapMode.READ_WRITE 使用的读写模式
         * 参数2:0 可以直接修改的起始位置
         * 参数3: 5 是映射到内存的大小(不是索引位置) ,即将 input.txt 的多少个字节映射到内存
         * 可以直接修改的范围就是 0-5
         * 实际类型 DirectByteBuffer
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        mappedByteBuffer.put(0,(byte) 'H');
        mappedByteBuffer.put(3,(byte) '9');
        //mappedByteBuffer.put(5,(byte) 'Y');//IndexOutOfBoundsException

        randomAccessFile.close();
        System.out.println("修改成功");
    }
}

 

参考: https://my.oschina.net/happyBKs/blog/1595866