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 | |
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 | |