jdk nio
参考视频 和参考demo代码
【【Netty精讲】NIO Epoll源码剖析】https://www.bilibili.com/video/BV1cJT9zREb2?vd_source=0b17a38779c085925c505c90e3b719aa
参考版本java8
demo代码
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; public class NioDemo { public static void main(String[] args) throws Exception { //创建一个服务端通道ServerSocketChannel*,用于监听客户端连接* ServerSocketChannel ssc = ServerSocketChannel.open(); //将通道设置为非阻塞模式(Selector机制要求所有通道必须是非阻塞的) ssc.configureBlocking(false); //创建一个Selector(多路复用器),底层封装epoll/kqueue等机制 Selector selector = Selector.open(); //将服务端通道注册到Selector上,关注"接收连接"事件(OP_ACCEPT) ssc.register(selector, SelectionKey.OP_ACCEPT); //绑定端口8080,开始监听客户端连接 ssc.bind(new InetSocketAddress(8080)); //主事件循环,持续监听并处理就绪事件 while (true) { *//阻塞直到至少一个事件就绪(或被wakeup*唤醒) selector.select(); //获取所有"就绪的事件key"集合(本轮select**中准备好的事件) Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); //遍历处理所有就绪事件 while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); iterator.remove(); //一定要移除,防止下一次重复处理 *//如果是"接收连接"事件 if (selectionKey.isAcceptable()) { *//从SelectionKey中取出服务端通道 ServerSocketChannel serverChannel = (ServerSocketChannel) selectionKey.channel(); //接受客户端连接,得到一个新的SocketChannel SocketChannel clientChannel = serverChannel.accept(); *//设置新连接为非阻塞模式 clientChannel.configureBlocking(false); *//将客户端通道注册到Selector上,关注*"读"事件(OP_READ) clientChannel.register(selector, SelectionKey.OP_READ); } *//如果是"可读"事件,说明客户端发送了数据 if (selectionKey.isReadable()) { *//从SelectionKey中取出客户端通道* SocketChannel clientChannel = (SocketChannel) selectionKey.channel(); //创建一个ByteBuffer*,用于读取客户端发送的数据* ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //从通道中读取数据到缓冲区 clientChannel.read(byteBuffer); *//*⚠️注意:这里原始逻辑没有处理读到的内容,比如打印或解码 *//*如果你想查看消息内容,可在此加上: // byteBuffer.flip(); // System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit())); } } } } } |
java 中是如何模拟文件描述符以及模拟socket的?
jdk Nio中的channel 和selctor
关注这个方法先。
ServerSocketChannel ssc = ServerSocketChannel.open();
我们可以看到channel的声明方式不是直接New出来的。而是一种类似于调用工厂实现 的。

SelectorProvider是一个抽象类关键是这个

我们可以看到provider是个抽象方法。这个方法返回一个对象。然后对用这个对象的open方法。其中关键的是这句
provider= sun.nio.ch.DefaultSelectorProvider.create();

追进去是一个很明显是一个基于操作系统区分的类。我们先留意下关注这个类的继承

所以这段方法是一段高级的工厂模式加懒汉式加载加支持返回一个子类实例加载的写法......
然后调用的这个对象是实现 这个SelectorProvider相关的类

我们可以看到实现这个抽象类的两个类是两个mpl 还有一个带有明显的windowsselectorProvider相关的类

这个类是继承自 mpl类的
先来看

mpl类的这个方法

追进去叫做 ServerSocketChannelImpl
我们可以看到在创建的时候在里会有一个外部的调用去获得fd 和一些相关的信息
追踪这个fd相关的方法?
我们可以发现这个fd的生成是在自于jdk中的io包中的。


最终跟踪到了

上图即 整个jdk相关的文件描述类
我们追入一程可以看到是当前的selctroprovidew的provider相关的方法只返回的。
我们先来研究什么是channel?

先来看我们的demo源码把channel追踪到顶点之后就是一个接口。我们可以到最原始的的channel只包含两个功能就是打开和抛出异常?
我们关注之前的
其中这个传入的参数是来自于 一个nat调用

也就是说我们的channel基于socket 的一个一对一的文件描述符对应的实体。
我们思考一个事情?仅仅只有jdk 可以实现nio 吗?
还记得之前我们研究的liunx。也就是文件如同流?
java中应该如何理解这一点。

selector比较长而且理解起来比较复杂。我们先关注它继承的这一个接口

我们看这个接口的描述是一个和流绑定相关的类?
(tip, 我们讨论流的时候不要狭隘的认为是一种传统的字节流。而是要结合liunx源码中流与文件描述符互相支持实现的思考)
回到我们的demo代码
关注这个代码

追入一层

这个openSelector有印象吗?
也就是我们的window相关的类

可以看到selector是要基于不同的操作系统的。而从socket分配或者是获取到fd则是可以跨系统的?
本质上是因为流才是一个操作系统产生差距的地方?
然后我们区分一下有个类叫做一个叫做EPollSelectorProvider相关的类open的。
然后我们回忆下有个类有个父类叫做SelectorProvider提供channel的open的。
然后这些provider各自下又有channel作为属性 或者是seletor的属性。这些属性也会各自有基于不同的操作系统的实现
注册逻辑
回到我们的demo代码
关注
ssc.register(selector, SelectionKey.OP_ACCEPT);
首先注意到我们是调用ssc也就是我们的channel的注册方法。
然后我们追入一层


同样是个抽象方法
我们关注它的几个传参和它这一个返回的对象 。
我

这段代码是 Java NIO 中 SelectableChannel 的核心方法之一,用于将当前的通道(Channel)注册到某个 Selector 上 ,并指定它感兴趣的 I/O 事件(如读、写、连接等)。
传参解释
- sel: 目标 Selector。
- ops: 感兴趣的事件(如 SelectionKey.OP_READ)。
- att: 附加对象,可以绑定用户定义的上下文信息(比如 Socket 对象、请求 ID 等)。
我们来精读这段代码首先这是段加锁的代码
然后几个If的流程是判断channel有没有打开,传入的事件是否合理。以及 blocking: 如果是阻塞模式,NIO 不允许注册,需要改为非阻塞后才能注册 Selector。
然后基于传入的selector查找是否存在并尝试获取到已有 SelectionKey
如果没有旧的 Key,就新注册一个
然后返回一个 SelectionKey对象。
也就是说从SelectionKey的获取方式我们判断SelectionKey 是基于selector对象的。我们来研究下key和selector的区别?

首先这是一个抽象类
然后里面包含了selector和channel两个类

还实现了一些事件的说明以及一些钩子类判断函数


我们来看看它的注释

这第一句很重要
这个类表示使用选择器注册SelectableChannel的token(令牌)。
然后还有
一个key里面包含两个表示为整数值的操作集。操作集的每一位都表示该channel支持的可选操作的类别。


关注这两段话
key的就绪集表明它的channel已经为某些操作类别做好了准备,这是一个提示,但不是保证,这样一个类别中的操作可以由线程执行而不会导致线程阻塞。在选择操作完成后,一个现成的集合最有可能是准确的。外部事件和在相应channel上调用的I/O操作可能会使它变得不准确。
该类定义了所有已知的操作集位,但是给定通道支持哪些位取决于通道的类型。SelectableChannel的每个子类都定义了一个validOps()方法,该方法返回一组标识通道支持的操作的集合。试图设置或测试密钥通道不支持的操作集位将导致适当的运行时异常。
我们来问个问题为什么这里的标识并不可靠。那为什么key还需要定义操作集呢?
这是因为 I/O 是一个 高度并发、系统级别不确定性很强的过程
- 你在 Selector.select() 后看到 OP_READ,是因为内核告诉你:这时 socket 上有数据。
- 但你还没 read() 的时候,别的线程可能已经把数据读光了 。
- 或者,对端把连接关了,这时候 read() 也可能返回 -1。
- 又或者你一看 OP_WRITE 为真,但写入数据时发现 buffer 满了,还是得阻塞。
换句话说:
由于并发访问 + 操作瞬变性 + 内核不可控事件 ,NIO 不能保证 readyOps 是永久准确的。
虽然 ready set 不保证"操作一定不会阻塞",但它仍然极大提高了性能与控制力 。
readyOps 是"hint",但:
- 操作集本身是用户和内核/底层通信的协议桥梁 。
- 即便是 hint,它也是反映了内核在你 select 时刻返回的 I/O 状态。
- 它可以极大减少你盲目轮询或阻塞的开销 。
回到代码

我们发现key本身是又是在selectionKeympl中实现的。

所以本质上来说key本质是从selector获取也就是说在selector的层实现的一个运行时关联?
所以整个注册过程就是依赖复用key对象或者说是创建key对象

我们再关注注册逻辑中的这一句调用时。我们发现本质是通过传入的selectror使用它的注册方法
所以 本质上 注册 是 基于 s e l e c t o r 的 注册
观察 其 实现

重点 是 这 一句 调用 k e y 的 实现 在 这里 调用 这 是 一个 n e w 逻辑 当 f i n d 找不到 复用 k e y 对象 是 就 由 s e l e c t o r 发 起 的 一个 新建 k e y 。 我们 可以 看到 k e y 的 新建 需要 借助 k e y 和 c h a n n e l 也 更加 说明 了 k e y 是 c h a n n e l 和 s e l e c t r o 的 关联 和 桥梁

回调 通知机制

观察demo代码的

我们追一层是是一个抽象方法

在selectormpl实现 注意返回Int类型

大概是一个等待超时抛出异常的分支。

我们关注这个方法
一个状态校验之后 重点是这个方法。

这又是一个抽象类

注意这里是mpl和上文的windowmpl是一个层级和windowprovider相关的类是不相属的。
在对应的操作系统类实现 。我们把jdk切成liunx版本的

直接看epollmpl的
有点复杂我们精读下先判断这个是否合法状态

this.processDeregisterQueue();
如果是合法状态执行这一句。这一句是什么意思?

在selectormpl实现可以理解为属selector 层的行为
这段代码中有一句
Set var1 = this.cancelledKeys();
我们再来看看这一句是什么?

这是一个selectorKey 的set(集合)也就是说(待取消的)key和selector是可以多对一的。多对一
然后后面的逻辑大概是从这个set中取一个key然后走

这个调用

同类的抽象方法在epollmpl实现

大致是一系列递归的移除注册的逻辑
回到


begin() 通过注册 interruptor 钩子,使得当前线程在阻塞期间可以被其他线程中断,从而调用 wakeup() 打破 select() 的阻塞。
然后这句调用很关键
this.pollWrapper.poll(var1);

追进一层有个epollwait

epollwait追进去就是native了系统调用
然后回到

int var3 = this.updateSelectedKeys();
有句这个调用我百度了一下是将wait返回的fd转换为key
而在系统调用前后
调用两次 processDeregisterQueue() 是为了确保:
在执行 epoll_wait 之前和之后,都清除掉所有被取消的 SelectionKey
我们怀疑其他系统的调用的时机是什么样的?
e p o l l c t l

我们通过检索发现ctl是在这里调用的

还有

和

针对
initInterrupt
针对方法我们发现这个方法的上层。居然是在构造器实现的。也就是说selector的创建的时候就已经在操作系统层面预注册了?
但是我们关注它的这个构造器传参这是一个provider这个眼熟吗?这个类又是什么时候被初始化的呢?

则是要在这个初始化的去做 。 但是 我们 发现 这条 链路 传参 是 没有 关联 信息 的 也就是 说 这是 一个 仅仅 是 一个 初始化 的 e p o l l c tl 和我 们 的 目的 不符合 。
针对 updateRegistrations
我们 发现 在 我们 发现 e p o l l wait 的 时候 也就是 这段 代码

它 就 先 调用 了 我们 的 目标 方法 。 所以 c t l 和 w a i t 方法 是 在 同一个 方法中 处理 的
而且 和 我们 业务 相关 的 c t l 真正 发生 的 节点 是 在 s e l e c t o r 阻塞 方法 中 做到 的 。
那么 我们 先 保留 一个 问题 就是 为什么 c t l 要 在 这里 实现 还有 就是

我们 可以 看到 c t l 的 参数 都是 从 一些 类似 于 事件 的 方法 和 数组 进行 获取 的 。 是 个 无参 输入 。
我们不禁要问的是支撑ctl完成业务的数据结构到底是什么样的?

先来 看 这个 v a r 3

这是 一个 6 4 位 的 数组
然后 v a r 2 是一个
一个 角标
v a r 4 是 一个 比较 复杂 的 方法

大致 是 某种 运算 返回 一个 b y t e
var 5 是 一个 标志位 我们 暂时 先 不 关注 我们 关注 这个 数组 和 这个 计数
除了 当前 方法 对 这个 数据 结构 还有 做出 操作 的 就是

这个 方法

而 这个 方法 在 m p l 层 唯一 的 引用 就是 这个 Put e v e n t o p s 之类 的 方法 。
而 这个 方法 被 引用 的 地方 则是

do s e l e ctor 方法
我们 关注 它 的 传参
this.pollWrapper.interruptedIndex()
方法

返回一个角标
传入 这个 角标和 0 之后
进入 这个 函数 (warpper)
做 了 一个 运算 然后 传入 了 一个 p u t i n t 的 方法

其中 这个 对象


我们 才知道 0 1 其实 是 个 标志位
往下 追 一层

往下 追 一层

是 个 n a v i t e 方法
我们 研究 这个 对象 的 两个 出口

在 第二个 出口 中 我们 发现 了 一个 老熟人

至此 以及 由 闭环 了 我们 反思 一下 全 流程 大概 做 了 什么 事 。
我们 要 研究 什么 样 的 数据 结构 支撑 c t l 然后 我们 定位到 以 一个 标志位 和 一个 数组
然后 我们 盘问 这个 数据 结构 在 哪里 被 引用 除了 当 前 方法 还有 一个 p u t e v e n t 的 方法 而 这个 方法 的 调用 呢 则是 在 d o sele c t o r 然后 这个 方法 。 但是 呢 d o s e l e c t o r 层 的 方法 调用 的 是 w o r p p e r 层 的 方法 。 传入 的 是 两个 数字 其中 一个 是 角标 一个 是 标志位 。 然后 通过 n a v i t e 之类 的 方法 算 了 一个 a d r e s s 子类 的 数据 。 然后 呢 这个 数据 又被 我们 的 p o l l 方法 调用 到 了 。
我们 都 直到 p o l l 方法 是 和 s e l e c o t r 有 关联 的 。
我们 可以 明确 的 是 我们 在 跟踪 put e v e n t 的 时候 走错 层 了 。 但是 我们 仍然 能够 挖掘 出 有 价值 的 东西 。
驱动 整个 event o p s 分支 的 的
if (this.pollWrapper.interrupted()) {
this.pollWrapper.putEventOps(this.pollWrapper.interruptedIndex(), 0);
synchronized(this.interruptLock) {
this.pollWrapper.clearInterrupted();
IOUtil.drain(this.fd0);
this.interruptTriggered = false;
}
}
是 一个 中断 位 回顾 整个 上下游 的 代码

我们 其实 不难 发现 e v e n t poll 是 在 真实 的 p o l l 之后 触发 的 。
而 这个 方法 的 作用 呢 ? 也是 某种 更新 上下 游 的 机制 。

然后 这个 方法 影响 的 是 p u t 这个 数据 结构 影响 的 是 p o l l 方法 的 这里 的 分支
也就是 说 我们 可以 推断 在 这个 数据 结构 本质 的 调用 在于 中断 的 时候 保存 上下文 。 然 后 下一次重入p o l l 的 时候 会 通过 这个 f o r
for(int var3 = 0; var3 < this.updated; ++var3) {
if (this.getDescriptor(var3) == this.incomingInterruptFD) {
this.interruptedIndex = var3;
this.interrupted = true;
break;
}
}
对齐 我们 的 i n d e x 和 我们 u p ted 标志位 。
然后 回顾 全局 想要 解决 问题 又有 两个 分支 。 一个 呢 是 研究 s e l e c t or 层 p u t e v e n t 调用 的 时机 一个 呢 则是 研究 这个 维护 的 i n d e x 又 影响 那些 东西 ?
我 终于 找到 了 s l e c t o r 层 的 这个 方法 的 调用 。

然后 这个 方法 的 上游 是

再 上 游 是

然后 上游 是

是 这个
至此 终于 闭环 了 这个 数据 结构 是 在 注册 的 时候 写入 的 维护 的 一个 数据 结构 。
epollcreate

e p o l l c r e a t e 是 在 构造器 中 被 引用 的
在selector 层 的 实现 如 上
然后

再往 上

往 上
就是 我们 的 o p e n 方法 了 . . . . . .