Java并发编程学习-日记6、Netty基础知识点

Netty的服务启动类ServerBootstrap:Bootstrap类是Netty提供的一个便利的工厂类,可以通过它来完成Netty的客户端或服务器端的Netty组件的组装,以及Netty程序的初始化。它的职责是一个组装和集成器,将不同的Netty组件组装在一起。另外,ServerBootstrap能够按照应用场景的需要,为组件设置好对应的参数,最后实现Netty服务器的监听和启动。Netty中的各种组件:服务器启动器、缓冲区、反应器、Handler业务处理器、Future异步任务监听、数据传输通道。

1、Channel通道组件

Netty中不直接使用Java NIO的Channel通道组件,对Channel通道组件进行了自己的封装。在Netty中,有一系列的Channel通道组件,为了支持多种通信协议,对于每一种通信连接协议,Netty都实现了自己的通道。除了Java的NIO,Netty还能处理Java的面向流的OIO(Old-IO,即传统的阻塞式IO)。Netty中的每一种协议的通道,都有NIO(异步IO)和OIO(阻塞式IO)两个。Netty中常见的通道类型如下:

  • NioSocketChannel:异步非阻塞TCP Socket传输通道。
  • OioSocketChannel:同步阻塞式TCP Socket传输通道。
  • NioServerSocketChannel:异步非阻塞TCP Socket服务器端监听通道。在Netty的NioSocketChannel内部封装了一个Java NIO的SelectableChannel成员。通过这个内部的Java NIO通道,Netty的NioSocketChannel通道上的IO操作,最终会落地到Java NIO的SelectableChannel底层通道。
  • OioServerSocketChannel:同步阻塞式TCP Socket服务器端监听通道。
  • NioDatagramChannel:异步非阻塞的UDP传输通道。
  • OioDatagramChannel:同步阻塞式的UDP传输通道。
  • NioSctpChannel:异步非阻塞Sctp传输通道。
  • OioSctpChannel:同步阻塞式Sctp传输通道。
  • NioSctpServerChannel:异步非阻塞Sctp服务器端监听通道。
  • OioSctpServerChannel:同步阻塞式Sctp服务器端监听通道。

2、Netty中的反应器

Netty中的反应器有多个实现类,与Channel通道类有关系。对应于NioSocketChannel通道,Netty的反应器类为:NioEventLoop。 NioEventLoop类绑定了两个重要的Java成员属性:一个是Thread线程类的成员,一个是Java NIO选择器的成员属性。一个NioEventLoop拥有一个Thread线程,负责一个Java NIO Selector选择器的IO事件轮询。理论上来说,一个EventLoopNetty反应器和NettyChannel通道是一对多的关系:一个反应器可以注册成千上万的通道。多个EventLoop线程组成一个EventLoopGroup线程。

3、Netty的Handler处理器

Netty的Handler处理器分为两大类:第一类是ChannelInboundHandler通道入站处理器(入站指的是输入,对于处理入站的IO事件的方法,ChannelInboundHandlerAdapter则是Netty提供的入站处理的默认实现);第二类是ChannelOutboundHandler通道出站处理器(出站指的是输出)。

  • Netty中的入站处理,触发的方向为:从通道到ChannelInboundHandler通道入站处理器。
  • Netty中的出站处理指的是从ChanneOutboundHandler通道出站处理器到通道的某次IO操作,例如,在应用程序完成业务处理后,可以通过ChanneOutboundHandler通道出站处理器将处理的结果写入底层通道。它的最常用的一个方法就是write()方法,把数据写入到通道。

(1)反应器(或者SubReactor子反应器)和通道之间是一对多的(2)通道和Handler处理器实例之间,是多对多的。

ChannelInitializer处理器有一个泛型参数SocketChannel,它代表需要初始化的通道类型,这个类型需要和前面的启动器中设置的通道类型,一一对应。

操作系统底层的socket描述符分为两类:

  • 连接监听类型。连接监听类型的socket描述符,放在服务器端,它负责接收客户端的套接字连接;在服务器端,一个“连接监听类型”的socket描述符可以接受(Accept)成千上万的传输类的socket描述符。
  • 传输数据类型。数据传输类的socket描述符负责传输数据。同一条TCP的Socket传输链路,在服务器和客户端,都分别会有一个与之相对应的数据传输类型的socket描述符。

