Selector
字数: 0 字 时长: 0 分钟
Selector 是 Java NIO 中用于实现 I/O 多路复用 的核心组件。
它可以让一个线程同时监听多个 Channel 的 I/O 事件,例如:
连接事件
读事件
写事件可以简单理解为:
多个 Channel
↓
Selector
↓
一个线程统一监听传统阻塞 IO 中,通常需要为每个连接分配一个线程:
客户端 1 → 线程 1
客户端 2 → 线程 2
客户端 3 → 线程 3
客户端 4 → 线程 4而使用 Selector 后:
SocketChannel 1 ─┐
SocketChannel 2 ─┤
SocketChannel 3 ─┼→ Selector → 一个线程
SocketChannel 4 ─┘这也是 NIO 在高并发网络通信中的核心优势之一。
1. Selector、Channel、Buffer 的关系
Java NIO 中三个核心组件:
Buffer
↓
存储数据
Channel
↓
传输数据
Selector
↓
监听和管理多个 Channel整体关系:
Selector
│
┌─────────┼─────────┐
│ │ │
Channel Channel Channel
│ │ │
Buffer Buffer Buffer可以简单记:
Buffer 存数据,Channel 传数据,Selector 管 Channel。
2. 创建 Selector
通过:
Selector.open()创建一个 Selector:
Selector selector = Selector.open();此时 Selector 还没有监听任何 Channel。
需要将 Channel 注册到 Selector:
channel.register(selector, SelectionKey.OP_READ);这样 Selector 才能监听这个 Channel 上发生的事件。
3. Channel 注册到 Selector
并不是所有 Channel 都可以注册到 Selector。
常见可以注册的 Channel:
ServerSocketChannel
SocketChannel
DatagramChannel例如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(
selector,
SelectionKey.OP_ACCEPT
);这里非常重要:
Channel 注册到 Selector 之前,必须设置为非阻塞模式。
也就是:
channel.configureBlocking(false);否则注册时会抛出:
IllegalBlockingModeException例如:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.register(
selector,
SelectionKey.OP_READ
);4. SelectionKey
Channel 注册到 Selector 后,会返回一个:
SelectionKey例如:
SelectionKey key = socketChannel.register(
selector,
SelectionKey.OP_READ
);SelectionKey 可以理解为:
Channel 在 Selector 中的注册凭证。
它记录了:
哪个 Channel
注册在哪个 Selector
关注哪些事件
当前发生了哪些事件关系:
Channel
↓
register()
↓
Selector
↓
SelectionKey5. Selector 监听的事件
Java NIO 中常见的事件有:
SelectionKey.OP_ACCEPT
SelectionKey.OP_CONNECT
SelectionKey.OP_READ
SelectionKey.OP_WRITE5.1 OP_ACCEPT
SelectionKey.OP_ACCEPT表示:
有新的客户端连接到达。
主要用于:
ServerSocketChannel例如:
serverSocketChannel.register(
selector,
SelectionKey.OP_ACCEPT
);5.2 OP_CONNECT
SelectionKey.OP_CONNECT表示:
客户端连接服务器的操作已经准备完成。
主要用于:
SocketChannel非阻塞连接。
5.3 OP_READ
SelectionKey.OP_READ表示:
Channel 中有数据可以读取。
例如:
socketChannel.register(
selector,
SelectionKey.OP_READ
);5.4 OP_WRITE
SelectionKey.OP_WRITE表示:
Channel 当前可以继续写数据。
主要用于处理网络发送缓冲区暂时写不完的情况。
常用事件可以总结为:
| 事件 | 含义 |
|---|---|
OP_ACCEPT | 有新的连接 |
OP_CONNECT | 连接建立完成 |
OP_READ | 数据可读 |
OP_WRITE | 数据可写 |
6. interestOps 和 readyOps
SelectionKey 中有两个比较重要的概念:
interestOps
readyOps6.1 interestOps
表示:
我们希望 Selector 关注哪些事件。
例如:
SelectionKey.OP_READ表示我们希望监听:
读事件注册时指定:
socketChannel.register(
selector,
SelectionKey.OP_READ
);也可以同时关注多个事件:
int interestSet =
SelectionKey.OP_READ |
SelectionKey.OP_WRITE;6.2 readyOps
表示:
当前真正已经准备好的事件。
通常不需要直接操作 readyOps(),而是通过下面这些方法判断:
key.isAcceptable();
key.isConnectable();
key.isReadable();
key.isWritable();例如:
if (key.isReadable()) {
// 处理读取事件
}7. selector.select()
注册完成以后,通过:
selector.select();等待 I/O 事件发生。
例如:
while (true) {
selector.select();
// 处理事件
}select() 会阻塞当前线程,直到至少有一个注册的 Channel 出现感兴趣的事件。
需要注意:
NIO 的“非阻塞”并不是说代码中完全没有阻塞。
例如:
selector.select();本身就可以阻塞线程。
区别在于:
传统 BIO
↓
线程阻塞在某一个具体连接上
Selector
↓
一个线程等待多个 Channel 的事件8. select 的几个方法
Selector 常用的等待方法:
8.1 select()
selector.select();阻塞等待,直到有事件发生。
8.2 select(long timeout)
selector.select(3000);最多阻塞指定时间。
例如:
3000ms如果期间没有事件,超时后也会返回。
8.3 selectNow()
selector.selectNow();立即返回,不阻塞。
如果当前没有事件:
返回 09. selectedKeys
当:
selector.select();返回后,可以通过:
selector.selectedKeys();获取已经发生事件的 SelectionKey。
例如:
Set<SelectionKey> selectedKeys =
selector.selectedKeys();实际开发中通常使用迭代器处理:
Iterator<SelectionKey> iterator =
selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 处理事件
}处理完成以后,需要:
iterator.remove();完整写法:
Iterator<SelectionKey> iterator =
selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 处理事件
}为什么要删除?
因为:
selectedKeys()中的 Key 不会因为我们处理完事件就自动移除。
如果不执行:
iterator.remove();下一轮事件循环可能再次处理旧的 SelectionKey。
10. 处理连接事件
当:
key.isAcceptable()为 true 时,表示有新的客户端连接。
例如:
if (key.isAcceptable()) {
ServerSocketChannel server =
(ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(
selector,
SelectionKey.OP_READ
);
}整个过程:
客户端建立连接
↓
Selector 检测到 OP_ACCEPT
↓
ServerSocketChannel.accept()
↓
获得 SocketChannel
↓
设置非阻塞
↓
注册 OP_READ11. 处理读事件
当:
key.isReadable()为 true 时,说明当前 Channel 有数据可以读取。
例如:
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());
}
}数据流向:
客户端
↓
SocketChannel
↓
ByteBuffer
↓
程序处理12. 处理写事件
当:
key.isWritable()为 true 时,说明 Socket 当前可以继续写入数据。
例如:
if (key.isWritable()) {
SocketChannel client =
(SocketChannel) key.channel();
ByteBuffer buffer =
ByteBuffer.wrap(
"Hello kerwim".getBytes()
);
client.write(buffer);
}不过需要注意:
OP_WRITE通常不要一直注册。
因为网络 Socket 在大多数情况下都是可写的,如果一直监听:
OP_WRITESelector 可能会持续返回写事件,导致事件循环无意义地消耗 CPU。
通常只有当:
一次 write() 没有把数据全部写完时,才临时关注:
OP_WRITE等剩余数据写完以后,再取消对写事件的关注。
13. Selector 服务端完整流程
一个基础的 NIO Selector 服务端:
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);
}
}
}
}
}
}整体流程:
创建 ServerSocketChannel
↓
设置为非阻塞
↓
创建 Selector
↓
注册 OP_ACCEPT
↓
selector.select()
↓
获取 selectedKeys
↓
遍历 SelectionKey
↓
判断事件类型
↓
┌──────────────────┐
│ │
OP_ACCEPT OP_READ
│ │
accept() read()
│ │
SocketChannel Buffer
│
注册 OP_READ14. attachment
SelectionKey 还可以绑定一个额外对象:
key.attach(object);获取:
Object object = key.attachment();也可以在注册 Channel 时直接绑定:
ByteBuffer buffer =
ByteBuffer.allocate(1024);
socketChannel.register(
selector,
SelectionKey.OP_READ,
buffer
);读取:
ByteBuffer buffer =
(ByteBuffer) key.attachment();这样可以把:
Channel
+
它自己的 Buffer关联起来。
例如:
SocketChannel A → ByteBuffer A
SocketChannel B → ByteBuffer B
SocketChannel C → ByteBuffer C这比每次收到事件时重新:
ByteBuffer.allocate()更加合理。
15. cancel()
如果不再需要监听某个 Channel,可以取消对应的 SelectionKey:
key.cancel();例如客户端断开连接:
int len = client.read(buffer);
if (len == -1) {
key.cancel();
client.close();
}可以理解为:
Channel 已关闭
↓
SelectionKey 取消
↓
Selector 不再监听16. wakeup()
如果一个线程正在:
selector.select();中阻塞,可以通过:
selector.wakeup();让它提前返回。
例如:
线程 A
↓
selector.select()
↓
阻塞等待
线程 B
↓
selector.wakeup()
↓
唤醒线程 A这个方法在多线程控制 Selector 时比较常见。
17. Selector 的核心工作流程
Selector 的整体工作方式:
1. 创建 Selector
↓
2. Channel 设置为非阻塞
↓
3. Channel 注册到 Selector
↓
4. 指定感兴趣的事件
↓
5. selector.select()
↓
6. 获取 selectedKeys()
↓
7. 遍历 SelectionKey
↓
8. 判断事件类型
↓
9. 处理对应 Channel
↓
10. iterator.remove()
↓
11. 下一轮 select()最核心的一段代码就是:
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 多路复用 的核心组件。
它主要解决的问题是:
大量网络连接
↓
不希望一个连接占用一个线程
↓
Selector 统一监听多个 Channel三个核心组件的关系:
Java NIO
│
┌───────────┼───────────┐
│ │ │
Buffer Channel Selector
│ │ │
存储数据 传输数据 监听多个
ChannelSelector 最核心的几个概念:
Selector
↓
管理多个 Channel
SelectionKey
↓
Channel 的注册信息
OP_ACCEPT
↓
连接事件
OP_READ
↓
读事件
OP_WRITE
↓
写事件
select()
↓
等待事件
selectedKeys()
↓
获取已经发生事件的 Channel一句话理解:
Selector 就像一个事件管理员,一个线程通过它监控多个 Channel,哪个 Channel 有事情就处理哪个。