网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

Selector ​

字数: 0 字 时长: 0 分钟

Selector 是 Java NIO 中用于实现 I/O 多路复用 的核心组件。

它可以让一个线程同时监听多个 Channel 的 I/O 事件,例如:

text
连接事件
读事件
写事件

可以简单理解为:

text
多个 Channel
      ↓
   Selector
      ↓
一个线程统一监听

传统阻塞 IO 中,通常需要为每个连接分配一个线程:

text
客户端 1 → 线程 1
客户端 2 → 线程 2
客户端 3 → 线程 3
客户端 4 → 线程 4

而使用 Selector 后:

text
SocketChannel 1 ─┐
SocketChannel 2 ─┤
SocketChannel 3 ─┼→ Selector → 一个线程
SocketChannel 4 ─┘

这也是 NIO 在高并发网络通信中的核心优势之一。


1. Selector、Channel、Buffer 的关系 ​

Java NIO 中三个核心组件:

text
Buffer
    ↓
存储数据

Channel
    ↓
传输数据

Selector
    ↓
监听和管理多个 Channel

整体关系:

text
                Selector
                   │
         ┌─────────┼─────────┐
         │         │         │
      Channel   Channel   Channel
         │         │         │
      Buffer    Buffer    Buffer

可以简单记:

Buffer 存数据,Channel 传数据,Selector 管 Channel。


2. 创建 Selector ​

通过:

java
Selector.open()

创建一个 Selector:

java
Selector selector = Selector.open();

此时 Selector 还没有监听任何 Channel。

需要将 Channel 注册到 Selector:

java
channel.register(selector, SelectionKey.OP_READ);

这样 Selector 才能监听这个 Channel 上发生的事件。


3. Channel 注册到 Selector ​

并不是所有 Channel 都可以注册到 Selector。

常见可以注册的 Channel:

text
ServerSocketChannel
SocketChannel
DatagramChannel

例如:

java
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.configureBlocking(false);

serverSocketChannel.register(
        selector,
        SelectionKey.OP_ACCEPT
);

这里非常重要:

Channel 注册到 Selector 之前,必须设置为非阻塞模式。

也就是:

java
channel.configureBlocking(false);

否则注册时会抛出:

text
IllegalBlockingModeException

例如:

java
SocketChannel socketChannel = SocketChannel.open();

socketChannel.configureBlocking(false);

socketChannel.register(
        selector,
        SelectionKey.OP_READ
);

4. SelectionKey ​

Channel 注册到 Selector 后,会返回一个:

java
SelectionKey

例如:

java
SelectionKey key = socketChannel.register(
        selector,
        SelectionKey.OP_READ
);

SelectionKey 可以理解为:

Channel 在 Selector 中的注册凭证。

它记录了:

text
哪个 Channel
注册在哪个 Selector
关注哪些事件
当前发生了哪些事件

关系:

text
Channel
   ↓
register()
   ↓
Selector
   ↓
SelectionKey

5. Selector 监听的事件 ​

Java NIO 中常见的事件有:

java
SelectionKey.OP_ACCEPT
SelectionKey.OP_CONNECT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

5.1 OP_ACCEPT ​

java
SelectionKey.OP_ACCEPT

表示:

有新的客户端连接到达。

主要用于:

java
ServerSocketChannel

例如:

java
serverSocketChannel.register(
        selector,
        SelectionKey.OP_ACCEPT
);

5.2 OP_CONNECT ​

java
SelectionKey.OP_CONNECT

表示:

客户端连接服务器的操作已经准备完成。

主要用于:

java
SocketChannel

非阻塞连接。


5.3 OP_READ ​

java
SelectionKey.OP_READ

表示:

Channel 中有数据可以读取。

例如:

java
socketChannel.register(
        selector,
        SelectionKey.OP_READ
);

5.4 OP_WRITE ​

java
SelectionKey.OP_WRITE

表示:

Channel 当前可以继续写数据。

主要用于处理网络发送缓冲区暂时写不完的情况。

常用事件可以总结为:

事件含义
OP_ACCEPT有新的连接
OP_CONNECT连接建立完成
OP_READ数据可读
OP_WRITE数据可写

6. interestOps 和 readyOps ​

SelectionKey 中有两个比较重要的概念:

text
interestOps
readyOps

6.1 interestOps ​

表示:

我们希望 Selector 关注哪些事件。

例如:

java
SelectionKey.OP_READ

表示我们希望监听:

text
读事件

注册时指定:

java
socketChannel.register(
        selector,
        SelectionKey.OP_READ
);

也可以同时关注多个事件:

java
int interestSet =
        SelectionKey.OP_READ |
        SelectionKey.OP_WRITE;

6.2 readyOps ​

表示:

