网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

NIO ​

字数: 0 字 时长: 0 分钟

1. NIO 与传统 IO ​

先来看一张传统 IO 和 NIO 的对比图:

image-20250324155641934

传统 IO 基于字节流或字符流进行数据读写,例如:

text
FileInputStream
FileOutputStream
BufferedReader
BufferedWriter

网络通信则主要使用:

text
Socket
ServerSocket

NIO 则主要通过:

text
Channel
    +
Buffer

进行数据读写。

网络通信中还提供了:

text
SocketChannel
ServerSocketChannel
Selector

需要注意:

NIO 的全称是 New I/O。

NIO 提供了非阻塞 IO 的能力,但并不意味着 java.nio 中的所有操作都是非阻塞的。非阻塞和 IO 多路复用的优势,主要体现在网络通信场景中。

简单对比:

对比传统 IONIO
数据处理StreamChannel + Buffer
文件操作FileInputStream、FileReader 等FileChannel + Buffer
网络通信Socket、ServerSocketSocketChannel、ServerSocketChannel
IO 模型主要是阻塞式支持非阻塞
多连接处理通常一个连接对应一个线程Selector 可以管理多个 Channel

2. NIO 与传统 IO 的文件操作 ​

JDK 1.4 中引入了 java.nio,提供了一套新的 Java IO API。

传统 IO 复制文件时,通常通过输入流和输出流完成:

java
BufferedInputStream bis =
        new BufferedInputStream(
                new FileInputStream(source)
        );

BufferedOutputStream bos =
        new BufferedOutputStream(
                new FileOutputStream(des)
        );

byte[] bytes =
        new byte[1024 * 1024];

int len;

while (
        (len = bis.read(bytes)) != -1
) {
    bos.write(
            bytes,
            0,
            len
    );
}

NIO 操作文件时,则主要使用:

text
FileChannel
    +
ByteBuffer

例如:

java
RandomAccessFile read =
        new RandomAccessFile(
                source,
                "rw"
        );

RandomAccessFile write =
        new RandomAccessFile(
                des,
                "rw"
        );

FileChannel readChannel =
        read.getChannel();

FileChannel writeChannel =
        write.getChannel();

ByteBuffer byteBuffer =
        ByteBuffer.allocate(
                1024 * 1024
        );

while (
        readChannel.read(byteBuffer) > 0
) {

    // 切换为读取 Buffer 中数据的状态
    byteBuffer.flip();

    writeChannel.write(
            byteBuffer
    );

    // 清空 Buffer,继续接收数据
    byteBuffer.clear();
}

这里涉及两个非常重要的 NIO 组件:

text
Channel
    ↓
数据传输通道

Buffer
    ↓
数据缓冲区

RandomAccessFile 是传统 java.io 中的类,而 FileChannel 属于 Java NIO。

与传统流不同,FileChannel 通常配合 ByteBuffer 完成数据传输。

2.1 NIO 操作文件一定更快吗? ​

不一定。

对于较小的文件:

text
传统 IO
    ≈
NIO

性能差异通常并不明显。

即使对于大文件,也不能简单认为:

text
NIO 一定比传统 IO 快

实际性能还会受到:

text
缓冲区大小
磁盘性能
操作系统缓存
JDK 实现
具体读写方式

等因素影响。

所以:

NIO 的核心价值并不是“复制文件一定更快”。

文件 IO 只是 NIO 的一个应用场景,它真正明显的优势主要体现在网络 IO 和高并发连接处理中。


3. NIO 在网络 IO 中的优势 ​

IO 操作主要可以分成两个典型场景:

text
IO
├── 文件 IO
└── 网络 IO

而 NIO 的魅力主要体现在:

网络 IO。

传统阻塞式网络 IO 中,通常一个连接需要一个线程进行处理。

大致模型:

text
客户端 1 ──→ 线程 1
客户端 2 ──→ 线程 2
客户端 3 ──→ 线程 3
客户端 4 ──→ 线程 4
...

当连接数量越来越大时,就需要维护大量线程。

随之而来的问题包括:

text
大量线程创建
大量线程销毁
线程上下文切换
线程资源占用

NIO 提供了:

text
非阻塞 IO
    +
IO 多路复用

通过 Selector,一个线程可以同时监控多个 Channel。

大致模型:

text
SocketChannel ──┐
SocketChannel ──┤
SocketChannel ──┼──→ Selector ──→ 一个线程处理事件
SocketChannel ──┤
SocketChannel ──┘

因此,在大量网络连接的场景下,NIO 可以减少线程数量,提高并发处理能力。


4. 传统 IO 网络模型 ​

传统网络 IO 主要使用:

text
ServerSocket
Socket

其中:

4.1 ServerSocket ​

ServerSocket 表示服务器端套接字,主要负责:

text
监听客户端连接
    ↓
accept()
    ↓
创建 Socket
    ↓
与客户端通信

例如:

java
ServerSocket serverSocket =
        new ServerSocket(8080);

while (true) {

    // 阻塞等待客户端连接
    Socket client =
            serverSocket.accept();

    InputStream in =
            client.getInputStream();

    OutputStream out =
            client.getOutputStream();

    byte[] buffer =
            new byte[1024];

    int bytesRead =
            in.read(buffer);

    out.write(
            buffer,
            0,
            bytesRead
    );

    client.close();
}

其中:

java
serverSocket.accept();

默认会阻塞线程,直到有新的客户端建立连接。

读取数据时:

java
in.read(buffer);

同样可能阻塞当前线程。

4.2 Socket ​

Socket 表示客户端套接字,负责与服务器建立 TCP 连接,并进行数据的读写。

所以传统阻塞式网络 IO 可以简单理解为:

text
ServerSocket
    ↓