在Netty中,将有接收关系的NioServerSocketChannel和NioSocketChannel,叫作父子通道。其中,NioServerSocketChannel负责服务器连接监听和接收,也叫父通道(Parent Channel)。对应于每一个接收到的NioSocketChannel传输类通道,也叫子通道(Child Channel)。

ChannelOption通道选项:

  • 1)SO_RCVBUF,SO_SNDBUF 此为TCP参数。每个TCP socket(套接字)在内核中都有一个发送缓冲区和一个接收缓冲区,这两个选项就是用来设置TCP连接的这两个缓冲区大小的。TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的缓冲区及其填充的状态。
  • 2)TCP_NODELAY 此为TCP参数。表示立即发送数据,默认值为True(Netty默认为True,而操作系统默认为False)。该值用于设置Nagle算法的启用,该算法将小的碎片数据连接成更大的报文(或数据包)来最小化所发送报文的数量,如果需要发送一些较小的报文,则需要禁用该算法。

这个参数的值,与是否开启Nagle算法是相反的,设置为true表示关闭,设置为false表示开启。

  • 3)SO_KEEPALIVE 此为TCP参数。表示底层TCP协议的心跳机制。true为连接保持心跳,默认值为false。启用该功能时,TCP会主动探测空闲连接的有效性。可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。
  • 4)SO_REUSEADDR 此为TCP参数。设置为true时表示地址复用,默认值为false。
  • 5)SO_LINGER 此为TCP参数。表示关闭socket的延迟时间,默认值为-1,表示禁用该功能。-1表示socket.close()方法立即返回,但操作系统底层会将发送缓冲区全部发送到对端。0表示socket.close()方法立即返回,操作系统放弃发送缓冲区的数据,直接向对端发送RST包,对端收到复位错误。非0整数值表示调用socket.close()方法的线程被阻塞,直到延迟时间到来、发送缓冲区中的数据发送完毕,若超时,则对端会收到复位错误。
  • 6)SO_BACKLOG 此为TCP参数。表示服务器端接收连接的队列长度,如果队列已满,客户端连接将被拒绝。默认值,在Windows中为200,其他操作系统为128。
  • 7)SO_BROADCAST 此为TCP参数。表示设置广播模式。

Netty在对通道进行初始化的时候,将pipeline属性初始化为DefaultChannelPipeline的实例,每个通道拥有一条ChannelPipeline处理器流水线。

通道接口的几个重要方法:

  • 方法1、ChannelFuture connect(SocketAddress address) 此方法的作用为:连接远程服务器。方法的参数为远程服务器的地址,调用后会立即返回,返回值为负责连接操作的异步任务ChannelFuture。此方法在客户端的传输通道使用。
  • 方法2、ChannelFuture bind(SocketAddress address) 此方法的作用为:绑定监听地址,开始监听新的客户端连接。此方法在服务器的新连接监听和接收通道使用。
  • 方法3、ChannelFuture close() 此方法的作用为:关闭通道连接,返回连接关闭的ChannelFuture异步任务。如果需要在连接正式关闭后执行其他操作,则需要为异步任务设置回调方法;或者调用ChannelFuture异步任务的sync( )方法来阻塞当前线程,一直等到通道关闭的异步任务执行完毕。
  • 方法4、Channel read() 此方法的作用为:读取通道数据,并且启动入站处理。具体来说,从内部的Java NIO Channel通道读取数据,然后启动内部的Pipeline流水线,开启数据读取的入站处理。此方法的返回通道自身用于链式调用。
  • 方法5、ChannelFuture write(Object o) 此方法的作用为:向通道写数据,启程出站流水处理,把处理后的最终数据写到底层Java NIO通道。此方法的返回值为出站处理的异步处理任务。
  • 方法6、Channel flush() 此方法的作用为:将缓冲区中的数据立即写出到对端。并不是每一次write操作都是将数据直接写出到对端,write操作的作用在大部分情况下仅仅是写入到操作系统的缓冲区,操作系统会将根据缓冲区的情况,决定什么时候把数据写到对端。而执行flush()方法立即将缓冲区的数据写到对端。

