Selector 选择器
10000 个连接,你会怎么管理?
如果是 BIO 思维,你会创建 10000 个线程,每个线程蹲守一个连接。这样能工作——前提是你的服务器有足够的内存。
但 10000 个线程意味着什么?每个线程默认栈大小约 1MB,光栈空间就要 10GB 内存。这还没算线程切换的 CPU 开销。
Selector 换了一种思路:不养闲人,谁有活叫谁。
Selector 的核心思想:多路复用
BIO 模型:每个连接一个线程蹲守
线程 1 ──→ 连接 1(大部分时间在等)
线程 2 ──→ 连接 2(大部分时间在等)
...
线程 10000 ──→ 连接 10000(大部分时间在等)
→ 10000 个线程,上下文切换成了性能瓶颈
NIO + Selector 模型:一个人管着所有连接
Selector(单线程)
├── 连接 1(没数据?继续等)
├── 连接 2(有数据!处理一下)
└── 连接 10000(没数据?继续等)
→ 1 个线程搞定所有连接Selector 就是一个事件监听器。你告诉它「帮我看着这些连接,有情况叫我」,然后它就帮你盯着。当某个 Channel 有数据就绪时,Selector 会通知你,你再去处理。
基本用法:四步走
java
// 1. 创建 Selector
Selector selector = Selector.open();
// 2. 把 Channel 注册到 Selector
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 必须是非阻塞模式
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// register() 返回 SelectionKey,记录这个 Channel 和 Selector 的关系
// 3. 阻塞等待就绪事件
while (selector.select() > 0) {
// 4. 获取就绪的 Channel,逐个处理
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
// 有新连接请求
handleAccept(key);
}
if (key.isReadable()) {
// 有数据可读
handleRead(key);
}
if (key.isWritable()) {
// 可以写数据
handleWrite(key);
}
}
// 5. 清空已处理的事件(重要!)
selector.selectedKeys().clear();
}select() 是阻塞方法,直到至少有一个 Channel 就绪才返回。返回值为就绪的 Channel 数量。
SelectionKey:关注什么事件
注册 Channel 时需要指定关注的事件类型:
java
// 四种事件类型
SelectionKey.OP_ACCEPT // 接受连接(ServerSocketChannel 用)
SelectionKey.OP_CONNECT // 连接建立(SocketChannel 客户端用)
SelectionKey.OP_READ // 可读
SelectionKey.OP_WRITE // 可写
// 可以组合关注
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
// SelectionKey 的常用方法
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
key.isAcceptable(); // 是否可接受
key.isReadable(); // 是否可读
key.isWritable(); // 是否可写
key.isConnectable(); // 连接是否完成
key.cancel(); // 取消关注
key.channel(); // 获取对应的 Channel
key.selector(); // 获取对应的 Selector
key.attach(new SessionData()); // 附加自定义对象完整示例:Echo 服务器
java
public class EchoServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
// 启动服务端
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Echo 服务器启动,端口 8080");
while (true) {
selector.select(); // 阻塞等待事件
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
// 处理新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("新连接: " + client);
}
if (key.isReadable()) {
// 处理读
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = client.read(buffer);
if (read > 0) {
buffer.flip();
client.write(buffer); // Echo 回去
} else if (read == -1) {
client.close();
}
}
}
selector.selectedKeys().clear();
}
}
}attach():附加上下文
每个 SelectionKey 可以附加一个对象,常用来存储会话数据:
java
// 注册时附加对象
SocketChannel channel = server.accept();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
key.attach(new SessionData()); // 附加会话数据
// 处理时取出对象
if (key.isReadable()) {
SessionData session = (SessionData) key.attachment();
// 用 session 存储的状态继续处理
}这在处理复杂协议时特别有用——你可以把解析状态、缓冲区等关联到连接上。
踩坑指南:这些坑我都踩过
坑一:selectedKeys() 必须手动清空
java
// ❌ 错误:不清空会导致死循环
while (true) {
selector.select();
for (SelectionKey key : selector.selectedKeys()) {
if (key.isReadable()) {
// 处理
}
}
// 没有清空,selectedKeys() 每次都返回同一个 key
// 结果:同一个事件被处理 N 次
}
// ✅ 正确:每次处理完必须清空
while (true) {
selector.select();
for (SelectionKey key : selector.selectedKeys()) {
if (key.isReadable()) {
// 处理
}
}
selector.selectedKeys().clear(); // 必须清空
}坑二:取消 key 后需要同步清空
java
// 当关闭 Channel 时,对应的 key 会自动失效
// 但它还在 selectedKeys() 集合里,需要手动移除
channel.close();
iterator.remove(); // 必须从 selectedKeys() 集合中移除最佳实践是用迭代器遍历,然后调用 iterator.remove()。
记住这个口诀:
注册要非阻塞,select 要清空,attachment 存会话。
下一节,我们把这些组件串起来,写一个完整的非阻塞通信服务器。
