多个 Channel 注册到同一个 Selector
上, Selector
能检测通道上是否有事件发生, 如果有事件发生, 便获取事件然后针对每个事件进行相应的处理.
这样就可以只用一个单线程去管理多个通道, 也就是管理多个连接和请求.
只有在连接真正有读写事件发生时, 才会进行读写, 就大大地减少了系统的开销, 而且不必为每个连接都创建一个线程, 不用去维护多个线程, 避免了多线程之间的上下文切换导致的开销.
下面是 java.nio.channels.Selector
的继承关系.
该类就是具体选择器的一个超类, 主要功能是通过 SelectorProvider
根据操作系统创建不同的 Selector
实例. 例如 Linux 下会创建 EPollSelectorImpl
实例.
而且还规定了一些基础通用方法:
SelectedKey
.SelectedKey
.AbstractSelector
类是 Selector
类的一个基本实现, 提供了:
SelectedKey
.Selector
上.SelectedKey
注册.SelectorImpl
类是一个比较全面的 Selector
实现类, 其它的 Selector
的具体实现都是继承此类.
我的代码分析不会一步一步看代码, 而是直接看重点.
大家最好能自己看一下代码.
创建 Selector 是通过 Selector.open()
静态方法进行创建, 在 Linux 下最终会创建 EPollSelectorImpl
对象:
EPollSelectorImpl(SelectorProvider sp) throws IOException {
super(sp);
// makePipe(调用系统函数 pipe) 返回管道的 2 个文件描述符, 编码在一个 long 类型的变量中
// 高 32 位代表读 低 32 位代表写
// 使用 pipe 为了实现 Selector 的 wakeup 逻辑.
long pipeFds = IOUtil.makePipe(false);
fd0 = (int) (pipeFds >>> 32);
fd1 = (int) pipeFds;
try {
// EPollArrayWrapper 是针对 EPoll 的操作.
pollWrapper = new EPollArrayWrapper();
// 绑定管道的读事件
pollWrapper.initInterrupt(fd0, fd1);
fdToKey = new HashMap<>();
} catch (Throwable t) {
try {
FileDispatcherImpl.closeIntFD(fd0);
} catch (IOException ioe0) {
t.addSuppressed(ioe0);
}
try {
FileDispatcherImpl.closeIntFD(fd1);
} catch (IOException ioe1) {
t.addSuppressed(ioe1);
}
throw t;
}
}
EPollSelectorImpl
可以理解为 EPollArrayWrapper
的又一层包装.
EPollArrayWrapper
构造函数 和 initInterrupt
函数.
EPollArrayWrapper() throws IOException {
// creates the epoll file descriptor
// 通过系统函数 epoll_create 创建 epoll, 与 select 函数类似, 用来监听 socket 上是否发生以及发生了什么事件.
epfd = epollCreate();
// the epoll_event array passed to epoll_wait
// 创建这块内存是为了存放事件结果.
int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT;
pollArray = new AllocatedNativeObject(allocationSize, true);
pollArrayAddress = pollArray.address();
// 如果 文件描述符 > 64k, 会使用 eventHigh.
if (OPEN_MAX > MAX_UPDATE_ARRAY_SIZE)
eventsHigh = new HashMap<>();
}
void initInterrupt(int fd0, int fd1) {
outgoingInterruptFD = fd1;
incomingInterruptFD = fd0;
// 调用系统函数 epoll_ctl, 给之前创建的管道的读 fd, 关联一个读事件, 并绑定到 epfd 中.
// 表示关联的 fd 可以进行读操作了.
epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);
}
虽然在 15.Channel 通道中的 SelectableChannel 有说过该类提供了一个 register
方法, 但是最终还是调用的是 SelectorImpl#register
方法.
下面是 AbstractSelectableChannel
中的方法.
public final SelectionKey register(Selector sel, int ops,
Object att)
throws ClosedChannelException
{
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (isBlocking())
throw new IllegalBlockingModeException();
// 该方法会遍历 SelectionKey 数组, 并获取每一个元素的对应的 Selector,
// 然后与该方法的参数比较是否为同一个对象, 详细流程查看下面对应方法.
SelectionKey k = findKey(sel);
if (k != null) {
k.interestOps(ops);
k.attach(att);
}
if (k == null) {
// New registration
synchronized (keyLock) {
if (!isOpen())
throw new ClosedChannelException();
// 下面有详细解释
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
}
return k;
}
}
private SelectionKey findKey(Selector sel) {
synchronized (keyLock) {
if (keys == null)
return null;
for (int i = 0; i < keys.length; i++)
if ((keys[i] != null) && (keys[i].selector() == sel))
return keys[i];
return null;
}
}
注意
keys
变量是在AbstractSelectableChannel
中声明, 可以认为该变量中保存的都是与当前 Channel 有关的 SelectionKey.
下面这是 SelectorImpl
中注册的具体实现, 也就是说 ((AbstractSelector)sel).register(this, ops, att);
调用后执行下面代码.
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
// SelectionKey 表示, Channel 注册到了一个 Selector 上.
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment); // 这个参数怎么使用我也没搞明白
synchronized (publicKeys) {
implRegister(k);
}
k.interestOps(ops);
return k;
}
// EPollSelectorImpl 中的 implRegister 实现.
protected void implRegister(SelectionKeyImpl ski) {
if (closed)
throw new ClosedSelectorException();
SelChImpl ch = ski.channel;
// 得到对应连接的文件描述符
int fd = Integer.valueOf(ch.getFDVal());
// 文件描述符与 SelectionKey 关联.
fdToKey.put(fd, ski);
// 该方法只是将对应描述符的事件先强制设置为 0.
// 注意: 在该方法中只是将 eventsLow 数组中, 下标为 fd 的值改为 0. 这个时候还没有调用系统函数进行修改.
pollWrapper.add(fd);
// keys 是一个 HashSet 集合, 保存了所有的 SelectionKey.
keys.add(ski);
}
最后是 k.interestOps(ops);
调用, 在该方法中会根据不同的 Channel 调用不同的 translateAndSetInterestOps
方法.
该方法作用是将程序内部事件, 例如 OP_ACCEPT
转换为 POLLIN
这种被 epoll_ctl
系统函数所支持的事件. 然后会重新设置 eventsLow
对应下标的值.
也就是说这里才确定了你的事件. 但是并没有调用系统函数, 来做绑定.
值得注意的是:
Net.POLLIN
的值并不是固定的, 会根据操作系统设置不同的值, 在 Linux 下该值为 0x0001
.
至于为什么要进行转换, 可以查看 参考文章 中的 epoll触发事件的分析.
通过调用 selector.select(1000)
方法, 该方法有一个超时时间, 当然你可以调用 select
或 selectNow
方法.
该方法最终会调用 EPollSelectorImpl
对象的 doSelect
方法.
protected int doSelect(long timeout) throws IOException {
if (closed)
throw new ClosedSelectorException();
// 删除已经取消绑定的 SelectionKey
processDeregisterQueue();
try {
// 标记 可能无限期阻塞的 I/O操作的开始
begin();
/*
* 在该方法中会先注册/更新 socket 绑定的事件, 或删除 socket 的事件.
* 然后调用系统的 epoll_wait(没有超时时间) 或 在 for (;;) 中调用 epoll_wait 方法,
* 到了超时时间后, 还没有数据则返回 0. >0 表示就绪的 socket 个数.
*/
pollWrapper.poll(timeout);
} finally {
// 标记 可能无限期阻塞的 I/O操作的结束
end();
}
processDeregisterQueue();
// 这里会通过 fd 找到对应的 SelectionKey, 然后添加到 selectedKeys 集合中,
// 并返回就绪的个数.
int numKeysUpdated = updateSelectedKeys();
if (pollWrapper.interrupted()) {
// Clear the wakeup pipe
// 为什么这里要做这些操作, 我就不清楚了.
pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
synchronized (interruptLock) {
pollWrapper.clearInterrupted();
IOUtil.drain(fd0);
interruptTriggered = false;
}
}
return numKeysUpdated;
}
updateSelectedKeys()
方法主要就是通过不同 Channel 的 translateAndSetReadyOps
方法判断事件类型.
private int updateSelectedKeys() {
// 已就绪(产生事件)的文件句柄 数量
int entries = pollWrapper.updated;
int numKeysUpdated = 0;
for (int i=0; i<entries; i++) {
// 获取事件是哪个文件描述符产生的
int nextFD = pollWrapper.getDescriptor(i);
// 通过文件描述符获取对应的 SelectionKey
// 关于 fdToKey 的添加后面再说.
SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
// ski is null in the case of an interrupt
// 至于为啥为 null 就是中断, 我也没明白.
if (ski != null) {
// 获取事件类型.
int rOps = pollWrapper.getEventOps(i);
if (selectedKeys.contains(ski)) {
if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
numKeysUpdated++;
}
} else {
// 在该方法中会设置 readyOps 属性, 该属性表示事件类型.
ski.channel.translateAndSetReadyOps(rOps, ski);
if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
selectedKeys.add(ski);
numKeysUpdated++;
}
}
}
}
return numKeysUpdated;
}
pollWrapper
变量的类型为 EPollArrayWrapper
, 主要用来操作关于 epoll 底层函数, 例如注册/更新 epoll 或删除 epoll 的事件以及获取发生的事件, 下面是 EPollArrayWrapper#poll
方法.
int poll(long timeout) throws IOException {
// 注册/更新 epoll 或删除 epoll 的事件, 实际调用 `epollCtl` 加入到 epollfd 中.
updateRegistrations();
// 获取已就绪(产生事件)的文件句柄
updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
for (int i=0; i<updated; i++) {
if (getDescriptor(i) == incomingInterruptFD) {
interruptedIndex = i;
interrupted = true;
break;
}
}
return updated;
}
通过 getDescriptor(i)
获得事件是哪个文件描述符产生的, 变量 incomingInterruptFD
值保存的是管道的读文件描述符.
也就是说当指定的管道有数据时, 也会产生事件. 并将执行 interrupted = true;
. 至于为什么要这个样子后续我就不知道了.
1. 相同 Channel 能否注册到不同 Selector 上
通过 注册 Channel 可以发现, 并没有做任何限制处理.
但我个人认为一般不会出现, 相同 Channel 注册到不同 Selector 上.
同步/异步,阻塞/非阻塞概念深度解析
pipe()函数Unix / Linux
C++ socket, fork & pipes
What’s the difference between pipe and socket?
浅析epoll - epoll函数深入讲解
【Linux】进程间通信(IPC)之管道详解与测试用例
【Linux】进程间通信(IPC)之消息队列详解及测试用例
epoll_ctl()函数 Unix/Linux
epoll触发事件的分析