Netty

285 阅读16分钟

Netty架构.png

综述:

Netty是什么?

Netty是个基于异步事件驱动的网络编程框架,它的高性能就来自于它的I/O模型和线程处理模型,I/O模型决定如何收发数据,线程处理模型决定如何处理数据。

Netty支持三种I/O模型随意切换我们一般使用NIO线程模型,用一个线程通过Selector就可以监控多个非阻塞channel,就避免了由于频繁的IO阻塞导致线程挂起。而且引入了Channel和Buffer的概念,可以任意操作channel与buffer的数据交互,避免了直接使用IO流的方式顺序操作。

事件驱动模型:Reactor事件分发模型 + Handlers异步处理模型

事件分发模型:Netty基于主从Reactors多线程模型,主Reactor负责客户端的连接请求,从Reactor负责相应channel的读写请求。而在Netty中,主Reactor是一个线程池,从Reactor和工作线程共用一个线程池。主线程池中的每个线程作为一个主Reactor,对应一个accept事件即一个端口;从线程池中的线程负责处理并执行读写事件,使得从线程池可以被充分利用。

Handlers异步处理模型:Netty中的I/O操作都是异步的,采用观察者模式的设计思路。主线程把事件放入事件队列中,另外的线程不断循环消费事件列表中的事件,返回一个ChannelFuture,然后主线程通过Listener机制,判断异步线程是否执行IO完成,并获取IO操作的结果。 事件驱动模型主要包括4个基本组件: 事件队列(event queue):接收事件的入口,存储待处理事件

分发器(event mediator):将不同的事件分发到不同的业务逻辑单元

事件通道(event channel):分发器与处理器之间的联系渠道

事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作

Netty组件

ServerBootStrap与BootStrap是引导,用于配置Netty程序的信息,来串联各个组件。EventLoopGroup和EventLoop,NioEventLoop中维护了一个线程和一个任务队列,负责监听并处理所有的IO操作并异步提交执行任务;ServerSocketChannel和SocketChannel,提供异步的IO操作,负责将网络层中的数据通过ByteBuf读到内存缓冲区中;Selector会轮询Channel并获取这些事件,将事件发送到ChannelPipeLine中。ChannelPineLine可以选择监听并处理自己感兴趣的事件,它可以通过ChannelHandler对事件进行拦截处理、向后向前传播事件。因为Netty中的I/O操作都是异步进行的,所以事件返回ChannelFuture执行结果,我们可以使用它的addListener()方法注册监听器,事件触发后就立即去获取执行结果。

为什么要使用Netty呢?

①NIO需要手动操控Selector、Channel、ByteBuffer,比较繁杂。

②使用NIO进行网络编程,避免不了开辟多线程,使用Reactor模式,所以就涉及到

③直接使用NIO网络编程,对于网络传输中的各种异常情况不好解决。比如粘包拆包,客户端断线重连、网络拥塞等。

④NIO的Selector空轮询,Netty完善解决。

⑤Netty基于可扩展可定制的事件模型和线程模型;Netty减少资源消耗,零拷贝最小化不必要的内存复制。

EventLoopGroup与EventLoop

EventLoop都实现了JUC下的ExecutorService接口,具有线程管理和任务队列,可以直接执行任务。一个EventLoopGroup可以包含多个EventLoop。一个EventLoop绑定一个线程,由EventLoop处理的I/O事件都由绑定在它上面的线程处理。一个Channel只能注册一个EventLoop。但一个EventLoop可能会同时被注册于多个Channel。

NioEventloop的创建

初始化NioEventLoopGroup时:

①创建ThreadPerTaskExecutor任务执行器(相当于异步线程):

使用DefaultThreadFactory创建CPU*2的线程,为每个线程独立命名。然后开启每个线程。

②循环创建NioEventLoop(相当于主从线程):

