Netty的服务启动类ServerBootstrap:Bootstrap类是Netty提供的一个便利的工厂类,可以通过它来完成Netty的客户端或服务器端的Netty组件的组装,以及Netty程序的初始化。它的职责是一个组装和集成器,将不同的Netty组件组装在一起。另外,ServerBootstrap能够按照应用场景的需要,为组件设置好对应的参数,最后实现Netty服务器的监听和启动。Netty中的各种组件:服务器启动器、缓冲区、反应器、Handler业务处理器、Future异步任务监听、数据传输通道。
Netty中不直接使用Java NIO的Channel通道组件,对Channel通道组件进行了自己的封装。在Netty中,有一系列的Channel通道组件,为了支持多种通信协议,对于每一种通信连接协议,Netty都实现了自己的通道。除了Java的NIO,Netty还能处理Java的面向流的OIO(Old-IO,即传统的阻塞式IO)。Netty中的每一种协议的通道,都有NIO(异步IO)和OIO(阻塞式IO)两个。Netty中常见的通道类型如下:
Netty中的反应器有多个实现类,与Channel通道类有关系。对应于NioSocketChannel通道,Netty的反应器类为:NioEventLoop。 NioEventLoop类绑定了两个重要的Java成员属性:一个是Thread线程类的成员,一个是Java NIO选择器的成员属性。一个NioEventLoop拥有一个Thread线程,负责一个Java NIO Selector选择器的IO事件轮询。理论上来说,一个EventLoopNetty反应器和NettyChannel通道是一对多的关系:一个反应器可以注册成千上万的通道。多个EventLoop线程组成一个EventLoopGroup线程。
Netty的Handler处理器分为两大类:第一类是ChannelInboundHandler通道入站处理器(入站指的是输入,对于处理入站的IO事件的方法,ChannelInboundHandlerAdapter则是Netty提供的入站处理的默认实现);第二类是ChannelOutboundHandler通道出站处理器(出站指的是输出)。
ChannelInitializer处理器有一个泛型参数SocketChannel,它代表需要初始化的通道类型,这个类型需要和前面的启动器中设置的通道类型,一一对应。
操作系统底层的socket描述符分为两类:
- 连接监听类型。连接监听类型的socket描述符,放在服务器端,它负责接收客户端的套接字连接;在服务器端,一个“连接监听类型”的socket描述符可以接受(Accept)成千上万的传输类的socket描述符。
- 传输数据类型。数据传输类的socket描述符负责传输数据。同一条TCP的Socket传输链路,在服务器和客户端,都分别会有一个与之相对应的数据传输类型的socket描述符。
在Netty中,将有接收关系的NioServerSocketChannel和NioSocketChannel,叫作父子通道。其中,NioServerSocketChannel负责服务器连接监听和接收,也叫父通道(Parent Channel)。对应于每一个接收到的NioSocketChannel传输类通道,也叫子通道(Child Channel)。
这个参数的值,与是否开启Nagle算法是相反的,设置为true表示关闭,设置为false表示开启。
Netty在对通道进行初始化的时候,将pipeline属性初始化为DefaultChannelPipeline的实例,每个通道拥有一条ChannelPipeline处理器流水线。
EmbeddedChannel仅仅是模拟入站与出站的操作,底层不进行实际的传输,不需要启动Netty服务器和客户端。而且Embedded-Channel的其他的事件机制和处理流程和真正的传输通道是一模一样。EmbeddedChannel单元测试的辅助方法中最为重要的两个方法为:writeInbound和readOutbound方法。
整个的IO处理操作环节包括:从通道读数据包、数据包解码、业务处理、目标数据编码、把数据包写到通道,然后由通道发送到对端。用户程序主要在Handler业务处理器中,Handler涉及的环节为:数据包解码、业务处理、目标数据编码、把数据包写到通道中。
当数据或者信息入站到Netty通道时,Netty将触发入站处理器ChannelInboundHandler所对应的入站API,进行入站操作操作。
在实际开发中,只需要继承这个ChannelInboundHandlerAdapter默认实现,重写自己需要的方法即可。
当业务处理完成后,需要操作Java NIO底层通道时,通过一系列的ChannelOutboundHandler通道出站处理器,完成Netty通道到底层通道的操作。比方说建立底层连接、断开底层连接、写入底层Java NIO通道等。
在Netty中,它的默认实现为ChannelOutboundHandlerAdapter,在实际开发中,只需要继承这个ChannelOutboundHandlerAdapter默认实现,重写自己需要的方法即可。
一条Netty通道需要很多的Handler业务处理器来处理业务。Netty设计了一个特殊的组件,叫作ChannelPipeline(通道流水线),它像一条管道,将绑定到一个通道的多个Handler处理器实例,串在一起,形成一条流水线。Netty的业务处理器流水线ChannelPipeline是基于责任链设计模式(Chain of Responsibility)来设计的,ChannelPipeline(通道流水线)的默认实现,实际上被设计成一个双向链表,能够支持动态地添加和删除Handler业务处理器。所有的Handler处理器实例被包装成了双向链表的节点,被加入到了ChannelPipeline(通道流水线)中。Netty是这样规定的:入站处理器Handler的执行次序,是从前到后;出站处理器Handler的执行次序,是从后到前。
为什么不需要装配父通道的流水线呢?
原因是:父通道也就是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会抛出异常。如果业务条件不满足,可以不启动出站处理。
与Java NIO的ByteBuffer相比,ByteBuf的优势如下:
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缓冲区,回收分为两种情况:
- 如果是堆(Heap)结构缓冲,会被JVM的垃圾回收机制回收;
- 如果是Direct类型,调用本地方法释放外部内存(unsafe.freeMemory)
Netty提供了ByteBufAllocator的两种实现:PoolByteBufAllocator和UnpooledByteBufAllocator。
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)浅层复制
2、整体(duplicate)浅层复制:
浅层复制方法不会实际去复制数据,也不会改变ByteBuf的引用计数,这就会导致一个问题:在源ByteBuf调用release()之后,一旦引用计数为零,就变得不能访问了;在这种场景下,源ByteBuf的所有浅层复制实例也不能进行读写了;如果强行对浅层复制实例进行读写,则会报错。 因此,在调用浅层复制实例时,可以通过调用一次retain()方法来增加引用,表示它们对应的底层内存多了一次引用,引用计数为2。在浅层复制实例用完后,需要调用两次release()方法,将引用计数减一,这样就不影响源ByteBuf的内存释放。
在Netty中,无论是出站操作,还是出站操作,都有两大的特点:
- (1)同一条通道的所有出/入站处理都是串行的,而不是并行的。换句话说,同一条通道上的所有出/入站处理都会在它所绑定的EventLoop线程上执行。既然只有一个线程负责,那就只有串行的可能。EventLoop线程的任务队列是一个MPSC队列(即多生产者单消费者队列)只有EventLoop线程自己是唯一的消费者,它将遍历任务队列,逐个执行任务;其他线程只能作为生产者,它们的出/入站操作都会作为异步任务加入到任务队列。通过MPSC队列,确保了EventLoop线程能做到:同一个通道上所有的IO操作是串行的,不是并行的。这样,不同的Handler业务处理器之间不需要进行线程的同步,这点也能大大提升IO的性能。
- (2)Netty的一个出/入站操作不是一次的单一Handler业务处理器操作,而是流水线上的一系列的出/入站处理流程。只有整个流程都处理完,出/入站操作才真正处理完成。
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()方法已经执行完毕。