From b4c823dc04d8bc5f3a39c04cb94d77d932d74b7e Mon Sep 17 00:00:00 2001 From: Guide Date: Mon, 26 Jun 2023 23:56:33 +0800 Subject: [PATCH] =?UTF-8?q?[docs=20update]=E5=AE=8C=E5=96=84NIO=20?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E7=9F=A5=E8=AF=86=E6=80=BB=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/.vuepress/sidebar/index.ts | 2 +- docs/database/redis/redis-questions-01.md | 4 +- docs/home.md | 1 + docs/java/io/io-model.md | 4 +- docs/java/io/nio-basis.md | 319 ++++++++++------------ 6 files changed, 147 insertions(+), 184 deletions(-) diff --git a/README.md b/README.md index 2db51f0b..55c95676 100755 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ - [IO 基础知识总结](./docs/java/io/io-basis.md) - [IO 设计模式总结](./docs/java/io/io-design-patterns.md) - [IO 模型详解](./docs/java/io/io-model.md) +- [NIO 核心知识总结](./docs/java/io/nio-basis.md) ### 并发 diff --git a/docs/.vuepress/sidebar/index.ts b/docs/.vuepress/sidebar/index.ts index f1b8f0dc..b0ca14da 100644 --- a/docs/.vuepress/sidebar/index.ts +++ b/docs/.vuepress/sidebar/index.ts @@ -126,7 +126,7 @@ export default sidebar({ prefix: "io/", icon: "code", collapsible: true, - children: ["io-basis", "io-design-patterns", "io-model"], + children: ["io-basis", "io-design-patterns", "io-model", "nio-basis"], }, { text: "JVM", diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index 4041cfe3..088bf2db 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -105,7 +105,9 @@ Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来 - **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。 - **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。相关阅读:[《我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了!》](https://mp.weixin.qq.com/s/kyFAWH3mVNJvurQDt4vchA)。 -- **消息队列**:Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。 +- **消息队列**:Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。 +- **延时队列**:Redisson 内置了延时队列(基于 sorted set 实现的)。 +- **分布式 Session** :利用 string 或者 hash 保存 Session 数据,所有的服务器都可以访问。 - **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。 - ...... diff --git a/docs/home.md b/docs/home.md index 7f74e551..4d79f5d5 100644 --- a/docs/home.md +++ b/docs/home.md @@ -72,6 +72,7 @@ title: JavaGuide(Java学习&面试指南) - [IO 基础知识总结](./java/io/io-basis.md) - [IO 设计模式总结](./java/io/io-design-patterns.md) - [IO 模型详解](./java/io/io-model.md) +- [NIO 核心知识总结](./java/io/nio-basis.md) ### 并发 diff --git a/docs/java/io/io-model.md b/docs/java/io/io-model.md index 8e093270..9c37d760 100644 --- a/docs/java/io/io-model.md +++ b/docs/java/io/io-model.md @@ -104,7 +104,7 @@ IO 多路复用模型中,线程首先发起 select 调用,询问内核数据 Java 中的 NIO ,有一个非常重要的**选择器 ( Selector )** 的概念,也可以被称为 **多路复用器**。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。 -![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f483f2437ce4ecdb180134270a00144~tplv-k3u1fbpfcp-watermark.image) +![Buffer、Channel和Selector三者之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer-selector.png) ### AIO (Asynchronous I/O) @@ -118,7 +118,7 @@ AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。 -![](https://images.xiaozhuanlan.com/photo/2020/33b193457c928ae02217480f994814b6.png) +![BIO、NIO 和 AIO 对比](https://oss.javaguide.cn/github/javaguide/java/nio/bio-aio-nio.png) ## 参考 diff --git a/docs/java/io/nio-basis.md b/docs/java/io/nio-basis.md index 7347700e..a3ae07a3 100644 --- a/docs/java/io/nio-basis.md +++ b/docs/java/io/nio-basis.md @@ -1,39 +1,52 @@ -在学习 `NIO` 之前,需要先了解一下计算机 `I/O`模型的基础理论知识。还不了解的话,可以参考这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)。 +--- +title: Java NIO 核心知识总结 +category: Java +tag: + - Java IO + - Java基础 +--- -## `NIO` 简介 +在学习 NIO 之前,需要先了解一下计算机 I/O模型的基础理论知识。还不了解的话,可以参考我写的这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html)。 -在传统的 `Java I/O` 模型(`BIO`)中,`I/O` 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 `I/O` 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈。 +## NIO 简介 -为了解决这个问题,在` Java `1.4 版本引入了一种新的` I/O` 模型 —— `Java NIO` (`New IO`,也称为 `Non-blocking IO`) 。`Java NIO` 弥补了原来同步阻塞`I/O`的不足,它在标准 `Java` 代码中提供了高速的、面向块的 `I/O`。定义包含数据的类,并通过以块的形式处理这些数据。`NIO` 包含三个核心的组件:`Buffer`(缓冲区)、`Channel`(通道)、`Selector`(多路复用器)。 +在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。 -![图片来着《面试官:Java NIO 了解?》](.\images\NIO\640.jpg) +为了解决这个问题,在Java1.4 版本引入了一种新的 I/O 模型 — **NIO** (New IO,也称为 Non-blocking IO) 。NIO 弥补了同步阻塞I/O的不足,它在标准 Java 代码中提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个连接,大大提高了 I/O 效率和并发。 -在开始介绍 `NIO` 的核心组件之前,可以先回顾一下`I/O` 模型中的同步非阻塞 `I/O` (`Non-blocking IO`)和 `I/O` 多路复用。读完文章之后,思考下对应 `Java NIO` 属于哪一种 `I/O` 模型? +下图是 BIO、NIO 和 AIO 处理客户端请求的简单对比图(关于 AIO 的介绍,可以看我写的这篇文章:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html),不是重点,了解即可)。 -## `NIO` 核心组件 +![BIO、NIO 和 AIO 对比](https://oss.javaguide.cn/github/javaguide/java/nio/bio-aio-nio.png) -`Java NIO` 主要包括以下三个核心组件:`Buffer`(缓冲区)、`Channel`(通道)和 `Selector`(选择器)。下面分别介绍这三个组件。 +⚠️需要注意:使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。 -### `Buffer`(缓冲区) +## NIO 核心组件 -在传统的 `BIO` 中,数据的读写是面向流的,通过各种输入/输出 `Stream` 对象实现数据的读写。`BIO` 中有一个庞大的 "`stream` 家族" 应用于各种场景的数据读写: +NIO 主要包括以下三个核心组件: -![BIO "stream 家族"](.\images\NIO\image-20230618202236755.png) +- **Buffer(缓冲区)**:NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer中,而写操作时将 Buffer中的数据写入到 Channel 中。 +- **Channel(通道)**:Channel 是一个双向的、可读可写的数据传输通道,NIO 通过Channel来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。 +- **Selector(选择器)**:允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到Selector上,由Selector来分配线程来处理事件。 +三者的关系如下图所示(暂时不理解没关系,后文会详细介绍): +![Buffer、Channel和Selector三者之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer-selector.png) -在`Java` 1.4 的 `NIO` 库中,所有数据都是用缓冲区处理的,这是新库和之前的 `BIO` 的一个重要区别。与 `BIO` 流式处理的 `Stream` 家族相对应,新库引入了 `Buffer` 对象。见名思义:`Buffer` 是一个缓存区,在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。 使用 `NIO` 在读写数据时,都是通过缓冲区进行操作。 +下面详细介绍一下这三个组件。 +### Buffer(缓冲区) +在传统的 BIO 中,数据的读写是面向流的, 分为字节流和字符流。 -◼ Buffer是抽象类,它有如下子类: -`ByteBuffer` 、`CharBuffer` 、`ShortBuffer` 、`IntBuffer`、`LongBuffer` 、`FloatBuffer` 、`DoubleBuffer` ;通过名称就可以看出来,这些子类用来存储对应类型的数据。 +在Java 1.4 的 NIO库中,所有数据都是用缓冲区处理的,这是新库和之前的 BIO 的一个重要区别,有点类似于 BIO 中的缓冲流。NIO 在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。 使用 NIO在读写数据时,都是通过缓冲区进行操作。 +`Buffer` 的子类如下图所示。其中,最常用的是 `ByteBuffer`,它可以用来存储和操作字节数据。 +![Buffer 的子类](https://oss.javaguide.cn/github/javaguide/java/nio/buffer-subclasses.png) -![NIO ”Buffer 家族“](.\images\NIO\image-20230620235630296.png) +你可以将 Buffer 理解为一个数组,`IntBuffer`、`FloatBuffer`、`CharBuffer` 等分别对应 `int[]`、`float[]`、`char[]` 等。 -◼ `Buffer` 中的四个成员变量: +为了更清晰地认识缓冲区,我们来简单看看`Buffer` 类中定义的四个成员变量: ~~~java public abstract class Buffer { @@ -45,23 +58,33 @@ public abstract class Buffer { } ~~~ -1. 容量(`capacity`):`Buffer`可以存储的最大数据量,该值不可改变; -2. 界限(`limit`):`Buffer` 中可以读/写数据的边界,`limit` 之后的数据不能访问; -3. 位置(`position`):下一个可以被读写的数据的位置(索引); +这四个成员变量的具体含义如下: + +1. 容量(`capacity`):`Buffer`可以存储的最大数据量,`Buffer`创建时设置且不可改变; +2. 界限(`limit`):`Buffer` 中可以读/写数据的边界。写模式下,`limit` 代表最多能写入的数据,一般等于 `capacity`(可以通过`limit(int newLimit) `方法设置);读模式下,`limit` 等于 Buffer 中实际写入的数据大小。 +3. 位置(`position`):下一个可以被读写的数据的位置(索引)。从写操作模式到读操作模式切换的时候(flip),`position` 都会归零,这样就可以从头开始读写了。 4. 标记(`mark`):`Buffer`允许将位置直接定位到该标记处,这是一个可选属性; -并且,上述变量满足如下的关系:0 <= mark <= position <= limit <= capacity 。 +并且,上述变量满足如下的关系:**0 <= mark <= position <= limit <= capacity** 。 -![Buffer 成员变量关系](.\images\image-20230615221211005.png) +另外,Buffer 有读模式和写模式这两种模式,分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。Buffer 被创建之后默认是写模式,调用 `flip()` 可以切换到读模式。如果要再次切换回写模式,可以调用 `clear()` 或者 `compact()` 方法。 -◼ `Buffer` 对象不能通过 `new` 调用构造方法创建对象 ,只能通过静态方法实例化 `Buffer`。下面是 `ByteBuffer `类中创建示例提供的静态方法: +![position 、limit 和 capacity 之前的关系](https://oss.javaguide.cn/github/javaguide/java/nio/JavaNIOBuffer.png) + +![position 、limit 和 capacity 之前的关系](https://oss.javaguide.cn/github/javaguide/java/nio/NIOBufferClassAttributes.png) + + `Buffer` 对象不能通过 `new` 调用构造方法创建对象 ,只能通过静态方法实例化 `Buffer`。 + +这里以 `ByteBuffer `为例进行介绍: ~~~java -public static ByteBuffer allocate(int capacity); // 分配堆内存 -public static ByteBuffer allocateDirect(int capacity); // 分配直接内存 +// 分配堆内存 +public static ByteBuffer allocate(int capacity); +// 分配直接内存 +public static ByteBuffer allocateDirect(int capacity); ~~~ -◼ `Buffer` 最核心的两个方法: + Buffer 最核心的两个方法: 1. `get` : 读取缓冲区的数据 2. `put` :向缓冲区写入数据 @@ -70,8 +93,9 @@ public static ByteBuffer allocateDirect(int capacity); // 分配直接内存 - `flip` :将缓冲区从写模式切换到读模式,它会将 `limit` 的值设置为当前 `position` 的值,将 `position` 的值设置为 0。 - `clear`: 清空缓冲区,将缓冲区从读模式切换到写模式,并将 `position` 的值设置为 0,将 `limit` 的值设置为 `capacity` 的值。 +- ...... -Buffer中数据变化的过程: +Buffer 中数据变化的过程: ~~~java import java.nio.*; @@ -116,192 +140,123 @@ public class CharBufferDemo { } ~~~ -打印结果: +输出: -~~~tex +~~~bash 初始状态: -capacity: 8, limit: 8, position: 0, mark 开始读取的字符:(8个空字符) -~~~ +capacity: 8, limit: 8, position: 0 -![初始状态](.\images\NIO\image-20230618215703502.png) - -~~~tex 写入3个字符后的状态: -capacity: 8, limit: 8, position: 3, mark 开始读取的字符:(5个空字符) -~~~ +capacity: 8, limit: 8, position: 3 -![写入3个字符后的状态](.\images\NIO\image-20230618215834167.png) +准备读取buffer中的数据! -~~~tex 调用flip()方法后的状态: -capacity: 8, limit: 3, position: 0, mark 开始读取的字符: abc -~~~ +capacity: 8, limit: 3, position: 0 -![调用flip方法后的状态](.\images\NIO\image-20230618220051437.png) +读取到的数据:abc - - -~~~tex 调用clear()方法后的状态: -capacity: 8, limit: 8, position: 0, mark 开始读取的字符: abc(5个空字符) +capacity: 8, limit: 8, position: 0 ~~~ +为了帮助理解,我绘制了一张图片展示 `capacity`、`limit`和`position`每一阶段的变化。 + +![capacity、limit和position每一阶段的变化](https://oss.javaguide.cn/github/javaguide/java/nio/NIOBufferClassAttributesDataChanges.png) + +### Channel(通道) + +Channel 是一个通道,它建立了与数据源(如文件、网络套接字等)之间的连接。我们可以利用它来读取和写入数据,就像打开了一条自来水管,让数据在 Channel 中自由流动。 + +BIO 中的流是单向的,分为各种 `InputStream`(输入流)和 `OutputStream`(输出流),数据只是在一个方向上传输。通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写。 + +Channel 与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer中,而写操作时将 Buffer中的数据写入到 Channel 中。 + +![Channel 和 Buffer之间的关系](https://oss.javaguide.cn/github/javaguide/java/nio/channel-buffer.png) + +另外,因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。 + +`Channel` 的子类如下图所示。 + +![Channel 的子类](https://oss.javaguide.cn/github/javaguide/java/nio/channel-subclasses.png) + +其中,最常用的是以下几种类型的通道: + +- `FileChannel`:文件访问通道; +- `SocketChannel`、`ServerSocketChannel`:TCP通信通道; +- `DatagramChannel`:UDP 通信通道; + +![Channel继承关系图](https://oss.javaguide.cn/github/javaguide/java/nio/channel-inheritance-relationship.png) -![调用clear方法后的状态](.\images\NIO\image-20230618220217580.png) + Channel 最核心的两个方法: +1. `read` :用于从 Buffer 中读取数据; +2. `write` :向 Buffer 中写入数据。 - - - -### `Channel`(通道) - -`Channel` 是一个通道,它代表着与数据源(如文件、网络套接字等)之间的连接。可以通过它读取和写入数据,它就像自来水管一样,网络数据通过 `Channel` 读取和写入。 - -`BIO` 中的 `Stream` 是单向的,分为各种 `InputStream`(输入流)和 `OutputStream`(输出流),数据只是在一个方向上传输。 `Channel`(通道)与流的不同之处在于`Channel`是双向的,它可以用于读、写或者同时用于读写。 - -因为 `Channel` 是全双工的,所以它可以比流更好地映射底层操作系统的 `API` 。特别是在 `UNIX` 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。 - - - -在 `Java NIO` 中,主要有以下几种类型的通道: - -◼ Channel的实现类: - -1. `FileChannel`:文件访问通道; -2. `SocketChannel`、`ServerSocketChannel`:`TCP`通信通道; -3. `DatagramChannel`:`UDP`通信通道; -4. `Pipe.SourceChannel`、`Pipe.SinkChannel`:线程通信通道。 - - - -![channel 类继承图](.\images\NIO\image-20230620115120520.png) - - - -◼ `Channel` 的实例化: - -1. 各个 `Channel` 类提供的 `open()` 方法; - -- 打开文件的 `FileChannel`: +这里我们以 `FileChannel` 为例演示一下是读取文件数据的。 ~~~java -FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ); +RandomAccessFile reader = new RandomAccessFile("/Users/guide/Documents/test_read.in", "r")) +FileChannel channel = reader.getChannel(); +ByteBuffer buffer = ByteBuffer.allocate(1024); +channel.read(buffer); ~~~ -- 建立网络连接的 `ServerSocketChannel`: +### Selector(选择器) -~~~java -ServerSocketChannel serverChannel = ServerSocketChannel.open(); -~~~ +Selector(选择器) 是 NIO中的一个关键组件,它允许一个线程处理多个 Channel。Selector 是基于事件驱动的 I/O 多路复用模型,主要运作原理是:通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel上面有新的 TCP 连接接入、读和写事件,这个 Channel就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel加入到就绪集合中。通过 SelectionKey可以获取就绪 Channel的集合,然后对这些就绪的 Channel进行响应的 I/O 操作。 +![Selector 选择器工作示意图](https://oss.javaguide.cn/github/javaguide/java/nio/selector-channel-selectionkey.png) +一个多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK使用了 `epoll()` 代替传统的 `select` 实现,所以它并没有最大连接句柄 `1024/2048` 的限制。这也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。 -2. `BIO` 的各种`Stream`提供了`getChannel()`方法,可以直接返回 `FileChannel`。 - -~~~java -FileInputStream inputStream = new FileInputStream("source.txt"); -FileChannel inputChannel = inputStream.getChannel(); -~~~ - - - - -◼ `Channel` 的方法: - -1. `map()`方法用于将 `Channel` 对应的数据映射成 `ByteBuffer`; -2. `read()` 方法有一系列重载的形式,用于从 `Buffer` 中读取数据; -3. `write()` 方法有一系列重载的形式,用于向 `Buffer` 中写入数据。 - - - -◼ 使用`Channel`复制数据的代码示例: - -~~~java -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; - -public class FileChannelExample { - public static void main(String[] args) throws Exception { - RandomAccessFile file = new RandomAccessFile("D:/source.txt", "rw"); - FileChannel channel = file.getChannel(); - ByteBuffer buffer = ByteBuffer.allocate(1024); - - // 从通道读取数据到 Buffer 中 - int bytesRead = channel.read(buffer); - while (bytesRead != -1) { - System.out.println("Read " + bytesRead + " bytes"); - buffer.flip(); - while (buffer.hasRemaining()) { - System.out.print((char) buffer.get()); - } - buffer.clear(); - bytesRead = channel.read(buffer); - } - - String data = "Hello, FileChannel!"; - buffer.put(data.getBytes()); - buffer.flip(); - // 向文件中追加写入数据 - int bytesWrite = channel.write(buffer); - System.out.println("\nwrite " + bytesWrite + " bytes"); - - channel.close(); - file.close(); - } -} -~~~ - - - - - -### `Selector`(选择器) - - - -`Selector`(选择器) 是 `Java NIO`中的一个关键组件,它允许一个线程处理多个 `Channel`。`Selector` 是基于事件驱动的 `I/O` 多路复用模型,主要运作原理是:通过 `Selector` 注册通道的事件,`Selector` 会不断地轮询注册在其上的 `Channel`。当事件发生时,比如:某个 `Channel` 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。`Selector` 会将相关的 `Channel` 加入到就绪集合中。通过 `SelectionKey` 可以获取就绪 `Channel` 的集合,然后对这些就绪的 `Channel` 进行响应的 `I/O` 操作。 - -![IO多路复用模型](.\images\NIO\image-20230621004820823.png) - -一个多路复用器 `Selector` 可以同时轮询多个 `Channel`,由于 `JDK` 使用了 `epoll()` 代替传统的 `select` 实现,所以它并没有最大连接句柄 1024/2048 的限制。这也就意味着只需要一个线程负责 `Selector` 的轮询,就可以接入成千上万的客户端。 - - - -◼监听事件类型 - -`Selector` 可以监听以下四种事件类型: +Selector 可以监听以下四种事件类型: 1. `SelectionKey.OP_ACCEPT`:表示通道接受连接的事件,这通常用于 `ServerSocketChannel`。 2. `SelectionKey.OP_CONNECT`:表示通道完成连接的事件,这通常用于 `SocketChannel`。 3. `SelectionKey.OP_READ`:表示通道准备好进行读取的事件,即有数据可读。 4. `SelectionKey.OP_WRITE`:表示通道准备好进行写入的事件,即可以写入数据。 +`Selector `是抽象类,可以通过调用此类的 `open()` 静态方法来创建 Selector 实例。Selector 可以同时监控多个 `SelectableChannel` 的 `IO` 状况,是非阻塞 `IO` 的核心。 +一个Selector 实例有三个 `SelectionKey` 集合: -◼`SelectionKey` 集合 - -`Selector `是抽象类,可以通过调用此类的 `open()` 静态方法来创建 `Selector` 实例。`Selector` 可以同时监控多个 `SelectableChannel` 的 `IO` 状况,是非阻塞 `IO` 的核心。一个`Selector` 实例有三个 `SelectionKey` 集合: - -1. 所有的 `SelectionKey` 集合:代表了注册在该 `Selector` 上的 `Channel`,这个集合可以通过 `keys()` 方法返回。 +1. 所有的 `SelectionKey` 集合:代表了注册在该 Selector 上的 `Channel`,这个集合可以通过 `keys()` 方法返回。 2. 被选择的 `SelectionKey` 集合:代表了所有可通过 `select()` 方法获取的、需要进行 `IO` 处理的 Channel,这个集合可以通过 `selectedKeys()` 返回。 -3. 被取消的 `SelectionKey` 集合:代表了所有被取消注册关系的 `Channel`,在下一次执行 `select()` 方法时,这些 `Channel` 对应的 `SelectionKey` 会被彻底删除,程序通常无须直接访问该集合。 +3. 被取消的 `SelectionKey` 集合:代表了所有被取消注册关系的 `Channel`,在下一次执行 `select()` 方法时,这些 `Channel` 对应的 `SelectionKey` 会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。 +简单演示一下如何遍历被选择的 `SelectionKey` 集合并进行处理: +```java +Set selectedKeys = selector.selectedKeys(); +Iterator keyIterator = selectedKeys.iterator(); +while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key != null) { + if (key.isAcceptable()) { + // ServerSocketChannel 接收了一个新连接 + } else if (key.isConnectable()) { + // 表示一个新连接建立 + } else if (key.isReadable()) { + // Channel 有准备好的数据,可以读取 + } else if (key.isWritable()) { + // Channel 有空闲的 Buffer,可以写入数据 + } + } + keyIterator.remove(); +} +``` -◼`select()` 相关的方法 +Selector 还提供了一系列和 `select()` 相关的方法: -`Selector` 还提供了一系列和 `select()` 相关的方法: +- `int select()`:监控所有注册的 `Channel`,当它们中间有需要处理的 `IO` 操作时,该方法返回,并将对应的 `SelectionKey` 加入被选择的 `SelectionKey` 集合中,该方法返回这些 `Channel` 的数量。 +- `int select(long timeout)`:可以设置超时时长的 `select()` 操作。 +- `int selectNow()`:执行一个立即返回的 `select()` 操作,相对于无参数的 `select()` 方法而言,该方法不会阻塞线程。 +- `Selector wakeup()`:使一个还未返回的 `select()` 方法立刻返回。 +- ...... -1. `int select()`:监控所有注册的 `Channel`,当它们中间有需要处理的 `IO` 操作时,该方法返回,并将对应的 `SelectionKey` 加入被选择的 `SelectionKey` 集合中,该方法返回这些 `Channel` 的数量。 -2. `int select(long timeout)`:可以设置超时时长的 `select()` 操作。 -3. `int selectNow()`:执行一个立即返回的 `select()` 操作,相对于无参数的 `select()` 方法而言,该方法不会阻塞线程。 -4. `Selector wakeup()`:使一个还未返回的 `select()` 方法立刻返回。 - - - -◼使用 Selector 实现网络读写的简单示例: +使用 Selector 实现网络读写的简单示例: ~~~java import java.io.IOException; @@ -384,12 +339,8 @@ public class NioSelectorExample { 在示例中,我们创建了一个简单的服务器,监听8080端口,使用 Selector 处理连接、读取和写入事件。当接收到客户端的数据时,服务器将读取数据并将其打印到控制台,然后向客户端回复 "Hello, Client!"。 - - ## NIO 零拷贝 -DirectBuffer是Java NIO库中提供的一种可以直接操作内存的缓冲区类型。 - 零拷贝是提升 IO 操作性能的一个常用手段,像 ActiveMQ、Kafka 、RocketMQ、QMQ、Netty 等顶级开源项目都用到了零拷贝。 零拷贝是指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷贝时间。也就是说,零拷贝主主要解决操作系统在处理 I/O 操作时频繁复制数据的问题。零拷贝的常见实现技术有: `mmap+write`、`sendfile`和 `sendfile + DMA gather copy` 。 @@ -407,8 +358,8 @@ DirectBuffer是Java NIO库中提供的一种可以直接操作内存的缓冲区 Java 对零拷贝的支持: -- `MappedByteBuffer` 是 NIO 基于内存映射(mmap)这种零拷⻉⽅式的提供的⼀种实现,它的底层是调用了 Linux 内核的 `mmap` 。 -- `FileChannel` 的`transferTo()/transferFrom()`调用了 Linux 内核的 `sendfile`。关于`FileChannel`的用法可以看看这篇文章:[Java NIO 文件通道 FileChannel 用法](https://www.cnblogs.com/robothy/p/14235598.html)。 +- `MappedByteBuffer` 是 NIO 基于内存映射(`mmap`)这种零拷⻉⽅式的提供的⼀种实现,底层实际是调用了 Linux 内核的 `mmap` 系统调用。它可以将一个文件或者文件的一部分映射到内存中,形成一个虚拟内存文件,这样就可以直接操作内存中的数据,而不需要通过系统调用来读写文件。 +- `FileChannel` 的`transferTo()/transferFrom()`是 NIO 基于发送文件(`sendfile`)这种零拷贝方式的提供的一种实现,底层实际是调用了 Linux 内核的 `sendfile`系统调用。它可以直接将文件数据从磁盘发送到网络,而不需要经过用户空间的缓冲区。关于`FileChannel`的用法可以看看这篇文章:[Java NIO 文件通道 FileChannel 用法](https://www.cnblogs.com/robothy/p/14235598.html)。 代码示例: @@ -425,10 +376,18 @@ private void loadFileIntoMemory(File xmlFile) throws IOException { } ``` +## 总结 + +这篇文章我们主要介绍了 NIO 的核心知识点,包括 NIO 的核心组件和零拷贝。 + +如果我们需要使用 NIO 构建网络程序的话,不建议直接使用原生 NIO,编程复杂且功能性太弱,推荐使用一些成熟的基于 NIO 的网络编程框架比如 Netty。Netty 在 NIO 的基础上进行了一些优化和扩展比如支持多种协议、支持 SSL/TLS 等等。 + ## 参考 - Java NIO浅析: - 面试官:Java NIO 了解?https://mp.weixin.qq.com/s/mZobf-U8OSYQfHfYBEB6KA +- Java NIO:Buffer、Channel 和 Selector:https://www.javadoop.com/post/java-nio + \ No newline at end of file