一、NIO 的介绍

1.1、什么是 Java NIO?


Java NIO (No-blocking IO) 一种同步非阻塞的I/O模型, 为了补充Java标准IO(BIO) 的不足在Java 1.4 中引入,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。

1.2、如何理解BIO、NIO、AIO的区别?

  • Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不作任何事情会造成不必要的线程开销。
  • Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求会被注册到多路复用器上,多路复用器轮询到有 I/O 请求就会进行处理。
  • Java AIO:异步非阻塞,AIO 引入了异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

二、NIO 相关概念

2.1、流与块


I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

面向流的 I/O 一次处理一个字节数据: 一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。

面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

I/O 包和 NIO 已经很好地集成了,java.io. 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io. 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。

2.2、Buffer(缓冲区)

  • 定义

Buffer对象可以被称为有固定容量的容器。它充当储存罐或临时暂存区,可以存储数据并在以后检索数据。缓冲器与通道密切配合。通道是进行I / O传输的实际门户; 缓冲区是这些数据传输的来源或目标。对于向外传输,您要发送的数据将放在缓冲区中,该缓冲区将传递到输出通道。对于向内传输,通道将数据存储在您提供的缓冲区中,然后将数据从缓冲区复制到通道中。在协作对象之间切换缓冲区是NIO API下有效数据处理的关键。

  • 类图

image.png

  • Buffer 的特点

    • 缓冲区的容量是固定的(在创建Buffer时,初始化容量)
    • 默认情况下,缓冲区是只读的,只有选中的缓冲区是可写的
    • 缓冲区是通道的端点
    • 在只读的缓冲区中, 其保存的内容是不可变的,但缓冲区的属性是可变的
    • 默认情况下,缓冲区是线程不安全的
  • Buffer 的数据结构(线性表形式)

Java IO 之 NIO 【三】 - 图2

  • Buffer 属性介绍

    1. // Invariants: mark <= position <= limit <= capacity
    2. private int mark = -1;
    3. private int position = 0;
    4. private int limit;
    5. private int capacity;
    • Capacity: 缓冲区的最大容量,在缓冲区创建时初始化,初始化后不能更改
    • Limit: 缓冲区可读写的范围, 即缓冲区实际数据大小
    • Position: 指出当前缓冲区可读或可写的位置,在缓冲区创建时初始化为0
    • Mark: 用于保存Position 位置,调用mark() 方法保存当前读取或写入的位置:mark = position, 调用reset( ) 方法恢复读取或写入的位置:position = mark。
  • Buffer 的操作图示

image.png
Java IO 之 NIO 【三】 - 图4

2.3、Channel(通道)

  • Channel 与 Buffer 交互图示

Java IO 之 NIO 【三】 - 图5

  • 定义

Channel是一种在字节缓冲区(Buffer)和通道另一端的实体(数据源)(通常是文件(磁盘IO)或套接字socket(网络IO))之间有效传输数据的媒介。

  • Channel 接口

    1. public interface Channel extends Closeable {
    2. //通道是否已开启
    3. public boolean isOpen();
    4. //用于关闭通道
    5. public void close() throws IOException;
    6. }
  • 类层次结构图

image.png

  • 文件拷贝例子 ```java //创建输入流 FileInputStream input = new FileInputStream (“/home/hdj/IdeaProjects/demoio/src/main/resources/copyToFile.txt”); //获取读取通道 ReadableByteChannel source = input.getChannel();

//创建输出流 FileOutputStream output = new FileOutputStream (“/home/hdj/IdeaProjects/demoio/src/main/resources/copyToFile4.txt”); //获取写入通道 WritableByteChannel dest = output.getChannel();

//创建直接缓冲区 ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);

while (source.read(buffer) != -1) { // 转换缓冲区为可读模式 //即使用limit 变量指定可读区域(范围position < read < limit) buffer.flip();

  1. // 保证读取全部缓冲区中的元素
  2. while (buffer.hasRemaining())
  3. {
  4. dest.write(buffer);
  5. }
  6. //标识缓冲区为空状态,表示可写入
  7. //position =0, limit=capacity
  8. //数据实际上并没有置空
  9. buffer.clear();

} //关闭通道 source.close(); dest.close();

  1. <a name="U9daU"></a>
  2. ###
  3. <a name="wiATs"></a>
  4. ### 2.4、Selector (选择器)
  5. - 定义
  6. Selector 是一个可选择通道的多路复用器,用作可以进入非阻塞模式的特殊通道(Channel), 它可以检查一个或多个NIO通道(Channel),并确定哪个通道准备好进行通信,即读或写。
  7. 那为什么要使用Selector 呢?原因是 Selector 用于单线程处理多个通道;因此,selector 能使用较少的线程来处理多个通道(Channel), 因为线程的切换比较耗资源,因此可用Selector 来提高效率。
  8. - 图示
  9. ![](https://cdn.nlark.com/yuque/0/2022/png/438760/1642058279694-7cd534fe-0a3c-4000-8aae-3c4255be615f.png#clientId=uc16e80cd-82bd-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u01981d4b&margin=%5Bobject%20Object%5D&originHeight=403&originWidth=541&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=uf9a161db-1fca-4926-94e6-e55cfb8ba90&title=)
  10. - NIO Socket 操作
  11. ```java
  12. public class NIOServer {
  13. public static void main(String[] args) throws IOException {
  14. //创建选择器
  15. Selector selector = Selector.open();
  16. ServerSocketChannel ssChannel = ServerSocketChannel.open();
  17. ssChannel.configureBlocking(false);
  18. //channel 注册选择器模式
  19. ssChannel.register(selector, SelectionKey.OP_ACCEPT);
  20. ServerSocket serverSocket = ssChannel.socket();
  21. InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
  22. serverSocket.bind(address);
  23. while (true) {
  24. selector.select();
  25. Set<SelectionKey> keys = selector.selectedKeys();
  26. Iterator<SelectionKey> keyIterator = keys.iterator();
  27. while (keyIterator.hasNext()) {
  28. SelectionKey key = keyIterator.next();
  29. if (key.isAcceptable()) {
  30. ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
  31. // 服务器会为每个新连接创建一个 SocketChannel
  32. SocketChannel sChannel = ssChannel1.accept();
  33. sChannel.configureBlocking(false);
  34. // 这个新连接主要用于从客户端读取数据
  35. sChannel.register(selector, SelectionKey.OP_READ);
  36. } else if (key.isReadable()) {
  37. SocketChannel sChannel = (SocketChannel) key.channel();
  38. System.out.println(readDataFromSocketChannel(sChannel));
  39. sChannel.close();
  40. }
  41. keyIterator.remove();
  42. }
  43. }
  44. }
  45. private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
  46. ByteBuffer buffer = ByteBuffer.allocate(1024);
  47. StringBuilder data = new StringBuilder();
  48. while (true) {
  49. buffer.clear();
  50. int n = sChannel.read(buffer);
  51. if (n == -1) {
  52. break;
  53. }
  54. buffer.flip();
  55. int limit = buffer.limit();
  56. char[] dst = new char[limit];
  57. for (int i = 0; i < limit; i++) {
  58. dst[i] = (char) buffer.get(i);
  59. }
  60. data.append(dst);
  61. buffer.clear();
  62. }
  63. return data.toString();
  64. }
  65. }
  1. public class NIOClient {
  2. public static void main(String[] args) throws IOException {
  3. Socket socket = new Socket("127.0.0.1", 8888);
  4. OutputStream out = socket.getOutputStream();
  5. String s = "hello world";
  6. out.write(s.getBytes());
  7. out.close();
  8. }
  9. }

2.4、MappeteBuffer

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

参考