netty-Java读源码之Netty深入剖析-imooc

第一章:

selectionKey = javaChannel().register(eventLoop().selector(),0,this)

​ selector,注册的时候需要关心的事件这里为0只是把channel绑定到selector上去,netty在注册到selector上的时候,是调用jdk底层的channel去注册并把this服务端的channel通过attachment

目的:selector轮询到java channel的读写事件之后可以直接把attacment拿出来,针对netty的channel做一些事件的传播。

第二章:

Netty.md

第三章:Netty服务端启动

问题3:端口绑定

(1)在创建服务端channel的时候,会创建一个jdk底层相关的channel进行保存,保存到成员变量里面。

 

总结端口绑定:

abstractUnsafe.bind()进入口调用doBind()方法,doBind()方法调用jdk的api做一个实际端口的绑定,绑定完成之后会触发一个

pipleline的chanelActive事件,而channelActive最终会从headContext触发,headContext会将事件向前传播再调用一个

readIfIsAutoRead()方法触发read事件,而read事件通过层层调用,最终调用到NioSocketserverChannel的beginRead()方法

而beginRead方法完成向selector注册一个accept事件。

问题4:服务端启动总结

 

1:首先调用newChannel()创建服务端的channel,这个过程实际上就是调用jdk底层的api来创建一个jdk的channel, 然后netty将其包装成一个自己服务端的channel,同时会创建一些基本组件,绑定到此channel上,比如pipeline,

2:调用init()初始化服务端channel,这个过程最重要就是给服务端channel添加一个连接处理器,随后调用Register方法注册

selector,这个过程中netty将jdk底层的channel注册到事件轮询器selector上面,并把netty的服务端channel作为一个attachMent绑定到对应的jdk底层的channel上,最后调用doBind()方法来实现对本地端口的监听后,netty会重新向selector注册一个Accept事件,这样netty就可以接受新的连接了。

第四章:NioEventLoop

问题1:默认情况下,Netty服务端起多少线程?何时启动?

多少线程?

构建NioEventLoopGroup不传参数的时候,会启动默认CPU两倍核数的线程,

启动?

在调用execute方法的时候会判断,当前是否在本线程,如果是在本线程,那么线程已经启动,
如果是在外部线程调用execute方法,首先会调用startThread()这个方法,这个方法首先会判断当前线程是否启动 
如果没有启动就启动这个线程。

 

问题2:Netty是如何解决空轮训bug的?

如果当前阻塞select操作实际上并没有花这么长时间,就会触发一次空轮训bug,默认情况下这个现象达到512次,就重建一个selector,把之前selector上面的所有key,移交到新的selecor上面,避免jdk空轮训的bug。

 

问题3:Netty如何保证异步串行无锁化?

netty在所有外部线程去调用eventloop和channel的一些方法的时候,通过inEventLoop()这个方法判断得出是外部线程,这种情况下把所有操作封装成一个task丢掉mpscQueue里面,然后在nioEventLoop执行逻辑的第三个过程这些task会挨个执行。

问题4:NioEventLoop创建

 

4.1:NioEventLoopGroup()通过构建创建,不传参数的情况下默认创建两倍CPU核数的NioEventLoop();

 

4.2:在创建NioeventLoopGroup()过程中,首先会创建一个ThreadPerTaskExecutor()线程执行器,

这个线程执行器的作用就是负责创建NioEventLoopGroup对应的底层线程

 

4.3:然后通过通过for循环创建NioEventLoop对象数组,创建每个NioEventLoop的时候会调用一个newChild方法,给每一个NioEventLoop

配置核心的参数

 

4.4:最后创建一个线程选择器的作用:给每一个新连接分配NioEventLoop线程