执行父类SingleThreadEventLoop的构造方法,创建MpscQueue任务队列,并与ThreadPerTaskExecutor绑定!初始化NioEventLoop中的SelectorProvider【梦幻联动,我可以传入IoSelectorProvider】,使用SelectorProvider创建获取原生的Selector。然后通过flag优化选项判断是否优化Selector【梦幻联动,我是直接用原生Selector】,若不优化则直接返回;若优化则反射创建Netty提供的优化Selector,初始化SelectedSelectionKeySet代替原生SelectionKeySet(内部是使用SelectionKey数组代替原生的HashSet,可自动扩容且省去繁杂的remove)。

③创建Executor选择器EventExecutorChooser(用于为任务选择主从线程):

Netty使用工厂模式,通过判断当前需要的TaskExecutor数量是否为2的幂次方来选择创建幂次方Chooser或普通Chooser,普通Chooser就是普通取模选择(自增 % length),幂次方Chooser是通过&运算选择(自增 & length-1)。【所以Netty服务端是使用单Selector与单Executor】

NioEventLoop启动

Netty把 channel绑定NioEventLoop 封装成一个任务,启动这个NioEventLoop(boss Thread)来异步执行这个任务(NioEventLoop本身也是个Executor啊)(补充:因为channel.bind()是异步的且需要时间,所以NioEventLoop会先判断是否绑定成功。若还未绑定则放入Future.listener监听器中,等Future完成后通知并执行)。任务到来后,NioEventLoop把任务放入它自己的MpscQueue队列中,然后将currentThread当前线程绑定到NioEventLoop上,然后执行NioEventLoop.run()方法(NioEventLoop本身就是ExecuteService)。

执行NioEventLoop执行

①轮询感兴趣的I/O事件select(oldWakenUp):

当select()轮询时间超过规定的超时时间、检查任务队列中有任务且selector处于未唤醒状态时(oldWakenUp==false,防止漏掉任务队列中的任务),则执行selectNow()非阻塞唤醒并执行(后续执行就又传入oldWakenUp=false。这样也减少了后面select.wakeup()的次数,提高效率),然后重置计数器。继续下一次轮询。

否则,往下走就再判断时间差是否超过了一次select的执行时间,若超过则说明已经执行了selectNow,重置计数器。继续下一次轮询

否则,判断selectKeys!=0即轮询到了IO事件、wakeuP=true即用户主动唤醒、任务队列中有任务了、第一个任务调度时间到了。则继续下一次轮询。

否则,判断当前时间>=select()前的时间 + select()阻塞时间,则表示发生了空轮询!计数器+1。判断计数器超过512次时,rebulidSelector()重建selector。

rebuildSelector():创建一个新的Selector,将老selector感兴趣的事件转移到新Selector上,然后close关闭老的selector。(空沦陷BUG:在Selector.select()阻塞轮询就绪事件时,它在某些情况下即使没有事件就绪也会被无故唤醒,此时selectedKeys为空,无法进入if块,那么在源码中它会直接跳到方法底部,导致了死循环。)

②处理I/O事件processSelectedKeys():遍历得到的selectedKeys,获取它们的attachment(即channel【可以联动!】),然后获取就绪的ops操作集,&判断操作类型并执行unsafe操作channel中的数据。

③处理任务队列,弹出队列中的任务执行

Selector操作类型

OP_ACCEPT操作类型:

attachment中得到ServerSocketChannel,调用accept()方法得到socketChannel。将SocketChannel与Worker Group中选的一个NioEventLoop绑定,注册OP_READ到NioEventLoop的Selector上。

OP_READ操作类型:

若attachment中收到的数据大于零,则执行接收操作。为Channel分配一个1024字节的ByteBuf负责接收数据,然后把数据传播入Pipeline中。若ByteBuf被读满,则读写指针移动并继续接收。接收完成后会记录接收的数据大小,为下次的OP_READ调整ByteBuf的容量大小。

