通道与流:

  • 通道可以同时进行读和写,而流只能读或者写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲
  • Buffer <—> Channel

java.nio.channels 接口:

  1. public interface Channel extends Closeable {
  2. public boolean isOpen();
  3. public void close() throws IOException;
  4. }
  • isOpen():检查一个通道是否打开
  • close():关闭一个打开的通道

InterruptibleChannel 是一个标记接口,当被通道使用时可以标示该通道是可以中断的(Interruptible)。

打开通道

通道是访问 IO 服务的导管,IO 可以分为广义的两大类别:

  • File IO :文件(file)通道
    • FileChannel 类:用于文件数据读写
  • Stream IO:套接字(socket)通道
    • SocketChannel 类:用于 TCP 客户端数据的读写
    • ServerSocketChannel 类:用于 TCP 服务端数据的读写
    • DatagramChannel 类:用于 UDP 数据的读写

FileChannel

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

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

示例一、本地文件写数据:
未命名绘图.png

  1. public static void main(String[] args) throws Exception {
  2. String str = "世界,你好";
  3. // 1.创建一个输出流
  4. FileOutputStream fos = new FileOutputStream("demo.txt");
  5. // 2.通过 fos 获取对应的 FileChannel
  6. // 注意:FileChannel 的真实类型为其实现类 FileChannelImpl
  7. FileChannel channel = fos.getChannel();
  8. // 3.创建一个缓冲区:ByteBuffer
  9. ByteBuffer buffer = ByteBuffer.allocate(1024);
  10. // 4.将str转换成的字节数组写入缓冲区
  11. buffer.put(str.getBytes());
  12. // 5.缓冲区读写切换
  13. buffer.flip();
  14. // 6.将缓冲区的数据写入通道
  15. channel.write(buffer);
  16. // 7.关闭文件输出流
  17. channel.close();
  18. fos.close();
  19. }

示例二、本地文件读数据:
未命名绘图.png

  1. public static void main(String[] args) throws Exception {
  2. // 1.创建文件输入流
  3. File file = new File("demo.txt");
  4. FileInputStream fis = new FileInputStream(file);
  5. // 2.获取 FileChannel
  6. FileChannel channel = fis.getChannel();
  7. // 3.创建缓冲区 ByteBuffer
  8. // file.length() 获取文件大小
  9. ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
  10. // 4.将数据从通道中写入缓冲区
  11. channel.read(buffer);
  12. // 5.直接通过 buffer.array() 获取缓冲区中的数组
  13. String str = new String(buffer.array());
  14. System.out.println(str);
  15. // 6.关闭文件输入流
  16. channel.close();
  17. fis.close();
  18. }

示例三:使用一个 Buffer 完成文件的读取
未命名绘图.png

  1. public static void main(String[] args) throws Exception {
  2. // 1.创建输入、输出流
  3. File source = new File("source.txt");
  4. FileInputStream fis = new FileInputStream(source);
  5. File target = new File("target.txt");
  6. FileOutputStream fos = new FileOutputStream(target);
  7. // 2.获取 Channel
  8. FileChannel fisChannel = fis.getChannel();
  9. FileChannel fosChannel = fos.getChannel();
  10. // 3.创建缓冲区
  11. ByteBuffer buffer = ByteBuffer.allocate(1024);
  12. // 4.循环读取source中的数据,并放入target中
  13. int read;
  14. // 输入管道中读取数据,read为读取的数据量
  15. while ((read = fisChannel.read(buffer)) != -1) {
  16. // 缓冲区切换到读模式
  17. buffer.flip();
  18. // 将 buffer 中的数据写入输出管道 fosChannel
  19. fosChannel.write(buffer);
  20. // 清空缓冲区
  21. buffer.clear();
  22. }
  23. // 5.关闭流
  24. fisChannel.close();
  25. fis.close();
  26. fosChannel.close();
  27. fos.close();
  28. }

示例四:使用 transferFrom 方法实现文件拷贝

  1. public static void main(String[] args) throws Exception {
  2. // 1.创建输入、输出流
  3. File source = new File("source.txt");
  4. FileInputStream fis = new FileInputStream(source);
  5. File target = new File("target.txt");
  6. FileOutputStream fos = new FileOutputStream(target);
  7. // 2.获取 Channel
  8. FileChannel fisChannel = fis.getChannel();
  9. FileChannel fosChannel = fos.getChannel();
  10. // 3.使用 transferFrom 方法从通道中直接读取数据
  11. fosChannel.transferFrom(fisChannel, 0, source.length());
  12. // 4.关闭通道和流
  13. fisChannel.close();
  14. fis.close();
  15. fosChannel.close();
  16. fos.close();
  17. }

