Java NIO Selector 多路复用选择器

引言

上篇文章我们简单的使用了NIOChannel通道,本期我们主要来介绍一下选择器(Selector
)的使用,SelectorJava NIO核心组件中的一个。在之前介绍5种I/O模型的时候,有介绍过多路复用模型。多路复用模型使得我们可以使用一个线程来管理成千上万的连接,避免了线程上下文切换带来的开销,使得性能得到极大的提升

基本模型

选择器Selector使用的基本模式,跟传统BIO处理模型不一样。传统BIO往往我们会使用多线程来提升处理性能,也就是说每接入一个Client,Server端就会为其新开一个线程,以此来提升并发吞吐量,使用这种模式的弊端很明显,因为线程是系统非常重要的资源,当并发量少的时候,感觉不到,一旦并发量上来,就会出现瓶颈。

再来看Selector是如何处理的,首先每接入一个Client,我们可以通过Selector选择器注册感兴趣的事件,然后通过一个线程去不停的轮询检测各个Client是否有感兴趣的事件发生,有则顺序处理该Client就绪的各个事件。

image.png

Selector 使用实例

我们先来看一个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
36
37
38
39
40
41
42
43
44
try {
// 打开一个通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8890));
// 打开 选择器 selector
Selector selector = Selector.open();
// 设置非阻塞
serverSocketChannel.configureBlocking(false);
// 为ServerSocketChannel注册 OP_ACCEPT 事件,返回一个SelectionKey 对象
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
// 返回至少有一个事件就绪的通道数,该方法是阻塞的
int readyNum = selector.select();
if (readyNum == 0) {
continue;
}

// 返回就绪的 SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iteratorKeys = selectionKeys.iterator();

// 遍历所有就绪的SelectionKey集合
while (iteratorKeys.hasNext()) {
SelectionKey key = iteratorKeys.next();
iteratorKeys.remove();
// 判断就绪的具体事件
if (key.isValid()) {
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受一个新连接
SocketChannel channel = serverChannel.accept();
channel.configureBlocking(false);
// 为该Channel注册可读事件
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
...
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}

通过查看上面的代码,可知我们通过Selector.open()创建了一个选择器,并且通过SocketChannelregister方法注册了选择器和事件,其中register方法会返回一个SelectionKey对象,该对象其实就是维护了ChannelSelector的对应关系

SelectionKey

上文我们提到SelectionKey对象维护了ChannelSelector的对应关系,现在我们来看下
SelectionKey对象内部几个非常重要的属性和方法

  • 属性

    • OP_READ OP_WRITE OP_CONNECT OP_ACCEPT 可注册的四个感兴趣的项目
    • interestOps 所有感兴趣的集合
    • readyOps 所有就绪的兴趣集合
  • 方法

    • boolean isValid() 判断该SelectionKey是否有效
    • void cancel() 取消该SelectionKey中 通道与其选择器 的注册
    • int interestOps() 返回该SelectionKey所包含的所有兴趣(可读可写)集合
    • SelectionKey interestOps(int ops) 设置一个感兴趣的项目
    • int readyOps() 返回已经就绪的兴趣集合
    • boolean isWritable() 判断是否可写
    • boolean isReadable() 判断是否可读
    • boolean isConnectable() 判断是否可连接
    • boolean isAcceptable() 判断是否可接受
    • Object attach(Object ob) 在该SelectionKey中附加一个对象信息
    • Object attachment() 获取附加对象信息

Selector

讲完SelectionKeySelector的关系之后,我们再次回到Selector类,我们首先需要知道Selector中3个重要的SelectionKey集合

  • keys:所有注册到Selector的Channel所表示的SelectionKey都会存在于该集合中。keys元素的添加会在Channel注册到Selector时发生。
  • selectedKeys:该集合中的每个SelectionKey都是其对应的Channel在上一次操作selection期间被检查到至少有一种SelectionKey中所感兴趣的操作已经准备好被处理。该集合是keys的一个子集。
  • cancelledKeys:执行了取消操作的SelectionKey会被放入到该集合中。该集合是keys的一个子集。

接下来将会介绍上文例子中所用到的几个方法

Selector.open()

这个静态方法可以打开一个Selector选择器

1
2
3
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}

通过查看源码可知在Linux系统下默认会使用EpollSelectorImpl来作为Selector实现类,注意各个系统下默认的实现类是不同的。

selector.select()

Selectorselect()方法会返回事件就绪的SelectionKey数目(也就是就绪的Channel数)

并且该方法会一直阻塞直到至少一个channel被选择(即,该channel注册的事件发生了)为止,除非当前线程发生中断或者selectorwakeup方法被调用

该方法还有一个重载select(long timeout),可以自定义超时时间

selector.selectNow()

该方法与上面方法类似,但该方法不会发生阻塞,即使没有一个channel被选择也会立即返回

selector.selectedKeys()

返回已就绪的SelectionKey集合,该方法在执行了selector.select()后调用,因为在执行selector.select()后就表示至少有一个SelectionKey已经就绪

尾言

好了,本篇文章就介绍到这里了,篇幅有限加上本人对NIO的理解也有待加深,希望可以在之后更深入的对Java NIO的实现进行分析。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 王俊男的技术杂谈 All Rights Reserved.

访客数 : | 访问量 :