深入理解NIO系列 - Selector详解
Selector简述
A multiplexor of {@link SelectableChannel} objects.
参照Java doc中Selector描述的第一句话,Selector的作用是Java NIO中管理一组多路复用的SelectableChannel对象,并能够识别通道是否为诸如读写事件做好准备的组件
Selector的创建过程如下:
1 | // 1.创建Selector |
一个Channel在Selector注册其代表的是一个SelectionKey
事件,SelectionKey
的类型包括:
OP_READ
:可读事件;值为:1<<0
OP_WRITE
:可写事件;值为:1<<2
OP_CONNECT
:客户端连接服务端的事件(tcp连接),一般为创建SocketChannel
客户端channel;值为:1<<3
OP_ACCEPT
:服务端接收客户端连接的事件,一般为创建ServerSocketChannel
服务端channel;值为:1<<4
一个Selector内部维护了三组keys:
key set
:当前channel注册在Selector上所有的key;可调用keys()
获取selected-key set
:当前channel就绪的事件;可调用selectedKeys()
获取cancelled-key
:主动触发SelectionKey#cancel()
方法会放在该集合,前提条件是该channel没有被取消注册;不可通过外部方法调用
Selector类中总共包含以下10个方法:
open()
:创建一个Selector对象isOpen()
:是否是open状态,如果调用了close()
方法则会返回false
provider()
:获取当前Selector的Provider
keys()
:如上文所述,获取当前channel注册在Selector上所有的keyselectedKeys()
:获取当前channel就绪的事件列表selectNow()
:获取当前是否有事件就绪,该方法立即返回结果,不会阻塞;如果返回值>0,则代表存在一个或多个select(long timeout)
:selectNow的阻塞超时方法,超时时间内,有事件就绪时才会返回;否则超过时间也会返回select()
:selectNow的阻塞方法,直到有事件就绪时才会返回wakeup()
:调用该方法会时,阻塞在select()
处的线程会立马返回;(ps:下面一句划重点)即使当前不存在线程阻塞在select()
处,那么下一个执行select()
方法的线程也会立即返回结果,相当于执行了一次selectNow()
方法close()
: 用完Selector
后调用其close()
方法会关闭该Selector,且使注册到该Selector
上的所有SelectionKey
实例无效。channel本身并不会关闭。
关于SelectionKey
谈到Selector就不得不提SelectionKey,两者是紧密关联,配合使用的;如上文所示,往Channel注册Selector会返回一个SelectionKey对象,
这个对象包含了如下内容:
- interest set,当前Channel感兴趣的事件集,即在调用
register
方法设置的interes set - ready set
- channel
- selector
- attached object,可选的附加对象
interest set
可以通过SelectionKey类中的方法来获取和设置interes set
1 | // 返回当前感兴趣的事件列表 |
ready set
当前Channel就绪的事件列表
1 | int readySet = key.readyOps(); |
channel和selector
我们可以通过SelectionKey来获取当前的channel和selector
1 | // 返回当前事件关联的通道,可转换的选项包括:`ServerSocketChannel`和`SocketChannel` |
attached object
我们可以在selectionKey中附加一个对象:
1 | key.attach(theObject); |
或者在注册时直接附加:
1 | SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject); |
一个Selector完整的例子
一个Selector的基本使用流程包括(读者不放试着按照这个流程自己实现一波):
- 创建一个Selector
- 将Channel注册到Selector中,并设置监听的interest set
- loop
- 执行select()方法
- 调用selector.selectedKeys()获取当前就绪的key
- 迭代selectedKeys
- 从key中获取对应的Channel和附加信息(if exist)
- 判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用 “SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()” 获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中.
- 根据需要更改 selected key 的监听事件.
- 将已经处理过的 key 从 selected keys 集合中删除.
1 | import java.io.IOException; |
深入Selector源码
下面我们继续按照Selector的编码过程来学习Selector源码
(1)创建过程
从上图上可以比较清晰得看到,openjdk中Selector的实现是SelectorImpl,
后SelectorImpl又将职责委托给了具体的平台,比如图中框出的linux2.6以后才有的EpollSelectorImpl, Windows平台则是WindowsSelectorImpl, MacOSX平台是KQueueSelectorImpl.
1 | public static Selector open() throws IOException { |
(2)注册过程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// AbstractSelectableChannel#register方法
SelectionKey register(Selector sel, int ops,Object att){
synchronized (regLock) {
// 判断当前Channel是否关闭
if (!isOpen())
throw new ClosedChannelException();
// 判断参数ops是否只包含OP_ACCEPT
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
// 使用Selector则Channel必须是非阻塞的
if (blocking)
throw new IllegalBlockingModeException();
// 根据Selector找到SelectionKey,它是可复用的,一个Selector只能有一个SelectionKey,如果存在则直接覆盖ops和attachedObject
SelectionKey k = findKey(sel);
if(key != null) {
....
}
// 如果不存在则直接实例化一个SelectionKeyImpl对象,并为ops和attachedObject赋值;实际调用AbstractSelector的register方法
// 将Selector和SelectionKey绑定
if (k == null) {
// New registration
synchronized (keyLock) {
if (!isOpen())
throw new ClosedChannelException();
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
}
return k;
}
}
(3)select过程
select是Selector模型中最关键的一步,下面让我们来研究一下其过程
1 | // 首先来看select的调用链 |
疑惑点
Q:各事件分别在什么条件下就绪?
- OP_ACCEPT:客户端向服务端发起TCP连接建立【服务端的代码监听】
- OP_CONNECT:客户端与服务端的连接建立成功或失败【客户端代码监听】
- OP_READ:客户端向服务端发请求或服务端向客户端写入数据时
- OP_WRITE:判断缓冲区是否有空闲空间