若attachment中收到的数据小于零,则执行关闭操作。清空当前NioEventLoop的Selector上的SelectorKeys,放弃掉任务队列中的所有等待任务。关闭channel。 OP_WRITE操作类型: 把ByteBuf中的数据写入channel,达到阙值时则停止。连续写16次,若16次之后还没有写完,则调度一个写任务来执行写操作。默认情况下Handler节点使用writeAndFlush()写数据,Write一次就会触发Flush系统调用,吞吐量很低。所以我们可以开启异步写增强。Netty会显示定义每Write多少次才会Flush一次,在没有到flush的次数前,会把flush操作进行schedule调度,防止Write不足而不进行Flush()

Channel

Channel为了支持不同协议和不同I/O模型的任意切换(我并没有支持切换协议与I/O模型,但我支持基于NIO-socket编程的改造模型),Netty抽象出一个顶级父类AbstractChannel,对于不同协议、不同I/O模型的连接都有不同的子类channel与之对应。

Channel创建:

显示地传入NioServerSocketChannle.class,由ChannelFactory来反射创建NioServerSocketChannel:

①由SelectorProvider打开服务器socket通道,返回一个ServerSocketChannel对象

②创建ChannelID、Unsafe对象、ChannelPipeline、ChannelConfig对象,设置readInterestOp为SelectionKey.OP_ACCEPT,设置channel为非阻塞状态。 Channel初始化:

将BootStrap设置的option参数(主要是TCP的属性)、用户自定义参数配置到ChannelConfig对象中。获取ServerSocketChannel中的ChannelPipeline对象,向ChannelPipeline的尾部添加ChannelInitializer初始化器。初始化器添加成功后会触发回调函数,遍历得到所有的ChannelHanderContext,用于下面的handlerAdded,然后再删除ChannelInitializer初始化器(这里 就是把所有处handler理器节点加入pipeline责任链中了!)。

Channel注册

把NioServerSocketChannel注册到EventLoop中的事件轮询器Selector上(底层调用channel.register(selector,Ops)),然后在ChannelPipeline触发handlerAdded事件和channelRegistered事件,触发Handler处理器添加事件和channel绑定事件。

绑定

使用channel.bind(port),然后在ChannelPipeline中触发channelActive事件,事件传播至某个节点会被执行,把selectionKey的兴趣集设置为SelectionKey.OP_ACCEPT表示可以接收新连接(interestOps | SelectionKey.OP_ACCEPT,我这边是直接绑定,Netty是通过事件 + 职责链)。

API

read():从当前channel读取数据到channelPipeline中的第一个inbound缓冲区

write():将数据从channelPipeline写入消息发送环形数组,调用flush()把环形数组中的数据写入到目标channel。]

ChannelPipeline与ChannelHandlerContext

clipboard.png

每一个被创建的Channel都会被分配一个新的ChannelPipeline,它的思想是局部串行 整体并行,要优于一个队列+多个线程的模式,ChannelPipeline内部是由ChannelHandlerContext父类节点组成的双向链表,此链表的头尾为两个默认的子类HeadContext和TailContext,中间为子类ChannelHandler(ChannelInboundHandler入站数据处理器处理读入,即Socket.read();ChannelOutboundHandler出站数据处理器接受I/O请求,职责链处理请求后通过Socket.write()方法写出。)。HeadContext是outbound类型,主要用来向下传播读出数据流;TailContext是inbound类型,主要处理读入数据流的收尾操作。

Channel每一次的状态变化(注册、绑定与取消),都会产生一个对应的事件,事件将会被channelInboundHandler处理,然后被转发给后面的ChannelHandler按序处理,最终返回一个ChannelFuture异步结果。

Pipeline节点添加:

首先检查是否重复添加(Netty通过handler上的@Sharable注解和added属性判断是否添加过,底层是通过名字是否重复),然后把Handler封装到一个AbstractChannelHandlerContext新节点中并加入Pipeline中。此后会判断Channel是否注册到EventLoop上、Executor是否注册到EventLoop上,若还没有则会添加一个Task回调任务,等待Channel注册成功后再调用添加节点后的回调方法;若都注册完成则直接调用添加节点后的回调方法,不同的ContextHandler对回调方法有不同的实现。