EmbeddedChannel

EmbeddedChannel仅仅是模拟入站与出站的操作,底层不进行实际的传输,不需要启动Netty服务器和客户端。而且Embedded-Channel的其他的事件机制和处理流程和真正的传输通道是一模一样。EmbeddedChannel单元测试的辅助方法中最为重要的两个方法为:writeInbound和readOutbound方法。

整个的IO处理操作环节包括:从通道读数据包、数据包解码、业务处理、目标数据编码、把数据包写到通道,然后由通道发送到对端。用户程序主要在Handler业务处理器中,Handler涉及的环节为:数据包解码、业务处理、目标数据编码、把数据包写到通道中。

  • 入站处理,触发的方向为:自底向上,Netty的内部(如通道)到ChannelInboundHandler入站处理器。数据包解码、业务处理两个环节——属于入站处理器的工作。
  • 出站处理,触发的方向为:自顶向下,从ChannelOutboundHandler出站处理器到Netty的内部(如通道)。目标数据编码、把数据包写到通道中两个环节——属于出站处理器的工作。

当数据或者信息入站到Netty通道时,Netty将触发入站处理器ChannelInboundHandler所对应的入站API,进行入站操作操作。

  • 1)channelRegistered 当通道注册完成后,Netty会调用fireChannelRegistered,触发通道注册事件。通道会启动该入站操作的流水线处理,在通道注册过的入站处理器Handler的channelRegistered方法,会被调用到。
  • 2)channelActive 当通道激活完成后,Netty会调用fireChannelActive,触发通道激活事件。通道会启动该入站操作的流水线处理,在通道注册过的入站处理器Handler的channelActive方法,会被调用到。
  • 3)channelRead 当通道缓冲区可读,Netty会调用fireChannelRead,触发通道可读事件。通道会启动该入站操作的流水线处理,在通道注册过的入站处理器Handler的channelRead方法,会被调用到。
  • 4)channelReadComplete 当通道缓冲区读完,Netty会调用fireChannelReadComplete,触发通道读完事件。通道会启动该入站操作的流水线处理,在通道注册过的入站处理器Handler的channelReadComplete方法,会被调用到。
  • 5)channelInactive 当连接被断开或者不可用,Netty会调用fireChannelInactive,触发连接不可用事件。通道会启动对应的流水线处理,在通道注册过的入站处理器Handler的channelInactive方法,会被调用到。
  • 6)exceptionCaught 当通道处理过程发生异常时,Netty会调用fireExceptionCaught,触发异常捕获事件。通道会启动异常捕获的流水线处理,在通道注册过的处理器Handler的exceptionCaught方法,会被调用到。注意,这个方法是在通道处理器中ChannelHandler定义的方法,入站处理器、出站处理器接口都继承到了该方法。

在实际开发中,只需要继承这个ChannelInboundHandlerAdapter默认实现,重写自己需要的方法即可。

当业务处理完成后,需要操作Java NIO底层通道时,通过一系列的ChannelOutboundHandler通道出站处理器,完成Netty通道到底层通道的操作。比方说建立底层连接、断开底层连接、写入底层Java NIO通道等。

  • 1)bind 监听地址(IP+端口)绑定:完成底层Java IO通道的IP地址绑定。如果使用TCP传输协议,这个方法用于服务器端。
  • 2)connect 连接服务端:完成底层Java IO通道的服务器端的连接操作。如果使用TCP传输协议,这个方法用于客户端。
  • 3)write 写数据到底层:完成Netty通道向底层Java IO通道的数据写入操作。此方法仅仅是触发一下操作而已,并不是完成实际的数据写入操作。
  • 4)flush 腾空缓冲区中的数据,把这些数据写到对端:将底层缓存区的数据腾空,立即写出到对端。
  • 5)read 从底层读数据:完成Netty通道从Java IO通道的数据读取。
  • 6)disConnect 断开服务器连接:断开底层Java IO通道的服务器端连接。如果使用TCP传输协议,此方法主要用于客户端。
  • 7)close 主动关闭通道:关闭底层的通道,例如服务器端的新连接监听通道。

