zl程序教程

您现在的位置是:首页 >  云平台

当前栏目

Netty网络编程第五卷

网络编程 Netty
2023-09-14 09:13:35 时间


原本只打算写四卷,结果第四卷结尾只分析到了netty启动初始化部分的源码,特开第五卷将netty剩余重点部分源码分析完毕


NioEventLoop 剖析

学习NioEventLoop之前,先要搞清楚NioEventLoop的重要组成是:Selector,线程,任务队列; 以及NioEventLoop既会处理io事件,也会处理普通任务和定时任务

NioEventLoop 线程不仅要处理 IO 事件,还要处理 Task(包括普通任务和定时任务),

NioEventLoop的探究过程从下面几个问题展开

1.Selector何时创建

先来看NioEventLoop这个类

在这里插入图片描述
找到了selector,下面找一下thread对象,thread对象要去其父类里面找

下面是继承图
在这里插入图片描述
在NioEventLoop的爷爷类SingleThreadEventExecutor中定义了线程对象和普通任务队列
在这里插入图片描述
找其曾祖父类AbstractScheduledEventExecutor

在这里插入图片描述

到此我们可以看出NioEventLoop具备处理io,普通任务和定时任务的能力


下面追踪一下Selector的初始化过程,首先就需要看NioEventLoop的构造方法了

在这里插入图片描述
在这里插入图片描述
对比下面nio源码:
在这里插入图片描述

到此,第一个问题回答完毕,selector是在NioEventLoop的构造方法中进行创建的


2.eventLoop为何有两个selector成员变量

在这里插入图片描述
下面分析为什么不用原生的selector,反而整出两个selector?

  • selector都有一个selectionKeys集合,该集合实现是set,set遍历性能不高,因为底层实现是哈希表
  • netty对此做了优化,将原生的selectionKeys底层的set集合换成了基于数组的实现

替换的源码展示如下:

在这里插入图片描述
为什么还要保留原始的seletor,而不直接使用包装后的selector?

在这里插入图片描述

  • 包装后的selector只是用来遍历selectionKeys的
  • 原生的selector的一些功能无法由包装后的selector实现,一些功能还是需要基于原生selector实现

答案:为了在遍历selectedKeys时提高性能


3. eventloop的nio线程在何时启动

提交任务代码 io.netty.util.concurrent.SingleThreadEventExecutor#execute
在这里插入图片描述
下面探究execute方法源码

首先我们主要execute方法所属的类---->SingleThreadEventExecutor从名字可以看出,这是一个单线程的任务执行器,这也很好理解,可以把NioEventLoopGroup看做一个线程池,线程池是懒加载的,并且线程池里面每一个线程执行器就是SingleThreadEventExecutor

还需要注意一点,我们上面在NioEventLoop的组成时分析的:,即SingleThreadEventExecutor有一个成员变量thread用来保存当前开启的线程,因为是懒加载的,初始为null
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
同理真正干活的是doStartThread方法

在这里插入图片描述

首先验证excutor中任务的调用时通过nio线程异步调用的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


下面我们再继续验证
在这里插入图片描述
线程工厂创建出来的线程启动执行run方法,run方法中会将新创建的线程赋值给单线程执行器的thread对象,
并且该线程执行完run方法后不会销毁,会进入死循环不断寻找新的任务执行
在这里插入图片描述


答案:当首次调用execute方法的时,nio线程启动,并且通过一个state状态位来控制线程只会启动一次
在这里插入图片描述
在这里插入图片描述


4. 提交普通任务会不会结束select阻塞

书接上回,当nio线程创建完毕启动后,会进入一个死循环
在这里插入图片描述
在这里插入图片描述

新创建出来的nio线程不仅处理io事件,其他任务来了也需要处理,因此nio线程不能无限阻塞下去,需要在有任务的时候醒过来,因此我们可以推断出,当有新任务来的时候,会唤醒阻塞的nio线程,当超时时间到的时候,也会唤醒阻塞的nio线程


下面验证提交普通任务,是否会唤醒阻塞的nio线程

在这里插入图片描述

在这里插入图片描述
显然这里excute方法调用的就是子类SingleThreadEventExecutor的实现,为什么不是AbstractEventExecutor呢?,因为AbstractEventExecutor是SingleThreadEventExecutor的父类实现

这里由于继承链比较多,很多方法都是调用父类的方法,有时候有会调用子类的实现,看起来比较迷糊

在这里插入图片描述


在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

提交普通任务会唤醒在select除阻塞住的nio线程,让他来处理我们提交的普通或者定时任务,而select阻塞监听的是io事件
总结:首先先从eventloopgroup中获取一个eventloop对象(本质是一个单线程执行器,内部维护一个thread对象)----》eventloop提交任务--->先将任务加入队列---->创建线程(通过线程工厂创建一个线程,赋值给成员变量thread),并用标记位防止重复创建--->thread赋值完毕后,会去运行传入的任务,该任务就是一个死循环,负责不断寻找新的可执行任务


5.wakeup方法中代码如何理解

在这里插入图片描述