Pipeline节点删除

逻辑同节点添加。

Pipeline事件传播:

触发channel的read事件,read事件从ChannelPipeline的tail节点开始传播(write事件从head节点传播)。那ChannelHandlerContext会获取事件消息和EventExecutor,然后执行它的channelRead()方法。那么我们的自定义ChannelHandler要继承SimpleChannelInboundHandler,重写channelRead()方法。

ByteBuf:

blog.csdn.net/zs319428/ar…

ByteBuf通过维护读索引和写索引来控制字节数组,读索引只控制读访问,写索引只控制写访问,就不需要原生ByteBuffer的flip()读写模式转换。 image.png ByteBuf有三种模式:

①Heap Buffer模式,将数据放入JVM的堆内存中,可以提供数组的快速访问;但进行IO操作时就需要发生系统调用到socket缓冲区(Unpooled.buffer());

②Direct Buffer模式:使用堆外的直接内存,避免了数据从JVM堆内存拷贝到socket缓冲区,但JVM不会自动回收直接内存,所以需要手动释放空间(cleaner().clean())

③Composite Buffer复合缓冲区:多个ByteBuf组合起来的组合视图,避免了ByteBuf之间相互的数据拷贝 Netty应用层零拷贝 1.使用Composite Buffer复合缓冲区:

①Unpooled.wrappedBuffer.(ByteBuf...bufs / byte[] array):将byte数组包装到一个UnpooledHeapByteBuf对象,返回新的ByteBuf实例,具有自己的读写索引,但与原ByteBuf对象共用同一个内存空间。

②byteBuf.slice():将一个ByteBuf分解为多个共享同一存储区域的ByteBuf。 2.FileChannel.tranferTo():将文件缓冲区的数据直接发送到目标channel,避免了传统的循环write导致的内存拷贝 如果申请的ByteBuf忘记释放,则会导致Direct Buffer OOM。所以ByteBuf需要手动释放 堆外:freeDictBuffer(buffer)

堆内:recyclerHandler.recycle(this)

为什么要用Netty代替NIO

支持常用的应用层协议

解决了粘包、半包问题:Netty封装ByteBuf一个数据积累器暂存数据(ByteBuf追加数据并会自动扩容),进行数据编码时会判断数据积累器的长度和预先知道的消息长度大小。若满足条件才会对此长度进行编码,解决了粘包拆包问题。(此数据编解码是针对字节的操作,Netty还支持Protobuf二次编解码,针对字节-对象的转换)

支持流量整型

完善的断连、空闲等

规避了空轮询Bug

封装了ByteBuf,避免了无法扩展而重置指针。

  1. Netty中的锁:
  • 使用CountDownLatch代替wait/notify。后者需要监视器?
  • NioEventLoop是多生产者单消费者。对于taskQueue,LinkedBlockingQeueu效率低下。所以Netty使用MpscQueue。
  • Netty的pipeline处理是局部串行 + 整体并行 优于 一个队列 + 多个线程模式
  • 使用threadLocal
  1. Netty减少内存使用技巧( / 减少full gc)
  • 能用基本类型就不用包装类型
  • 类变量优于实例变量
  • 分配内存时预估
  • 根据接收的数据动态调整下一个要分配的buffer大小

心跳机制

IdelStateHandler

源码:blog.csdn.net/u013967175/…

向EventLoopExecutor对应的任务队列中调度定时任务,判断channel的读写是否空闲超时。若超时则会触发超时事件到ChannelPipeline中,执行自定义的userEventTriggered()方法,发送自定义心跳包维持连接。

断线重连:

源码:blog.csdn.net/u010889990/…