在Netty中,它的默认实现为ChannelOutboundHandlerAdapter,在实际开发中,只需要继承这个ChannelOutboundHandlerAdapter默认实现,重写自己需要的方法即可。

ChannelPipeline(通道流水线)

一条Netty通道需要很多的Handler业务处理器来处理业务。Netty设计了一个特殊的组件,叫作ChannelPipeline(通道流水线),它像一条管道,将绑定到一个通道的多个Handler处理器实例,串在一起,形成一条流水线。Netty的业务处理器流水线ChannelPipeline是基于责任链设计模式(Chain of Responsibility)来设计的,ChannelPipeline(通道流水线)的默认实现,实际上被设计成一个双向链表,能够支持动态地添加和删除Handler业务处理器。所有的Handler处理器实例被包装成了双向链表的节点,被加入到了ChannelPipeline(通道流水线)中。Netty是这样规定的:入站处理器Handler的执行次序,是从前到后;出站处理器Handler的执行次序,是从后到前。

  • 每一个通道的子通道,都用一条ChannelPipeline流水线。它的内部有一个双向的链表。装配流水线的方式是:将业务处理器ChannelHandler实例加入双向链中。
  • 装配子通道的Handler流水线调用childHandler()方法,传递一个ChannelInitializer通道初始化类的实例。在父通道成功接收一个连接,并创建成功一个子通道后,就会初始化子通道。

为什么不需要装配父通道的流水线呢?

原因是:父通道也就是NioServerSocketChannel连接接受通道,它的内部业务处理是固定的:接受新连接后,创建子通道,然后初始化子通道,所以不需要特别的配置。如果需要完成特殊的业务处理,可以使用ServerBootstrap的handler(ChannelHandler handler)方法,为父通道设置ChannelInitializer初始化器。

在Handler业务处理器被添加到流水线中时,会创建一个通道处理器上下文ChannelHandlerContext,它代表了ChannelHandler通道处理器和ChannelPipeline通道流水线之间的关联。ChannelHandlerContext中包含了有许多方法,主要可以分为两类:第一类是获取上下文所关联的Netty组件实例,如所关联的通道、所关联的流水线、上下文内部Handler业务处理器实例等;第二类是入站和出站处理方法。

在Channel、ChannelPipeline、ChannelHandlerContext三个类中,会有同样的出站和入站处理方法,如果通过Channel或Channel-Pipeline 的实例来调用这些方法,它们就会在整条流水线中传播。然而,如果是通过ChannelHandlerContext通道处理器上下文进行调用,就只会从当前的节点开始执行Handler业务处理器,并传播到同类型处理器的下一站(节点)。

Channel、Handler、ChannelHandlerContext三者的关系为:Channel通道拥有一条ChannelPipeline通道流水线,每一个流水线节点为一个ChannelHandlerContext通道处理器上下文对象,每一个上下文中包裹了一个ChannelHandler通道处理器。在ChannelHandler通道处理器的入站/出站处理方法中,Netty都会传递一个Context上下文实例作为实际参数。通过Context实例的实参,在业务处理中,可以获取ChannelPipeline通道流水线的实例或者Channel通道的实例。

如何截断入站处理流程?

在channelRead方法中,不再调用父类的channelRead入站方法。在channelRead方法中,入站处理传入下一站还有一种方法:调用Context上下文的ctx.fireChannelRead(msg)方法。如果要截断流水线的处理,很显然,就不能调用ctx.fireChannelRead(msg)方法。如果要截断其他的入站处理的流水线操作(使用Xxx指代),也可以同样处理: (1)不调用supper.channelXxx (ChannelHandler-Context …)  (2)也不调用ctx.fireChannelXxx()。

出站处理流程只要开始执行,就不能被截断。】强行截断的话,Netty会抛出异常。如果业务条件不满足,可以不启动出站处理。

4、Netty buffer:

与Java NIO的ByteBuffer相比,ByteBuf的优势如下:

  • Pooling(池化,这点减少了内存复制和GC,提升了效率)
  • 复合缓冲区类型,支持零复制
  • 不需要调用flip()方法去切换读/写模式
  • 扩展性好,例如StringBuffer ·可以自定义缓冲区类型
  • 读取和写入索引分开
  • 方法的链式调用
  • 可以进行引用计数,方便重复使用

ByteBuf通过三个整型的属性有效地区分可读数据和可写数据,使得读写之间相互没有冲突。这三个属性定义在AbstractByteBuf抽象类中,分别是: ·readerIndex(读指针) ·writerIndex(写指针) ·maxCapacity(最大容量)Netty的ByteBuf的内存回收工作是通过【引用计数】的方式管理的。JVM中使用“计数器”(一种GC算法)来标记对象是否“不可达”进而收回(注:GC是Garbage Collection的缩写,即Java中的垃圾回收机制),Netty也使用了这种手段来对ByteBuf的引用进行计数。Netty采用“计数器”来追踪ByteBuf的生命周期,一是对Pooled ByteBuf的支持,二是能够尽快地“发现”那些可以回收的ByteBuf(非Pooled),以便提升ByteBuf的分配和销毁的效率。

默认情况下,当创建完一个ByteBuf时,它的引用为1;每次调用retain()方法,它的引用就加1;每次调用release()方法,就是将引用计数减1;如果引用为0,再次访问这个ByteBuf对象,将会抛出异常;如果引用为0,表示这个ByteBuf没有哪个进程引用它,它占用的内存需要回收。在Netty中,引用计数为0的缓冲区不能再继续使用。为了确保引用计数不会混乱,在Netty的业务处理器开发过程中,应该坚持一个原则:retain和release方法应该结对使用。简单地说,在一个方法中,调用了retain,就应该调用一次release。

如果retain和release这两个方法,一次都不调用呢?

在缓冲区使用完成后,调用一次release,就是释放一次。例如在Netty流水线上,中间所有的Handler业务处理器处理完ByteBuf之后直接传递给下一个,由最后一个Handler负责调用release来释放缓冲区的内存空间。

当引用计数已经为0,Netty会进行ByteBuf的回收。分为两种情况:

  • Pooled池化的ByteBuf内存,回收方法是:放入可以重新分配的ByteBuf池子,等待下一次分配。
  • Unpooled未池化的ByteBuf缓冲区,回收分为两种情况:
  1. 如果是堆(Heap)结构缓冲,会被JVM的垃圾回收机制回收;
  2. 如果是Direct类型,调用本地方法释放外部内存(unsafe.freeMemory)

Netty提供了ByteBufAllocator的两种实现:PoolByteBufAllocator和UnpooledByteBufAllocator。

  • PoolByteBufAllocator(池化ByteBuf分配器)将ByteBuf实例放入池中,提高了性能,将内存碎片减少到最小;这个池化分配器采用了jemalloc高效内存分配的策略。
  • UnpooledByteBufAllocator是普通的未池化ByteBuf分配器,它没有把ByteBuf放入池中,每次被调用时,返回一个新的ByteBuf实例;通过Java的垃圾回收机制回收。
  • 在Netty中,默认的分配器为ByteBufAllocator.DEFAULT,可以通过Java系统参数(System Property)的选项io.netty.allocator.type进行配置,配置时使用字符串值:"unpooled","pooled"。

ByteBuffer缓冲区类型:根据内存的管理方不同,分为堆缓存区和直接缓存区,也就是Heap ByteBuf和Direct ByteBuf。

Direct Memory特殊说明:

  • Direct Memory不属于Java堆内存,所分配的内存其实是调用操作系统malloc()函数来获得的;由Netty的本地内存堆Native堆进行管理。
  • Direct Memory容量可通过-XX:MaxDirectMemorySize来指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)。
  • Direct Memory的使用避免了Java堆和Native堆之间来回复制。
  • 在需要频繁创建缓冲区的场合,由于创建和销毁Direct Buffer(直接缓冲区)的代价比较高昂,因此不宜使用Direct Buffer。
  • 对Direct Buffer的读写比Heap Buffer快,但是它的创建和销毁比普通Heap Buffer慢。
  • 在Java的垃圾回收机制回收Java堆时,Netty框架也会释放不再使用的Direct Buffer缓冲区,因为它的内存为堆外内存,所以清理的工作不会为Java虚拟机(JVM)带来压力。
  • 垃圾回收的应用场景:

(1)垃圾回收仅在Java堆被填满,以至于无法为新的堆分配请求提供服务时发生;

(2)在Java应用程序中调用System.gc()函数来释放内存。

Heap ByteBuf和Direct ByteBuf两类缓冲区的使用。它们有以下几点不同:

  • 创建的方法不同:Heap ByteBuf通过调用分配器的buffer()方法来创建;而Direct ByteBuf的创建,是通过调用分配器的directBuffer()方法。
  • Heap ByteBuf缓冲区可以直接通过array()方法读取内部数组;而Direct ByteBuf缓冲区不能读取内部数组。
  • 可以调用hasArray()方法来判断是否为Heap ByteBuf类型的缓冲区;如果hasArray()返回值为true,则表示是Heap堆缓冲,否则就不是。
  • Direct ByteBuf要读取缓冲数据进行业务处理,相对比较麻烦,需要通过getBytes/readBytes等方法先将数据复制到Java的堆内存,然后进行其他的计算。

Netty的Reactor反应器线程会在底层的Java NIO通道读数据时,也就是AbstractNioByteChannel.NioByteUnsafe.read()处,调用ByteBufAllocator方法,创建ByteBuf实例,从操作系统缓冲区把数据读取到Bytebuf实例中,然后调用pipeline.fireChannelRead(byteBuf)方法将读取到的数据包送入到入站处理流水线中。

入站ByteBuf自动释放的方法:

  • TailHandler自动释放

Netty默认会在ChannelPipline通道流水线的最后添加一个TailHandler末尾处理器,它实现了默认的处理方法,在这些方法中会帮助完成ByteBuf内存释放的工作。如果自定义的InboundHandler入站处理器继承自ChannelInboundHandlerAdapter适配器,那么可以调用以下两种方法来释放ByteBuf内存:

(1)手动释放ByteBuf。具体的方式为调用byteBuf.release()。

(2)调用父类的入站方法将msg向后传递,依赖后面的处理器释放ByteBuf。具体的方式为调用基类的入站处理方法super.channelRead(ctx,msg)。

  • SimpleChannelInboundHandler自动释放

如果Handler业务处理器需要截断流水线的处理流程,不将ByteBuf数据包送入后边的InboundHandler入站处理器,这时,流水线末端的TailHandler末尾处理器自动释放缓冲区的工作自然就失效了。 在这种场景下,Handler业务处理器有两种选择:

(1)手动释放ByteBuf实例。

(2)继承SimpleChannelInboundHandler,利用它的自动释放功能。

ByteBuf的浅层复制分为两种,有切片(slice)浅层复制和整体(duplicate)浅层复制。

1、切片(slice)浅层复制

  • ByteBuf的slice方法可以获取到一个ByteBuf的一个切片。一个ByteBuf可以进行多次的切片浅层复制;多次切片后的ByteBuf对象可以共享一个存储区域。
  • 调用slice()方法后,返回的切片是一个新的ByteBuf对象,该对象的几个重要属性值,大致如下:
    1. ·readerIndex(读指针)的值为0。
    2. ·writerIndex(写指针)的值为源Bytebuf的readableBytes()可读字节数。
    3. ·maxCapacity(最大容量)的值为源Bytebuf的readableBytes( )可读字节数。
  • 切片后的新Bytebuf有两个特点:
    1. ·切片不可以写入,原因是:maxCapacity与writerIndex值相同。
    2. ·切片和源ByteBuf的可读字节数相同,原因是:切片后的可读字节数为自己的属性writerIndex – readerIndex,也就是源ByteBuf的readableBytes() - 0。
  • 切片后的新ByteBuf和源ByteBuf的关联性:
    1. ·切片不会复制源ByteBuf的底层数据,底层数组和源ByteBuf的底层数组是同一个。
    2. ·切片不会改变源ByteBuf的引用计数。

