NIO 核心:Buffer、Channel、Selector
hardNIOBufferChannelSelectorByteBufferFileChannel
NIO 由三驾马车构成:Buffer(数据容器)、Channel(数据通道)、Selector(多路复用器)。本篇深入讲解这三者的工作原理和常见陷阱。
Buffer(缓冲区)
三个关键指针
Buffer 的核心是一个字节数组加上三个指针:
初始状态(容量 10,空)
capacity = 10
position = 0
limit = 10
│◄─────────── capacity = 10 ──────────────►│
│ 0 1 2 3 4 5 6 7 8 9 │
│▲ │
│position=0 │
│ ▲│
│ limit=10│
写入 5 个字节后:
position = 5
limit = 10
│ d1 d2 d3 d4 d5 ░ ░ ░ ░ ░ │
│ ▲ │
│ position=5 │
flip()(切换为读模式):
position = 0
limit = 5
│ d1 d2 d3 d4 d5 ░ ░ ░ ░ ░ │
│▲ ▲ │
│position=0 limit=5 │
读完后 clear()(重置为写模式):
position = 0
limit = capacity = 10
| 指针 | 说明 |
|---|---|
capacity |
Buffer 总容量,创建后不变 |
position |
当前读/写位置,自动推进 |
limit |
有效数据边界(写模式:= capacity;读模式:= 写入的数据量) |
关键方法
ByteBuffer buf = ByteBuffer.allocate(10);
// 写入数据
buf.put((byte) 'H');
buf.put((byte) 'i');
// position = 2, limit = 10
// 切换为读模式
buf.flip();
// position = 0, limit = 2
// 读取数据
while (buf.hasRemaining()) {
byte b = buf.get(); // 每次读取后 position++
}
// 重置(清空,可重新写)
buf.clear();
// position = 0, limit = 10(数据还在,只是指针重置)
// 或者 compact():未读的数据移到头部,继续写
buf.compact();
常见陷阱
// ❌ 错误:忘记 flip(),读的是空数据
ByteBuffer buf = ByteBuffer.allocate(10);
buf.put("hi".getBytes());
channel.write(buf); // 写出去 0 字节!position 到 limit 没有数据
// ✅ 正确
buf.flip(); // position=0, limit=2
channel.write(buf); // 写出 "hi"
直接缓冲区 vs 堆缓冲区
// 堆缓冲区(常用)
ByteBuffer heap = ByteBuffer.allocate(1024);
// 直接缓冲区(Zero-copy)
ByteBuffer direct = ByteBuffer.allocateDirect(1024);
| 类型 | 分配位置 | GC | 适用场景 |
|---|---|---|---|
| 堆缓冲区 | JVM 堆 | 会被 GC | 一般场景 |
| 直接缓冲区 | 操作系统内存 | 不受 GC 管理 | 频繁 I/O、Netty |
直接缓冲区省去了"内核空间 → 用户空间"的一次数据拷贝,I/O 性能更好,但分配/释放成本高,适合长生命周期的 Buffer。
Channel(通道)
Channel 的特点
- 双向:既可读又可写(部分 Channel 单向)
- 非阻塞:可以配置为非阻塞模式
- 与 Buffer 配合:Channel 不直接操作数据,通过 Buffer 中转
常用 Channel 类型
| Channel | 用途 |
|---|---|
FileChannel |
文件读写,不支持非阻塞 |
SocketChannel |
TCP 客户端 |
ServerSocketChannel |
TCP 服务端(监听连接) |
DatagramChannel |
UDP 通信 |
FileChannel 使用示例
// 读文件
try (FileChannel channel = FileChannel.open(Path.of("input.txt"))) {
ByteBuffer buf = ByteBuffer.allocate(4096);
while (channel.read(buf) != -1) {
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear();
}
}
// 写文件
try (FileChannel channel = FileChannel.open(
Path.of("output.txt"),
StandardOpenOption.WRITE,
StandardOpenOption.CREATE)) {
ByteBuffer buf = ByteBuffer.wrap("Hello NIO".getBytes());
channel.write(buf);
}
Channel 的零拷贝:transferTo
// 零拷贝:文件数据直接从内核 page cache 传输,不经过用户空间
try (FileChannel src = FileChannel.open(Path.of("src.txt"));
FileChannel dest = FileChannel.open(Path.of("dest.txt"),
StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
src.transferTo(0, src.size(), dest);
// 底层调用 sendfile()/sendfile64(),数据不经过用户态 Buffer
}
零拷贝的意义:
普通文件传输(有用户态拷贝):
磁盘 → 内核缓冲区 → 用户缓冲区 → 内核 Socket 缓冲区 → 网卡(4 次拷贝)
transferTo(零拷贝):
磁盘 → 内核缓冲区 → 网卡(2 次拷贝,或支持 DMA Gather 时 1 次)
Netty 的 FileRegion、Kafka 的网络传输都用了零拷贝原理。
Selector(选择器)
工作流程
1. 创建 Selector
Selector selector = Selector.open();
2. 注册 Channel(非阻塞)
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
3. 阻塞等待就绪
int readyCount = selector.select(); // 阻塞,直到至少一个 Channel 就绪
4. 遍历就绪 Channel
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey k = iter.next();
iter.remove(); // ⚠️ 必须手动移除
if (k.isReadable()) { ... }
}
三个 select 方法
// 阻塞,直到至少有一个 Channel 就绪
selector.select();
// 阻塞,最多等待 timeout 毫秒
selector.select(1000);
// 非阻塞,立即返回,可能返回 0
selector.selectNow();
SelectionKey 详解
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
// 关注的事件(注册时指定)
int interestOps = key.interestOps();
// 就绪的事件(select 后获取)
int readyOps = key.readyOps();
// 直接判断方法
key.isAcceptable(); // OP_ACCEPT 就绪
key.isConnectable(); // OP_CONNECT 就绪
key.isReadable(); // OP_READ 就绪
key.isWritable(); // OP_WRITE 就绪
// 获取通道
SocketChannel channel = (SocketChannel) key.channel();
// 附加对象(常用于绑定业务对象)
key.attach(mySession); // 绑定
MySession session = (MySession) key.attachment(); // 取出
为什么 selectedKeys 要手动 remove?
// ❌ 不移除的后果
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 不移除 key
process(key);
}
// 下次 select() 返回时,selectedKeys 中还有上次的 key(即使事件已处理)
// → 导致重复处理
// ✅ 正确:每次处理后立即移除
iter.remove();
Selector 的 selectedKeys 是已就绪事件的集合,select() 只负责向里面添加,不负责清除,所以需要我们手动 remove。
完整 NIO 服务端示例
public class NioServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Server started on 8080");
while (true) {
selector.select(); // 阻塞等就绪
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须移除
try {
if (key.isAcceptable()) {
handleAccept(server, selector);
} else if (key.isReadable()) {
handleRead(key);
}
} catch (IOException e) {
key.cancel();
key.channel().close();
}
}
}
}
static void handleAccept(ServerSocketChannel server, Selector selector)
throws IOException {
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("New connection: " + client.getRemoteAddress());
}
static void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int n = channel.read(buf);
if (n == -1) {
// 客户端关闭
channel.close();
return;
}
buf.flip();
byte[] data = new byte[buf.remaining()];
buf.get(data);
System.out.println("Received: " + new String(data));
// 回写
buf.rewind();
channel.write(buf);
}
}