4.5 newChild()具体做了那些事情

  • 保存线程执行器ThreadPerTaskExector

  • 创建一个MpscQueue (保存一个业务的任务队列)

  • 创建一个selector(去轮询注册到NioEventLoop上的连接)

    知识点:

     

    taskQueue:用在外部线程在执行netty的一些任务的时候,如果判断不是在nioEventLoop对应的线程中去执行,而是直接塞到一个任务队列里面由NioEventLoop对应的线程去执行。

4.6 chooserFactory.newChooser()给新连接绑定NioEventLoop

  • 执行过程

 

  • 优化过程

     

 

netty为什么这么做?

&(底层支持)比%(基于库实现的)效率要高很多

 

问题5:NioEventLoop启动

 

  • 服务端启动绑定端口

  •  

    首先bind()方法将绑定的具体操作封装成一个task,然后调用eventLoop的execute()方法

     

    然后netty会判断调用execute的线程不是nioeventloop线程,于是调用startThread()方法尝试创建线程,

     

    创建线程是线程执行器 创建,线程执行器的作用:每次执行任务的时候都会创建一个线程,这里创建出来的线程就是nioEventLoop底层的线程

     

    创建之后,nioEventLoop这个对象会对线程进行保存,

    保存的目的就是为了判断后续对nioEventLoop的相关执行线程是否是本身 ,

    如果不是就封装成一个task扔到它的taskQueue里面去串行执行(能够做到线程安全),

     

    最后调用run方法(是驱动NioEventLoop的核心方法)

  • 总结:

    bind()将实际绑定流程封装成一个Task然后调用服务端channel.eventLoop().execute去具体的执行,然后netty会判断调用execute方法不是nioEventLoop线程,于是调用startThread()创建线程,具体创建就是ThreadPerTaskExecutor.execute()每次执行都会新创建一个线程,对应的就是nioEventLoop底层对应的一个线程,创建线程具体逻辑,首先nioEventLoop会将当前创建完的这个线程进行保存,最后调用一个run方法进行nioEventLoop的启动。

  • 新连接接入通过chooser绑定一个NioEventLoop

问题6:NioEventLoop执行

1:调用run方法,启动nioEventLoop

run方法里面的逻辑是,代码中有一段无限for循环,for循环里面它干了三件事情。

 

首先调用select(),轮询注册到selector上面连接的io事件,

 

然后通过调用processSelectedKeys()处理上一次轮询出来的io事件。

 

最后通过调用runAllTasks()方法处理外部线程扔到TaskQueue里面的任务


2:select()方法执行逻辑,检测IO事件

 

2.1:dealine以及任务穿插逻辑处理

2.2:阻塞式select

select操作其实也是一个无限for循环,首先计算本次执行select的截止时间,截止时间主要根据eventloop当前是否有定时任务需要处理。

以及判断在select的时候是否有任务需要处理,也就是说你在select的时候需要执行一个任务,那么select操作就会停止。

否则会进入上一个过程,也就是说没有到截止时间或者任务队列为空,就会进行一次阻塞式的selec操作(select操作默认1s), 在这个过程中外部线程

也可以将当前select操作进行唤醒,


2.3:避免jdk空轮训的bug

 

解除nio空轮询的bug过程,采用非常巧妙的方法避免了bug。


2:源码阅读

 

2.1:deadline以及任务穿插逻辑处理

 

 

 

 

2.2:阻塞的select操作

 

2.3:避免空轮询bug

 

2.4:总结

首先select操作会进行deadline处理,然后判断当前有任务在里面就终止本次select操作,那么如果没有到截止时间,并且taskQueue里面没有任务那么就进行一次阻塞式的select操作,那么在select阻塞式操作结束以后,判断这次select操作的是否真正阻塞了这么长时间,如果没有阻塞这么长时间就触发了jdknio的空轮训bug,接下来netty会判断空轮训bug的次数是否达到阈值,达到阈值通过替换selector操作的方式巧妙避开了空轮训。

3:processSelectedKey()执行逻辑 (处理IO事件)

3.1:selected keySet优化

话述:

select操作每次都会把就绪状态的IO事件添加到底层的hashSet数据结构,而netty会通过反射的方式将hashset替换成数组的实现,这样才任何情况下select操作的时间复杂度都是O(1),优于hashset。

源码解析:

 

 

 

 

 

3.2:processSelectedKeysOptimized()去真正的处理io事件

 

 

 

3.3:总结

netty在默认情况下会通过反射将selector底层hashset转化成数组优化然后在处理每一个keyset的时候都会拿到attachment(NioChannel), attchment就是向selector注册IO事件的时候绑定的经过netty封装后的channel

4:runAllTasks()执行逻辑

​ ~ task的分类和添加

 

​ task的执行,这些task包括普通task和定时任务task,分别存放于不同的任务队列里面,nioEventLoop通过暴露两个方法来进行task的添加

 

 

​ ~ 任务的聚合

 

 

​ 把定时任务队列的的task聚合到普通的taskQueue里面

​ ~ 任务的执行

 

​ 然后挨个执行每个任务。

~总结:

nioEventloop最后一个过程就是执行任务,而这个任务分为两种,一种是普通任务,一种是定时任务,netty在执行这些任务的时候,首先会把任务聚合到普通任务队列里面,然后挨个执行每个任务,并且在每次执行过64个任务后,计算当前时间是否超过最大允许的执行时间,如果超过就直接中断,就直接进行下次循环。

5:本章总结

用户代码在创建workgroup和bossgroup的时候nioEventLoop被创建,默认不传的参数的时候会创建2倍cpu核数的nioEventLoop,
每个nioEventLoop都会有个chooser替线程做分配,chooser会针对eventGroup的个数做一定的优化,
nioeventloop在创建的时候会创建一个selector和定时任务队列,在创建selector的时候netty会通过反射的方式,用数组实现替换到selector里面的两个hashset数据结构 
nioEventLoop在首次调用execute方法的时候启动线程,而这个线程是xxxx,启动线程知乎,netty会将线程保存到成员变量这样就能判断执行nioEventLoop里面的逻辑是否是本线程
NioEventLoop的执行逻辑在run()方法里面,主要包括三个过程,
检测IO事件
处理IO事件
执行任务队列

第五章 Netty处理新连接接入

问题一:Netty是在哪里检测有新连接接入的?

一句话:boss线程第一个过程轮询出accpet事件,boss线程的第二个过程通过jdk的底层的accpet方法创建连接。

问题二:新连接是怎样注册到NioEventLoop线程的?

简单来说就是boss线程调用chooser的next方法,拿到一个NioEventLoop,然后将这条连接注册到EventLoop的selector上面去

问题三:Netty新连接接入处理逻辑

 

1:检测新连接

····processSelectedKey(key,channel)[入口]

········NioMessageUnsafe.read()

············doReadMessages()[while循环]

················javaChannel.accept()【创建JDK的channel】

总结:在服务端NioEventLoop的第二个过程processSelectedKey检测出accpet事件之后,通过jdk的accpet方法创建jdk的channel,然后包装成netty自定义的channel,在这个过程中通过Handle这个对象去控制连接接入的速率,默认情况下一次性读取16个连接

2:创建NioSocketChannel

问题:客户端channel(new NioSocketChannel) 直接new出来 ,而服务端channel是通过反射的方式创建,netty为什么会这么设计?

new NioSocketChannel(parent,ch)[入口]做了两件事情

1:调用父类构造函数做了一些事情:AbstractNioByteChannel(p,ch,op_read)

​ 1.1:配置此channel为非阻塞,然后将感兴趣的读事件op read,保存到成员变量,方便后续注册到selector上:configureBlocking(false)& save op

​ 1.2:创建和此channel相关的一些组件创建id作为channel的唯一标识,创建unsafe作为底层数据的读写,创建pipeline作为业务逻辑的载体:

​ create id,unsafe,pipeline