①我们可以设置客户端的写超时 小于 服务端的读超时,若客户端触发了userEventTriggered()写超时方法,则主动发送自定义心跳包,防止触发服务端的读超时(当然了在发送心跳包时要判断能否发送成功,若发送失败则直接关闭连接)

如果客户端没有发送自定义心跳包,则会触发服务端的读超时。那服务端再去比较客户端的读入时间差和服务端的写超时时间间。那如果读入时间差小,说明连接正常,客户端有可能在GC 有可能在大文件处理,没来得及发送心跳包。这种情况客户端读超时也不会关闭channel连接

②Netty中的channelInactive()方法是通过Socket连接关闭时挥手数据包触发的,因此可以通过channelInactive()方法感知正常的下线情况,但是因为网络异常等非正常下线则无法感知。所以我们还需要通过等待心跳机制的三倍心跳时间无心跳接收(防止服务端正在GC等),若有心跳了则进行client.reconnect()断线重连。

Netty调优

参数调优

  • 若打开太多文件句柄,则报too many open file。在linux中使用ulimit -n [xxx],命令。
  • SokcetChannel.childOption(TCP_NODELAY,true):开启Nagle算法
  • serverSocket.bind(addr,SO_BACKLOG):服务端最大的等待连接数
  • 共用:SO_ReuseAddr地址重用:若网卡绑定相同端口,则会Address already in use。这个参数可以让关闭连接释放的端口更早地使用。原理是缩短TCP四次挥手的timewait时间,让端口更早地完全关闭连接。

使用优化

  • @Sharable:标识Handler是否可以共享,若标记了则此Hnadler不能重复加入Pipeline。
  • 完善线程名:在EventLoopGroup的构造参数中传入new DefaultThreadFactory("xxx")
  • 完善Handler名:Handler构造参数可以传入名。

我为什么要写Hetty,我从Hetty中学到了什么?

Netty是一个代码质量极高的网络编程框架,而且被运用于各大框架中,我们有必要搞清楚它的底层原理。而且我们可以从他的代码中学会很多巧妙的代码设计,并运用到自己的项目中 如果要你来设计这些功能会怎么做?在弄清楚细节后,画流程图或类图,加上关键备注。 结合法:先弄明白最小单位组件的用途,然后再把它们拼接组合起来,掌握组件组合后的功能。然后再从一个大的功能点入手,逐步深入到各个底层组件的源码。

Starter

角色:透出给用户的Starter,初始化内存池、主从线程池、EventLoop、开启网络编程

             → abstract-NioStarter ->ServerClient

abstract-Starter
→ abstract-AioStarter ->Server、Client

Channel

整个消息传播的网络通道抽象,可以直接写入通道、也可走Pipeline写入通道,含有多个亮点

亮点1:Function函数式编程,基于一个输入值确定一个输出值(还是不太理解?) 亮点2:使用Map + ReadWriteLock代替ConcurrentHashMap,用于设置codec的属性值。因为在同一时间无数数据收发,就有无数codec在工作。

                   → NioChannel / AioChannel

abstract-SocketChannel → UdpChannel

EventLoop

角色:处理读写事件的事件循环(While),一个EventLoop只对应一个线程,这样就避免了EventLoop内的并发;Channel中使用Semaphore,保证了一个channel在某个时刻只能由一个线程处理。

收发消息底层实际上是两层缓存:网络通道 → ByteBuffer → ByteBuf(自己封装) -> 格式化 -> Pipeline

EventLoop接口 → NioEventLoop

Pipeline

ChannelBoundHandler接口 → In/OutboundHandler接口(顶级接口,Adapter为适配器模式) ↓ ChannelHandlerAdapter抽象类 → In/OutboundHandlerAdapter -> 各种Codec

Codec

进行byte与Object的转化。

In/OutboundHandlerAdapter → ObjectToMessageDecoder/...Encoder → StringDecoder/...Encoder

buffer

对byte[]进行index与offset的移动