2、整体(duplicate)浅层复制:

  • duplicate()返回的是源ByteBuf的整个对象的一个浅层复制,包括如下内容:
  1. ·duplicate的读写指针、最大容量值,与源ByteBuf的读写指针相同。
  2. ·duplicate()不会改变源ByteBuf的引用计数。
  3. ·duplicate()不会复制源ByteBuf的底层数据。

浅层复制方法不会实际去复制数据,也不会改变ByteBuf的引用计数,这就会导致一个问题:在源ByteBuf调用release()之后,一旦引用计数为零,就变得不能访问了;在这种场景下,源ByteBuf的所有浅层复制实例也不能进行读写了;如果强行对浅层复制实例进行读写,则会报错。 因此,在调用浅层复制实例时,可以通过调用一次retain()方法来增加引用,表示它们对应的底层内存多了一次引用,引用计数为2。在浅层复制实例用完后,需要调用两次release()方法,将引用计数减一,这样就不影响源ByteBuf的内存释放。

在Netty中,无论是出站操作,还是出站操作,都有两大的特点:

  • (1)同一条通道的所有出/入站处理都是串行的,而不是并行的。换句话说,同一条通道上的所有出/入站处理都会在它所绑定的EventLoop线程上执行。既然只有一个线程负责,那就只有串行的可能。EventLoop线程的任务队列是一个MPSC队列(即多生产者单消费者队列)只有EventLoop线程自己是唯一的消费者,它将遍历任务队列,逐个执行任务;其他线程只能作为生产者,它们的出/入站操作都会作为异步任务加入到任务队列。通过MPSC队列,确保了EventLoop线程能做到:同一个通道上所有的IO操作是串行的,不是并行的。这样,不同的Handler业务处理器之间不需要进行线程的同步,这点也能大大提升IO的性能。
  • (2)Netty的一个出/入站操作不是一次的单一Handler业务处理器操作,而是流水线上的一系列的出/入站处理流程。只有整个流程都处理完,出/入站操作才真正处理完成。

5、NETTY Future

Netty继承和扩展了JDK Future系列异步回调的API,定义了自身的Future系列接口和类,实现了异步任务的监控、异步执行结果的获取。Netty对JavaFuture异步任务的扩展如下:

(1)继承Java的Future接口,得到了一个新的属于Netty自己的Future异步任务接口;该接口对原有的接口进行了增强,使得Netty异步任务能够以非阻塞的方式处理回调的结果。

(2)引入了一个新接口——GenericFutureListener,用于表示异步执行完成的监听器。这个接口和Guava的FutureCallbak回调接口不同。Netty使用了监听器的模式,异步任务的执行完成后的回调逻辑抽象成了Listener监听器接口。可以将Netty的GenericFutureListener监听器接口加入Netty异步任务Future中,实现对异步任务执行状态的事件监听。

GenericFutureListener拥有一个回调方法:operationComplete,表示异步任务操作完成。在Future异步任务执行完成后,将回调此方法。在大多数情况下,Netty的异步回调的代码编写在GenericFutureListener接口的实现类中的operationComplete方法中。GenericFutureListener的父接口EventListener是一个空接口,没有任何的抽象方法,是一个仅仅具有标识作用的;GenericFutureListener接口在Netty中是一个基础类型接口。在网络编程的异步回调中,一般使用Netty中提供的某个子接口,如ChannelFutureListener。

Netty的Future接口一般不会直接使用,而是会使用子接口。Netty有一系列的子接口,代表不同类型的异步任务,如ChannelFuture接口。在Netty的网络编程中,网络连接通道的输入和输出处理都是异步进行的,都会返回一个ChannelFuture接口的实例。

如何判断writeAndFlush()执行完毕?

writeAndFlush()方法会返回一个ChannelFuture异步任务实例,通过为ChannelFuture异步任务增加GenericFutureListener监听器的方式来判断writeAndFlush()是否已经执行完毕。当GenericFutureListener监听器的operationComplete方法被回调时,表示writeAndFlush()方法已经执行完毕。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章