NioEventLoop:
在这里插入图片描述
在这里插入图片描述
这里说的nio线程就是每个单线程执行器里面对应的成员变量thread

如果是nio线程自己去提交任务,不会执行wakeup(),它内部有唤醒的机制


在这里插入图片描述
为什么需要防止wakeup被重复调用呢? —>因为wakeup方法是重量级操作,很消耗系统资源

在这里插入图片描述


6.每次循环的时候,什么时候会进入SelectStrategy.SELECT分支

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下面我们看一下selectNow()方法

在这里插入图片描述
在这里插入图片描述

当没有任务的时候,才会进入SELECT分支,当有任务的时候,会调用selectNow方法,顺带拿到io事件

在这里插入图片描述

在这里插入图片描述


7.何时会select阻塞,会阻塞多久

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
此时select的沉睡时间会被设置为long的最大值,即无限时长阻塞。直到有io事件发生,或者有任务出现,被其他线程唤醒


在这里插入图片描述
在这里插入图片描述
主要是为了防止定时任务不能被及时执行,因此如果存在定时任务,那么select就不能阻塞,或者在定时任务执行之前结束阻塞
在这里插入图片描述
执行完之后,再次进入下一轮循环,继续寻找任务和io事件进行处理

从上面的分析可以看出,一个创建完毕的nio线程,会不断循环处理io事件,普通任务和定时任务,还是非常勤恳的


8.nio空轮询bug在哪里体现,如何解决

nio的空轮询bug体现在,即使没有io事件发生,select也不会阻塞住,导致cpu占用率飙升,白白浪费cpu时间,并且如果同时存在好多个nio线程空轮询,cpu就被压榨没了

netty是如何解决这个bug的呢?

很简单,通过一个循环计数解决

在这里插入图片描述
每循环一次,计数加一
在这里插入图片描述
既然通过计数来防止空轮询bug,那么如何避免不是空轮询,而是真正有事件发生的循环导致计数累加呢?

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
jdk底层nio的selector的空轮询bug是发生在linux上面


9.ioRatio控制什么,设置100有什么作用

首先,我们必须明白,一个nio线程在一个死循环里面不断轮询执行各种io,普通,定时任务,如果任务的执行占据了很长的时间,那么io事件就不能够被及时处理

因此,为了避免普通任务执行时间过长影响io任务,我们需要一个参数来控制普通任务的执行时间占比

  • ioRatio:控制处理io事件的事件占用比例,默认是百分之50,一半时间用来处理io事件,一半时间用来处理任务

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


10.selectedKeys优化是怎么回事

源码体现在下面

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当然,这里也不一定是NioServerSocketChannel,也可能是普通的channel,要看初始化的时候,传入的channel类型,因此a instanceof AbstractChannel的意图所在

这里selectionKey的优化体现在数组下标获取selectionKey的过程,而不是通过查找哈希桶,还可能需要查看对应哈希桶上的链表才能得到对应的结果
在这里插入图片描述
在这里插入图片描述


Accept流程

首先先看一下原生的nio实现

在这里插入图片描述


accept源码跟进

在这里插入图片描述
在这里插入图片描述

我们进入unsafe.read()方法体内部,注意如果是不同的事件,read方法的实现是不同的

在这里插入图片描述

该方法主要是创建SocketChannel,并设置到一个新的NioSocketChannel中,然后设置SocketChannel为非阻塞,拿到监听事件的ops.方便在后续注册到选择器上面,然后还会对nioSocketChannel进行初始化,主要是设置一些默认属性,例如默认pipeline

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果记得初始化过程的小伙伴,应该知道,这里的默认pipeline会创建出头尾两个处理器,然后初始化过程当通道就绪后,会触发active事件,调用每个处理器的active事件,在头处理器中,会处理read事件,如果是accept事件,会注册当前socketChannel到一个选择器上面

在这里插入图片描述
在这里插入图片描述


下面回到unsafe.read方法体内:

在这里插入图片描述
在这里插入图片描述


下面就是将socketChannel注册到选择器上面,然后关注读事件了,该源码体现部分如下

在这里插入图片描述
现在我们来回忆一下初始化过程中,在new channel()后的init()方法中,会添加一个初始化器,该初始化器在后续回调过程中,会向添加一个acceptor处理器,专门处理客户端连接事件的,这里触发的read方法重点就在于通过那个acceptor处理,完成通道在选择器上面的注册和监听事件

在这里插入图片描述
这个register和启动流程的register很相似


在这里插入图片描述

在这里插入图片描述
inEventloop作用就是判断当前调用方线程,和自身绑定的线程是否一致
在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
上面excute方法执行时会新创建一个线程,来执行传入的任务,此时就完成了线程的切换,并且还会将新创建的thread绑定到当前channel上
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
其实就是在触发通道就绪事件这里完成了读事件的注册监听操作—》head处理器中完成

在这里插入图片描述
这里调用链非常长,下面直接刨根问底:
在这里插入图片描述


Read流程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


总结

到此,简单的流程就走完了,后续还会出一卷,对netty整体架构进行分析,把上面的源码流程全部串起来走一遍