Selector多路复用
Selector 是 Java NIO 网络编程的核心。它允许一个线程监听多个 Channel 的 IO 事件,从而避免传统 BIO 中“一个连接一个线程”的高成本模型。
# 1. 多路复用解决什么问题
传统阻塞 IO 中,如果一个线程调用 read() 后没有数据,它会阻塞在那里。为了同时处理多个连接,常见做法是每个连接分配一个线程。
连接数很大时会带来问题:
- 线程数量多,占用内存。
- 上下文切换频繁。
- 大量线程处于阻塞状态,资源利用率低。
Selector 的思路是:
多个 Channel -> 注册到 Selector -> 一个线程等待就绪事件 -> 处理可读/可写/连接事件
# 2. SelectionKey
Channel 注册到 Selector 后会得到一个 SelectionKey。
常见事件:
SelectionKey.OP_ACCEPT:服务端接收连接。SelectionKey.OP_CONNECT:客户端连接完成。SelectionKey.OP_READ:通道可读。SelectionKey.OP_WRITE:通道可写。
一个 Key 代表一个 Channel 在某个 Selector 上的注册关系。
# 3. Selector 服务端骨架
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int len = client.read(buffer);
if (len == -1) {
client.close();
continue;
}
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
}
}
}
这是最小骨架,真实生产代码还需要处理半包、粘包、写缓冲、连接关闭、异常清理、协议解析和线程模型。
# 4. 为什么要 remove
处理 selected key 后必须调用:
iterator.remove();
selectedKeys() 是已就绪事件集合。如果不移除,下一轮循环仍然会处理旧事件,导致重复处理。
# 5. OP_WRITE 的特殊性
OP_WRITE 表示通道当前可写。大多数 TCP 连接在大部分时间都是可写的,如果一直关注 OP_WRITE,Selector 可能不断返回写事件,造成 CPU 空转。
常见做法:
- 只有待发送数据无法一次写完时,才注册
OP_WRITE。 - 写完输出缓冲区后,取消对
OP_WRITE的关注。
# 6. Selector 和 Reactor 模式
基于 Selector 的服务端通常符合 Reactor 模式:
- Reactor:负责监听和分发事件。
- Acceptor:处理新连接。
- Handler:处理读写事件。
- Worker:处理耗时业务逻辑。
Netty 就是基于类似思想构建的高性能网络框架,只是它封装了大量复杂细节。
# 7. 常见问题
- 忘记设置
configureBlocking(false),注册 Selector 时会报错。 - 忘记移除 selected key,导致重复处理。
- 在 IO 线程中执行耗时业务,阻塞整个事件循环。
- 写操作没有处理半包写出。
- 协议解析没有处理 TCP 粘包和拆包。
- 连接异常关闭时没有取消 key 和释放资源。
# 专家实践清单
| 关注点 | 建议 | 风险 |
|---|---|---|
| 事件处理 | IO 线程只做轻量工作 | 业务阻塞拖慢所有连接 |
| 读事件 | 处理返回 -1 和异常 | 连接泄漏 |
| 写事件 | 只在有待写数据时注册 | OP_WRITE 空转 |
| Key 管理 | 关闭连接时取消 key | 无效 key 堆积 |
| 协议解析 | 维护连接级状态 | 粘包拆包错误 |
Selector 事件循环:
select 等待事件
▼
遍历 selectedKeys
▼
处理 accept/read/write/connect
▼
更新兴趣事件
▼
回到 select
# Tips 快问快答
Q:Selector 解决什么问题? A:让一个线程监听多个 Channel 的就绪事件。
Q:Selector 是不是直接读写数据? A:不是,它只告诉你哪些 Channel 就绪,读写仍由 Channel 完成。
Q:OP_READ 表示什么?
A:通道有数据可读或连接状态发生可读相关变化。
Q:为什么 OP_WRITE 不能一直注册?
A:大多数时候 Socket 都可写,长期注册会造成事件循环空转。
Q:selectedKeys 处理完为什么要移除? A:避免下次循环重复处理同一个事件。
Q:Selector 适合阻塞业务逻辑吗? A:不适合,耗时业务应交给业务线程池。
Q:Selector 和 Reactor 有什么关系? A:Selector 是 Java NIO 多路复用基础,Reactor 是基于事件分发的架构模式。
Q:手写 Selector 最大难点是什么? A:连接状态、半包、半写、异常关闭和背压处理。
# 8. 总结
Selector 是 NIO 支撑高并发网络连接的关键。它把“等待多个连接事件”的工作交给一个选择器线程完成,从而显著减少线程数量。掌握 Selector 是理解 Netty、Redis 单线程事件模型、Reactor 模式的重要基础。