jdk1.8 nio相关。java对象和epoll三大函数怎么关联的?(有点乱有点跳)
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使用它的注册方法
所以本质上注册是基于selector的注册
观察其实现
重点是这一句调用key的实现在这里调用这是一个new逻辑当find找不到复用 key对象是就由selector发起的一个新建key。我们可以看到key的新建需要借助key 和channel 也更加说明了key 是channel 和selectro的关联和桥梁
回调通知机制
观察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
我们怀疑其他系统的调用的时机是什么样的?
epollctl
我们通过检索发现ctl是在这里调用的
还有
和
针对
initInterrupt
针对方法我们发现这个方法的上层。居然是在构造器实现的。也就是说selector的创建的时候就已经在操作系统层面预注册了?
但是我们关注它的这个构造器传参这是一个provider这个眼熟吗?这个类又是什么时候被初始化的呢?
则是要在这个初始化的去做。但是我们发现这条链路传参是没有关联信息的也就是说这是一个仅仅是一个初始化的epollctl和我们的目的不符合。
针对updateRegistrations
我们发现在我们发现epollwait的时候也就是这段代码
它就先调用了我们的目标方法。所以ctl和wait方法是在同一个方法中处理的
而且和我们业务相关的ctl真正发生的节点是在selector阻塞方法中做到的。
那么我们先保留一个问题就是为什么ctl要在这里实现还有就是
我们可以看到ctl的参数都是从一些类似于事件的方法和数组进行获取的。是个无参输入。
我们不禁要问的是支撑ctl完成业务的数据结构到底是什么样的?
先来看这个var3
这是一个64位的数组
然后var2是一个
一个角标
var4是一个比较复杂的方法
大致是某种运算返回一个byte
var5是一个标志位我们暂时先不关注我们关注这个数组和这个计数
除了当前方法对这个数据结构还有做出操作的就是
这个方法
而这个方法在mpl层唯一的引用就是这个Puteventops之类的方法。
而这个方法被引用的地方则是
doselector 方法
我们关注它的传参
this.pollWrapper.interruptedIndex()
方法
返回一个角标
传入这个角标和0之后
进入这个函数(warpper)
做了一个运算然后传入了一个putint的方法
其中这个对象
我们才知道0 1 其实是个标志位
往下追一层
往下追一层
是个navite方法
我们研究这个对象的两个出口
在第二个出口中我们发现了一个老熟人
至此以及由闭环了我们反思一下全流程大概做了什么事。
我们要研究什么样的数据结构支撑ctl然后我们定位到以一个标志位和一个数组
然后我们盘问这个数据结构在哪里被引用除了当前方法还有一个 putevent的方法而这个方法的调用呢则是在do selector然后这个方法。但是呢do selector层的方法调用的是worpper层的方法。传入的是两个数字其中一个是角标一个是标志位。然后通过navite之类的方法算了一个adress子类的数据。然后呢这个数据又被我们的poll方法调用到了。
我们都直到poll方法是和selecotr有关联的。
我们可以明确的是我们在跟踪 putevent 的时候走错层了。但是我们仍然能够挖掘出有价值的东西。
驱动整个eventops 分支的的
if (this.pollWrapper.interrupted()) {
this.pollWrapper.putEventOps(this.pollWrapper.interruptedIndex(), 0);
synchronized(this.interruptLock) {
this.pollWrapper.clearInterrupted();
IOUtil.drain(this.fd0);
this.interruptTriggered = false;
}
}
是一个中断位回顾整个上下游的代码
我们其实不难发现eventpoll是在真实的poll之后触发的。
而这个方法的作用呢?也是某种更新上下游的机制。
然后这个方法影响的是put这个数据结构影响的是poll方法的这里的 分支
也就是说我们可以推断在这个数据结构本质的调用在于中断的时候保存上下文。然后下一次重入poll的时候会通过这个for
for(int var3 = 0; var3 < this.updated; ++var3) {
if (this.getDescriptor(var3) == this.incomingInterruptFD) {
this.interruptedIndex = var3;
this.interrupted = true;
break;
}
}
对齐我们的index 和我们upted标志位。
然后回顾全局想要解决问题又有两个分支。一个呢是研究selector层putevent调用的时机一个呢则是研究这个维护的index又影响那些东西?
我终于找到了slector层的这个方法的调用。
然后这个方法的上游是
再上游是
然后上游是
是这个
至此终于闭环了这个数据结构是在注册的时候写入的维护的一个数据结构。
epollcreate
epollcreate是在构造器中被引用的
在selector层的实现如上
然后
再往上
往上
就是我们的open方法了......