Skip to content

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 存会话。

下一节,我们把这些组件串起来,写一个完整的非阻塞通信服务器。

基于 VitePress 构建