NIO实践与原理

Java NIO是 Java 1.4 引入的一组新的 I/O API,相较于传统的 Java IO(即 BIO,Blocking I/O),它提供了更高效、非阻塞的 I/O 操作方式。它的核心思想是面向缓冲区和非阻塞,结合 Selector 实现高效的事件驱动模型,更适合用于高并发、高性能的网络编程场景,例如服务器端通信、文件传输等。

Java NIO 的核心组件

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

Channel(通道)

Channel 是对传统 IO 流的改进,可以同时进行读写操作。 Channel 是双向的,可以读和写,并且支持异步读/写。它必须从 Buffer 读取或写入数据。Channel 可以通过 configureBlocking(false) 方法配置为非阻塞模式。常见的Channel类型:

  • FileChannel:用于文件读写。它只能在阻塞模式下运行,其他 3 个 Channel 都可以配置成非阻塞模式。
  • SocketChannel:用于 TCP 客户端通信。
  • ServerSocketChannel:用于 TCP 服务端监听连接。
  • DatagramChannel:用于 UDP 通信。
1
2
3
// 示例:打开一个 FileChannel
RandomAccessFile file = new RandomAccessFile("file.txt", "rw");
FileChannel channel = file.getChannel();

Buffer(缓冲区)

Buffer 是一个容器对象,本质上是一块内存,内部实现是一个数组,用于向 Channel 写入和读取数据。

  • Buffer 的几个关键属性

    • capacity:容量,最大存储数据量。
    • position:当前位置,下一次读写的位置。
    • limit:限制,表示可操作的数据长度。
    • mark:标记位置,可用于重置 position。
  • Buffer 的常用方法:

    • flip():切换为读模式(将 limit 设置为当前 position,position 设置为 0)。
    • clear():清空 buffer,准备重新写入。
    • rewind():重置 position 为 0,重新读取 buffer 中的数据。
  • Buffer使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 创建一个 1024 字节的缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 写入数据
    buffer.put("Hello, NIO!".getBytes());
    // 切换到读模式
    buffer.flip();
    while (buffer.hasRemaining()) {
    System.out.print((char) buffer.get());
    }
  • Buffer源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    // Invariants: mark <= position <= limit <= capacity  
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;

    public byte get() {
    return hb[ix(nextGetIndex())];
    }

    // 切换为读模式
    public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
    }

    // 切换为写模式
    public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
    }

    public final Buffer mark() {
    mark = position;
    return this;
    }

    public final Buffer reset() {
    int m = mark;
    if (m < 0)
    throw new InvalidMarkException();
    position = m;
    return this;
    }

    public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
    }

    // 清空
    final void truncate() {
    position = 0;
    limit = 0;
    capacity = 0;
    mark = -1;
    }

rewind ()方法主要是调整了缓冲区的position属性与mark属性。 mark() + reset() 实现相同功能,rewind()方法与flip()方法很相似,区别在于倒带方法rewind()不会影响limit属性值,而翻转方法flip()会重设limit属性值。

Selector(选择器)

这Selector是用来实现多路复用 I/O,它可以监听多个 Channel 是否有事件就绪(如连接、读、写)。这样就可以通过一个线程管理多个 Channel,从而管理多个网络连接。

通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。

一个多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK 使用了 epoll() 代替传统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制。这也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。

Selector 可以监听以下四种事件类型:

  • SelectionKey.OP_ACCEPT:表示通道接受连接的事件,这通常用于 ServerSocketChannel
  • SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于 SocketChannel
  • SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读。
  • SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。

一个 Selector 实例有三个 SelectionKey 集合:

  • 所有的 SelectionKey 集合:代表了注册在该 Selector 上的 Channel,这个集合可以通过 keys() 方法返回。
  • 被选择的 SelectionKey 集合:代表了所有可通过 select() 方法获取的、需要进行 IO 处理的 Channel,这个集合可以通过 selectedKeys() 返回。
  • 被取消的 SelectionKey 集合:代表了所有被取消注册关系的 Channel,在下一次执行 select() 方法时,这些 Channel 对应的 SelectionKey 会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。

核心方法

  • register(Selector sel, int ops):Channel 的方法,用于注册 Channel 到 Selector,让 Selector 多路复用地监听 Channel 上感兴趣的事件。第二个参数可以是上面 4 种类型的事件中的一种或几种。SelectionKey:注册后返回的选择键,表示 Channel 在 Selector 上的注册信息,当中包含这些方法:

    • interestOps():监听事件的集合
    • readyOps():当前收到的事件集合
    • channel():被注册的通道
    • selector():注册到的选择器
    • attachment():附加的一个对象(可选)。在 Reactor 模式中,会把 Acceptor 添加到 Selector 中,Acceptor 是用于处理客户端连接的组件,attach 到 Selector 上之后就可以在客户端连接事件到达时取出 Acceptor,处理客户端连接。
  • select() :阻塞当前线程,直到至少有一个 Channel 在这个 Selector 上注册的事件就绪。返回当前就绪的 Channel 的数量。

  • selectedKeys():返回已经就绪的通道的选择键,通常在 select() 方法之后调用,获取就绪的 SelectionKey,然后遍历它们处理 IO 事件。可以通过 selectedKeys().iterator().next().channel() 方法遍历和访问这些通道。

StandardSocketOptions

  • SO_KEEPALIVE: 设置长连接
  • SO_BACKLOG: 设置等待连接队列的最大长度
  • SO_SNDBUF: 设置发送缓冲区的大小
  • SO_RCVBUF: 设置接收缓冲区的大小
  • SO_REUSEADDR/SO_REUSEPORT: 允许重用本地地址和端口(通常用于快速重启服务器)
  • SO_LINGER:当启用了 SO_LINGER 并设置了非零的 linger 时间(以秒为单位),套接字在调用 close() 后不会立即返回。套接字会尝试将所有排队的数据发送出去。如果在这段时间内未能成功发送所有数据,则强制关闭套接字,并丢弃剩余的数据。如果在这段时间内成功发送了所有数据,则正常关闭套接字。当禁用了 SO_LINGER 或设置了 linger 时间为零,套接字在调用 close() 后会立即返回。任何排队的数据都会被丢弃,可能导致数据丢失。
  • TCP_NODELAY: 禁用 Nagle 算法,确保数据立即发送而不是合并成更大的数据包
  • IP_TOS: 设置 IP 数据报头中的服务类型字段(Type of Service)
  • IP_MULTICAST_IF
  • IP_MULTICAST_TTL
  • IP_MULTICAST_LOOP

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SimpleNIOServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);

while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = socketChannel.read(buffer);
}
}
}
}
}

NIO实践与原理
http://example.com/2025/06/14/network-NIO实践与原理/
作者
ares
发布于
2025年6月14日
许可协议