2:创建了一个和NioSocketChannel绑定的配置类:new NioSocketChannelConfig()

​ 2.1:setTcpNoDelay(true),禁止Nagle算法,让小的数据包发出去,尽可能降低延时。

3:向selector注册读事件

1:服务端Channel的pipeline构成

 

Head->ServerBootstrapAcceptor->Tail


2:unsafe的

 

最终会把客户端的每条连接通过fireChannelRead传播的ServerBootstrapAcceptor的ChannelRead方法


3:ServerBootstrapAcceptor的channelRead主要做以下的事情

3.1:添加channelHandler:首先将用户自定义的channelHandler添加到新连接的pipeline里面

3.2:然后设置options和attrs

4:分配线程及注册selector

next()调用chooser分配线程选择NioEventLoop并将事件注册到selector

image-20211115160532748

5:channel的分类

5.1:NioServerSocketChannel:服务端channel

5.2:NioSocketChannel:客户端channel

5.3:Unsafe:实现每种channel底层具体的协议

层级关系

1:Channel:读,写,连接,绑定的抽象

2:AbstractChannel:骨架类的实现,通过成员变量看出不管是客户端NioSocketchannel还是服务端NioServerSocketchannel都会逐层调用父类的构造函数,最终会调用到AbstractChannel的构造方法

3:AbstractNioChannel:使用selector的方式实现IO事件的监听,维护selector

服务端channel和客户端channel的区别

1:客户端向AbstractNioChannel注册一个读事件,服务向AbstactNioChannel注册了一个Accept事件,客户端和服务端共同部分由AbstractNioChannel实现而AbsractNioChannel维护selector

2:对应的unsafe不一样,客户端NioByteUnsafe,服务端是NioMessageUnsafe,unsafe主要实现每种channel的读写抽象,服务端channel的读读一条新的连接,客户端channel的读指的是读取io数据。

channel的配置继承关系

第六章 pipeline

1:pipeline是netty的大动脉,主要负责事件的传播。

2:netty是如何判断ChannelHandler类型的?

3:对于ChannelHandler的添加应该遵循什么样的顺序?

4:用户手动触发事件传播,不同的触发方式有什么样的区别?

5:pipeline的初始化

​ 5.1:pipeline在创建Channel的时候被创建

​ 5.2:pipeline节点数据结构:ChannelHandlerContext

​ 5.3:Pipeline中的两大哨兵:head和tail

6:添加ChannelHandler

1:添加channelHandler

 

2:判断是否重复添加

 

3:创建节点并添加至链表

​ 3.1:节点就是channelHandlerContext,

​ 3.2:把channelHandler包装成channelHandlerContext添加到链表

3.3:回调添加完成事件

总结:如果channel不是sharable并且已经添加过了,那么就拒绝添加,如果channel是sharable或者没有被添加过,接下来就创建一个channelhandlerContext节点,把channelHandler包装进去,将节点通过链表的形式添加到pipeline中,最终回调添加完成事件(channelInitializer,回调到用户的方法把自身删除)。

7:删除ChannelHandler

删除场景

1:找到节点

2:链表的删除

3:回调删除Handler事件

 

总结: 通过遍历列表的方式找到当前chanelhandler对应的chanelhandlercontext节点,然后第二个过程通过一个标准的链表删除方式,把当前的这个channelhandlercontext节点从链表中删除,最后一个过程是回调删除事件。


8:inBound事件的传播

1:何为inBound事件以及ChannelInboundHandler

事件传播注意的地方

ctx.fireChannelRead(msg)是从当前节点往下传播。

ctx.channel().pipeline().fireChannelRead("hello world")是从head节点往下传播


2:ChannelRead事件的传播

3:SimpleInBoundHandler处理器

 

SimpleInBoundHandler会帮你释放内存

9:事件和异常的传播

 

posted @ 2023-05-24 16:08  CharyGao  阅读(207)  评论(0)    收藏  举报