Buffer 和 Channel 使用细节:
1、ByteBuffer 支持类型化的 put 和 get,put 放入的是什么数据类型,get 就应该使用相同的数据类型来取出,否则抛出 BufferUnderflowException 异常

  1. public static void main(String[] args) {
  2. ByteBuffer buffer = ByteBuffer.allocate(1024);
  3. // 1.往缓冲区中存放带类型的值
  4. buffer.putInt(12);
  5. buffer.putFloat(12.02f);
  6. buffer.putDouble(12.03);
  7. buffer.putChar('f');
  8. // 2.从缓冲区获取带类型的参数
  9. // 当获取的类型与存放的类型不一致时,抛出BufferUnderflowException异常
  10. buffer.flip();
  11. System.out.println(buffer.getInt());
  12. System.out.println(buffer.getFloat());
  13. System.out.println(buffer.getDouble());
  14. System.out.println(buffer.getChar());
  15. }

2、可以将一个普通的 Buffer 转成只读 Buffer: Buffer.asReadOnlyBuffer()

  1. public static void main(String[] args) {
  2. ByteBuffer buffer = ByteBuffer.allocate(64);
  3. // 数据填充
  4. for (int i = 0; i < 64; i++) {
  5. buffer.put((byte) i);
  6. }
  7. // 读取
  8. buffer.flip();
  9. // 得到一个只读的 Buffer
  10. ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
  11. System.out.println(readOnlyBuffer.getClass());
  12. // 读取
  13. while (readOnlyBuffer.hasRemaining()) {
  14. System.out.println(readOnlyBuffer.get());
  15. }
  16. }

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

  1. public static void main(String[] args) throws Exception {
  2. // MappedByteBuffer 可让文件直接在堆外内存修改,操作系统不需要拷贝一次
  3. File file = new File("source.txt");
  4. RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
  5. // 获取对应的通道
  6. FileChannel channel = randomAccessFile.getChannel();
  7. /**
  8. * 实际类型 DirectByteBuffer,在操作数据时如果超出范围,抛出 IndexOutOfBoundsException异常
  9. * 参数1:FileChannel.MapMode.READ_WRITE 使用的读写模式
  10. * 参数2:0 可以直接修改的起始位置
  11. * 参数3:file.length() 映射到内存的大小(不是索引位置),即 source.txt 的多少个字节映射到内存,可以直接修改的范围就是 0 - file.length()
  12. */
  13. MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
  14. map.put(0,(byte)'H');
  15. map.put(1,(byte)'E');
  16. // 关闭流
  17. randomAccessFile.close();
  18. }

4、NIO 支持通过多个 Buffer (即 Buffer 数组)完成读写操作,即 Scattering 和 Gatering

  1. public static void main(String[] args) throws Exception {
  2. // Scattering:将数据写入到 buffer,可以采用 buffer 数组,依次写入[分散]
  3. // Gathering:从 buffer 读取数据时,可以采用 buffer 数组,依次读
  4. // 使用 ServerSocketChannel 和 SocketChannel 网络
  5. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  6. InetSocketAddress inetSocketAddress = new InetSocketAddress(8888);
  7. // 绑定端口到socket,并启动
  8. serverSocketChannel.socket().bind(inetSocketAddress);
  9. // 创建buffer数组
  10. ByteBuffer[] buffers = new ByteBuffer[2];
  11. buffers[0] = ByteBuffer.allocate(5);
  12. buffers[1] = ByteBuffer.allocate(5);
  13. // 等待客户端连接
  14. SocketChannel socketChannel = serverSocketChannel.accept();
  15. int messageLength = 10; // 假定从客户端接受10个字节
  16. while (true) {
  17. int read = 0;
  18. while (read < messageLength) {
  19. long l = socketChannel.read(buffers);
  20. read += l; // 累计读取的字符数
  21. // 打印当前buffer的position和limit
  22. Arrays.stream(buffers).map(buffer -> "position=" + buffer.position() + ",limit=" + buffer.limit())
  23. .forEach(System.out::println);
  24. }
  25. Arrays.asList(buffers).forEach(Buffer::flip);
  26. // 将数据回显到客户端
  27. long byteWrite = 0;
  28. while (byteWrite < messageLength) {
  29. long l = socketChannel.write(buffers);
  30. byteWrite += l;
  31. }
  32. // 将所有的buffers进行clear
  33. Arrays.asList(buffers).forEach(Buffer::clear);
  34. }
  35. }