当前真正已经准备好的事件。

通常不需要直接操作 readyOps(),而是通过下面这些方法判断:

java
key.isAcceptable();
key.isConnectable();
key.isReadable();
key.isWritable();

例如:

java
if (key.isReadable()) {
    // 处理读取事件
}

7. selector.select() ​

注册完成以后,通过:

java
selector.select();

等待 I/O 事件发生。

例如:

java
while (true) {
    selector.select();

    // 处理事件
}

select() 会阻塞当前线程,直到至少有一个注册的 Channel 出现感兴趣的事件。

需要注意:

NIO 的“非阻塞”并不是说代码中完全没有阻塞。

例如:

java
selector.select();

本身就可以阻塞线程。

区别在于:

text
传统 BIO
    ↓
线程阻塞在某一个具体连接上

Selector
    ↓
一个线程等待多个 Channel 的事件

8. select 的几个方法 ​

Selector 常用的等待方法:

8.1 select() ​

java
selector.select();

阻塞等待,直到有事件发生。

8.2 select(long timeout) ​

java
selector.select(3000);

最多阻塞指定时间。

例如:

text
3000ms

如果期间没有事件,超时后也会返回。

8.3 selectNow() ​

java
selector.selectNow();

立即返回,不阻塞。

如果当前没有事件:

text
返回 0

9. selectedKeys ​

当:

java
selector.select();

返回后,可以通过:

java
selector.selectedKeys();

获取已经发生事件的 SelectionKey。

例如:

java
Set<SelectionKey> selectedKeys =
        selector.selectedKeys();

实际开发中通常使用迭代器处理:

java
Iterator<SelectionKey> iterator =
        selector.selectedKeys().iterator();

while (iterator.hasNext()) {
    SelectionKey key = iterator.next();

    // 处理事件
}

处理完成以后,需要:

java
iterator.remove();

完整写法:

java
Iterator<SelectionKey> iterator =
        selector.selectedKeys().iterator();

while (iterator.hasNext()) {
    SelectionKey key = iterator.next();

    iterator.remove();

    // 处理事件
}

为什么要删除?

因为:

selectedKeys() 中的 Key 不会因为我们处理完事件就自动移除。

如果不执行:

java
iterator.remove();

下一轮事件循环可能再次处理旧的 SelectionKey。


10. 处理连接事件 ​

当:

java
key.isAcceptable()

为 true 时,表示有新的客户端连接。

例如:

java
if (key.isAcceptable()) {
    ServerSocketChannel server =
            (ServerSocketChannel) key.channel();

    SocketChannel client = server.accept();

    client.configureBlocking(false);

    client.register(
            selector,
            SelectionKey.OP_READ
    );
}

整个过程:

text
客户端建立连接
      ↓
Selector 检测到 OP_ACCEPT
      ↓
ServerSocketChannel.accept()
      ↓
获得 SocketChannel
      ↓
设置非阻塞
      ↓
注册 OP_READ

11. 处理读事件 ​

当:

java
key.isReadable()

为 true 时,说明当前 Channel 有数据可以读取。

例如:

java
if (key.isReadable()) {
    SocketChannel client =
            (SocketChannel) key.channel();

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    int len = client.read(buffer);

    if (len == -1) {
        client.close();
        return;
    }

    buffer.flip();

    while (buffer.hasRemaining()) {
        System.out.print((char) buffer.get());
    }
}

数据流向:

text
客户端
   ↓
SocketChannel
   ↓
ByteBuffer
   ↓
程序处理

12. 处理写事件 ​

当:

java
key.isWritable()

为 true 时,说明 Socket 当前可以继续写入数据。

例如:

java
if (key.isWritable()) {
    SocketChannel client =
            (SocketChannel) key.channel();

    ByteBuffer buffer =
            ByteBuffer.wrap(
                    "Hello kerwim".getBytes()
            );

    client.write(buffer);
}

不过需要注意:

OP_WRITE 通常不要一直注册。

因为网络 Socket 在大多数情况下都是可写的,如果一直监听:

java
OP_WRITE

Selector 可能会持续返回写事件,导致事件循环无意义地消耗 CPU。

通常只有当:

text
一次 write() 没有把数据全部写完

时,才临时关注:

java
OP_WRITE

等剩余数据写完以后,再取消对写事件的关注。


13. Selector 服务端完整流程 ​

一个基础的 NIO Selector 服务端:

