Java NIO系列(四) - Selector

前言

Selector 是 Java NIO 中的一个组件,用于检查一个多个通道 Channel 的状态是否处于可读可写状态。如此可以实现单线程管理多个通道,也就是可以管理多个网络连接

为什么使用Selector?

单线程处理多个 Channel 的好处是我需要更少的线程来处理 Channel 。实际上,你甚至可以用一个线程来处理所有的Channel。从操作系统的角度来看,切换线程的开销是比较昂贵的,并且每个线程都需要占用系统资源,因此暂用线程越少越好。

简而言之,通过 Selector 我们可以实现单线程操作多个 Channel。下面是单线程使用一个 Selector 处理 3 个 Channel 的示例图:


正文

Selector的组件

Java NIO Selector中有三个重要的组成:SelectorSelectableChannel 和 SelectionKey

(一) 选择器(Selector)

Selector选择器类管理着一个被注册通道集合的信息和它们的就绪状态选择器所在线程不停地更新通道的就绪状态,对通道注册的连接数据读写事件等事件进行响应。

(二) 可选择通道(SelectableChannel)

SelectableChannel 是一个抽象类,提供了通道可选择性所需要的公共方法的实现,它是所有支持就绪检查通道类父类

因为 FileChannel 类没有继承 SelectableChannel,因此不是可选通道。而所有 Socket 通道都是可选择的,包括从管道 (Pipe) 对象的中获得的通道。
SelectableChannel 可以被注册到 Selector 对象上,并且注册时可以指定感兴趣的事件操作,比如:数据读取数据写入操作。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次

(三) 选择键(SelectionKey)

选择键封装了特定的通道特定的选择器的注册关系。选择键对象由被 SelectableChannel.register() 返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

Selector的使用

(一) 创建Selector对象

Selector 对象是通过调用静态工厂方法 open() 来实例化的,如下:

1
Selector Selector = Selector.open();

(二) 将SelectableChannel注册到Selector

为了将 Channel 和 Selector 配合使用,必须将 Channel 注册到 Selector 上。通过 SelectableChannel.register() 方法来实现,如下:

1
2
3
channel.configureBlocking(false);
// 对读操作感兴趣,向Selector注册读事件
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

与 Selector 一起使用时,Channel 必须处于非阻塞模式下。这意味着不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式,而套接字通道都可以。

注意 register() 方法的第二个参数。这是一个兴趣 (interest) 集合,意思是在通过 Selector 监听 Channel 时对什么事件感兴趣。可以监听四种不同类型的事件:

  • 连接操作(Connect):监听 SocketChannel 到来的连接事件。
  • 接受操作(Accept):对应常量 SelectionKey.OP_ACCEPT,专注于监听 ServerSocketChannel 接受 SocketChannel 的事件。
  • 读操作(Read):对应常量 SelectionKey.OP_READ,监听数据完全到达,通道可读的事件。
  • 写操作(Write):对应常量 SelectionKey.OP_READ,监听数据准备完成,通道可写的事件。

注意:并非所有的操作在所有的可选择通道上都能被支持。比如 ServerSocketChannel 支持 Accept操作,而 SocketChannel 中不支持。我们可以通过通道上的 validOps() 方法来获取特定通道下所有支持的操作集合

以上四种事件用 SelectionKey 的四个常量来表示:

1
2
3
4
public static final int OP_READ = 1 << 0; // 1
public static final int OP_WRITE = 1 << 2; // 4
public static final int OP_CONNECT = 1 << 3; // 8
public static final int OP_ACCEPT = 1 << 4; // 16

如果一个通道同时对多种操作感兴趣,可以用 “位或” 操作符将常量连接起来,如下:

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

(三) 为SelectionKey绑定附加对象

可以将一个对象或者更多信息附着到 SelectionKey 上,这样就能方便的识别某个给定的通道。例如,可以附加与通道一起使用的 Buffer,或是包含聚集数据的某个对象。使用方法如下:

1
2
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

还可以在用 register() 方法向 Selector 注册 Channel 的时候附加对象,例如:

1
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

如果要取消该对象,则可以通过该种方式:

1
selectionKey.attach(null);

(四) 通过Selector选择通道

一旦向 Selector 注册了一或多个通道,就可以调用几个重载的 select() 方法。这些方法返回你所感兴趣的事件 (如连接接受) 已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select() 方法会返回读事件已经就绪的那些通道的 SelectionKey

下面是 select() 方法的几个重载:

  • int select()阻塞到至少有一个通道在此选择器注册的事件上就绪了。
  • int select(long timeout)select(long timeout) 和 select() 一样,除了最长会阻塞timeout毫秒(参数)。
  • int selectNow()不会阻塞,不管什么通道就绪都立刻返回。如果没有通道变成可选择的,则此方法直接返回 0

也可以通过遍历 SelectionKey 上的已选择键集合来访问就绪的通道,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// 一个连接被ServerSocketChannel接受
} else if (key.isConnectable()) {
// 与远程服务器建立了连接
} else if (key.isReadable()) {
// 一个channel做好了读准备
} else if (key.isWritable()) {
// 一个channel做好了写准备
}
keyIterator.remove();
}

注意:每次迭代完成时 Selector 自己不会将已经处理完成的 SelectionKey实例移除,在迭代的末尾需要调用 keyIterator.remove() 方法手动移除。

SelectionKey.channel() 方法返回的通道需要强转为你要处理的类型,如:ServerSocketChannel 或 SocketChannel 等。

Selector完整实例

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));

Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
int number = selector.select();
if (number == 0)
continue;

Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
// 获取客户端通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.configureBlocking(false);
// 将客户端通道注册到选择器上
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}
if (selectionKey.isReadable()) {
handleRead(selectionKey);
}
if (selectionKey.isWritable()) {
handleWrite(selectionKey);
}
if (selectionKey.isConnectable()) {
System.out.println("Isonnectable := true");
}
iterator.remove();
}
}

服务端操作过程

  1. 创建 ServerSocketChannel 实例,设置为非阻塞模式,并绑定指定的服务端口
  2. 创建 Selector 实例;
  3. 将 serverSocketChannel 注册到 selector 上面,并指定事件 OP_ACCEPT,最底层的 socket 通过 channel 和 selector 建立关联;
  4. 如果没有准备好 (Accept) 的socketselect方法会被阻塞一段时间并返回 0
  5. 如果底层有 socket 已经准备好,selector 的 select() 方法会返回 socket 的个数,而且 selectedKeys 方法会返回 socket 对应的事件(connectacceptread 和 write);
  6. 根据事件类型,进行不同的处理逻辑。

总结

这里简单的介绍了 Java NIO 中选择器的用法,有关 Selector 底层的实现原理需要进一步查看源码。


欢迎关注技术公众号: 零壹技术栈

零壹技术栈零壹技术栈

本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。

猜你喜欢

转载自www.cnblogs.com/ostenant/p/9695187.html