等待连接
    ↓
Socket
    ↓
InputStream / OutputStream
    ↓
读写数据

5. NIO 网络模型 ​

NIO 网络通信主要使用:

text
ServerSocketChannel
SocketChannel
Selector
ByteBuffer

它们之间大致是这样的关系:

text
                    Selector
                       │
          ┌────────────┼────────────┐
          │            │            │
  SocketChannel SocketChannel SocketChannel
          │            │            │
        Buffer       Buffer       Buffer

5.1 ServerSocketChannel ​

ServerSocketChannel 类似于传统 IO 中的:

text
ServerSocket

用于监听客户端连接。

但它可以设置为非阻塞模式:

java
ServerSocketChannel serverSocketChannel =
        ServerSocketChannel.open();

serverSocketChannel.bind(
        new InetSocketAddress(8081)
);

serverSocketChannel.configureBlocking(
        false
);

5.2 SocketChannel ​

SocketChannel 类似于传统 IO 中的:

text
Socket

负责:

text
建立连接
读取数据
写入数据

同样可以设置为非阻塞模式:

java
client.configureBlocking(false);

5.3 Selector ​

Selector 是 Java NIO 中实现 IO 多路复用 的核心组件。

它可以让一个线程同时监控多个:

text
ServerSocketChannel
SocketChannel

例如:

java
Selector selector =
        Selector.open();

把 ServerSocketChannel 注册到 Selector:

java
serverSocketChannel.register(
        selector,
        SelectionKey.OP_ACCEPT
);

表示我们关注:

text
OP_ACCEPT
    ↓
新的客户端连接事件

当客户端建立连接后,可以再把对应的 SocketChannel 注册到 Selector:

java
client.register(
        selector,
        SelectionKey.OP_READ
);

表示关注:

text
OP_READ
    ↓
数据可读事件

因此,整个 NIO 网络模型可以简单理解为:

text
Channel
    ↓
注册到
    ↓
Selector
    ↓
监听 IO 事件
    ↓
SelectionKey
    ↓
处理对应 Channel

Selector、Channel、Buffer 后面分别单独细讲。


6. NIO 服务端基本流程 ​

一个简单的 NIO 服务端基本流程如下:

java
// 创建 ServerSocketChannel
ServerSocketChannel serverSocketChannel =
        ServerSocketChannel.open();

// 绑定端口
serverSocketChannel.bind(
        new InetSocketAddress(8081)
);

// 设置为非阻塞模式
serverSocketChannel.configureBlocking(
        false
);

// 创建 Selector
Selector selector =
        Selector.open();

// 注册连接事件
serverSocketChannel.register(
        selector,
        SelectionKey.OP_ACCEPT
);

while (true) {

    // 等待 IO 事件
    selector.select();

    Iterator<SelectionKey> iterator =
            selector.selectedKeys().iterator();

    while (
            iterator.hasNext()
    ) {

        SelectionKey key =
                iterator.next();

        iterator.remove();

        if (
                key.isAcceptable()
        ) {

            ServerSocketChannel server =
                    (ServerSocketChannel) key.channel();

            SocketChannel client =
                    server.accept();

            client.configureBlocking(
                    false
            );

            client.register(
                    selector,
                    SelectionKey.OP_READ
            );

        } else if (
                key.isReadable()
        ) {

            SocketChannel client =
                    (SocketChannel) key.channel();

            ByteBuffer buffer =
                    ByteBuffer.allocate(1024);

            int bytesRead =
                    client.read(buffer);

            if (
                    bytesRead == -1
            ) {

                client.close();

            } else {

                buffer.flip();

                client.write(buffer);
            }
        }
    }
}

核心流程可以简化成:

text
1. 创建 ServerSocketChannel
        ↓
2. 设置非阻塞
        ↓
3. 创建 Selector
        ↓
4. Channel 注册到 Selector
        ↓
5. selector.select()
        ↓
6. 获取 SelectionKey
        ↓
7. 判断事件类型
        ↓
8. 处理对应 Channel

7. NIO 的三个核心组件 ​

学习 Java NIO,最核心的三个组件就是:

text
NIO
│
├── Buffer
│     └── 数据存储
│
├── Channel
│     └── 数据传输
│
└── Selector
      └── IO 多路复用

可以先简单理解为:

Buffer ​

text
数据放在哪里?
    ↓
Buffer

例如:

java
ByteBuffer

详细内容见:[[Buffer]]

Channel ​

text
数据通过什么传输?
    ↓
Channel

常见的 Channel:

text
FileChannel
SocketChannel
ServerSocketChannel

详细内容见:[[Channel]]

Selector ​

text
一个线程怎么管理多个网络连接?
    ↓
Selector

Selector 主要用于:

text
监听多个 Channel
    ↓
发现 IO 事件
    ↓
处理对应连接

详细内容见:[[Selector]]


8. 小结 ​

传统 IO 和 NIO 的核心区别,可以先记住:

text
传统 IO
    ↓
Stream
    ↓
Socket / ServerSocket
    ↓
主要采用阻塞式处理


NIO
    ↓
Channel + Buffer
    ↓
SocketChannel / ServerSocketChannel
    ↓
支持非阻塞模式
    ↓
Selector
    ↓
IO 多路复用

在文件 IO 场景下:

NIO 不意味着一定比传统 IO 更快。

而在大量网络连接的场景下:

text
传统 IO
    ↓
大量连接
    ↓
大量线程


NIO
    ↓
多个 Channel
    ↓
Selector
    ↓
少量线程管理多个连接

因此,NIO 的核心价值主要体现在:

非阻塞网络 IO + IO 多路复用,提高大量并发连接下的处理能力。

Noctiflorous Garden · MIT License