java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class SelectorServer {

    public static void main(String[] args) throws IOException {

        // 创建服务器 Channel
        ServerSocketChannel serverSocketChannel =
                ServerSocketChannel.open();

        // 设置非阻塞模式
        serverSocketChannel.configureBlocking(false);

        // 绑定端口
        serverSocketChannel.bind(
                new InetSocketAddress(8080)
        );

        // 创建 Selector
        Selector selector = Selector.open();

        // 注册连接事件
        serverSocketChannel.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 server =
                            (ServerSocketChannel) key.channel();

                    SocketChannel client = server.accept();

                    client.configureBlocking(false);

                    client.register(
                            selector,
                            SelectionKey.OP_READ
                    );
                }

                // 可读事件
                if (key.isReadable()) {

                    SocketChannel client =
                            (SocketChannel) key.channel();

                    ByteBuffer buffer =
                            ByteBuffer.allocate(1024);

                    int len = client.read(buffer);

                    if (len == -1) {
                        key.cancel();
                        client.close();
                        continue;
                    }

                    buffer.flip();

                    // 将客户端的数据原样写回
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                }
            }
        }
    }
}

整体流程:

text
创建 ServerSocketChannel
        ↓
设置为非阻塞
        ↓
创建 Selector
        ↓
注册 OP_ACCEPT
        ↓
selector.select()
        ↓
获取 selectedKeys
        ↓
遍历 SelectionKey
        ↓
判断事件类型
        ↓
┌──────────────────┐
│                  │
OP_ACCEPT         OP_READ
│                  │
accept()          read()
│                  │
SocketChannel     Buffer
│
注册 OP_READ

14. attachment ​

SelectionKey 还可以绑定一个额外对象:

java
key.attach(object);

获取:

java
Object object = key.attachment();

也可以在注册 Channel 时直接绑定:

java
ByteBuffer buffer =
        ByteBuffer.allocate(1024);

socketChannel.register(
        selector,
        SelectionKey.OP_READ,
        buffer
);

读取:

java
ByteBuffer buffer =
        (ByteBuffer) key.attachment();

这样可以把:

text
Channel
    +
它自己的 Buffer

关联起来。

例如:

text
SocketChannel A → ByteBuffer A

SocketChannel B → ByteBuffer B

SocketChannel C → ByteBuffer C

这比每次收到事件时重新:

java
ByteBuffer.allocate()

更加合理。


15. cancel() ​

如果不再需要监听某个 Channel,可以取消对应的 SelectionKey:

java
key.cancel();

例如客户端断开连接:

java
int len = client.read(buffer);

if (len == -1) {
    key.cancel();
    client.close();
}

可以理解为:

text
Channel 已关闭
    ↓
SelectionKey 取消
    ↓
Selector 不再监听

16. wakeup() ​

如果一个线程正在:

java
selector.select();

中阻塞,可以通过:

java
selector.wakeup();

让它提前返回。

例如:

text
线程 A
    ↓
selector.select()
    ↓
阻塞等待


线程 B
    ↓
selector.wakeup()
    ↓
唤醒线程 A

这个方法在多线程控制 Selector 时比较常见。


17. Selector 的核心工作流程 ​

Selector 的整体工作方式:

text
1. 创建 Selector
        ↓
2. Channel 设置为非阻塞
        ↓
3. Channel 注册到 Selector
        ↓
4. 指定感兴趣的事件
        ↓
5. selector.select()
        ↓
6. 获取 selectedKeys()
        ↓
7. 遍历 SelectionKey
        ↓
8. 判断事件类型
        ↓
9. 处理对应 Channel
        ↓
10. iterator.remove()
        ↓
11. 下一轮 select()

最核心的一段代码就是:

java
while (true) {
    selector.select();

    Iterator<SelectionKey> iterator =
            selector.selectedKeys().iterator();

    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();

        iterator.remove();

        if (key.isAcceptable()) {
            // 处理连接
        }

        if (key.isReadable()) {
            // 处理读取
        }

        if (key.isWritable()) {
            // 处理写入
        }
    }
}

18. 小结 ​

Selector 是 Java NIO 实现 I/O 多路复用 的核心组件。

它主要解决的问题是:

text
大量网络连接
    ↓
不希望一个连接占用一个线程
    ↓
Selector 统一监听多个 Channel

三个核心组件的关系:

text
                 Java NIO
                    │
        ┌───────────┼───────────┐
        │           │           │
      Buffer      Channel    Selector
        │           │           │
     存储数据      传输数据     监听多个
                               Channel

Selector 最核心的几个概念:

text
Selector
    ↓
管理多个 Channel

SelectionKey
    ↓
Channel 的注册信息

OP_ACCEPT
    ↓
连接事件

OP_READ
    ↓
读事件

OP_WRITE
    ↓
写事件

select()
    ↓
等待事件

selectedKeys()
    ↓
获取已经发生事件的 Channel

一句话理解:

Selector 就像一个事件管理员,一个线程通过它监控多个 Channel,哪个 Channel 有事情就处理哪个。

Noctiflorous Garden · MIT License