Netty继承和扩展了JDK Future系列异步回调的API,定义了自身的Futrue系列接口和类,实现了异步任务的监控、异步执行结果的获取。总体来说Netty对Java Future异步任务的扩展如下:
public interface GenericFutureListener<F extends Future<?>> extends EventListener {
// 监听器的回调方法
void operationComplete(F var1) throws Exception;
}
GenericFutureListener拥有一个回调方法operationComplete,表示异步任务操作完成。在Future异步任务执行完成后将回调执行此方法。在大多数情况下,Netty的异步回调代码编写在GenericFutureListener接口的实现类中的operationComplete方法中。
它的父接口EventListener是一个空接口,没有任何的抽象方法,是一个仅仅具有标识作用的接口。
Netty也定义了自己的Future接口,对JDK原有的Future接口进行了扩展:
public interface Future<V> extends java.util.concurrent.Future<V> {
boolean isSuccess(); // 判断异步执行是否成功
boolean isCancellable(); // 判断异步执行是否取消
Throwable cause(); // 获取异步任务异常的原因
// 增加异步任务执行完成与否的监听器
Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);
// 溢出异步任务执行完成与否的监听器
Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);
}
Netty的Future接口一般不会直接使用,而是会使用子接口。Netty有一系列的子接口,代表不同类型的异步任务,如ChannelFuture接口。
在Netty的网络编程中,网络连接通道的输入和输出处理都是异步进行的,都会返回一个ChannelFuture接口的实例。通过返回的异步任务实例,可以为它增加异步回调的监听器。在异步任务真正完成后,回调才会执行。
// connect是异步的,仅提交异步任务
ChannelFuture future = bootstrap.conncect(new InetSocketAddress("www.wanning.com", 80));
// connect的异步任务真正执行完成后,future回调监听器才会执行
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if(channelFuture.isSuccess()) {
...
}
}
});
GenericFutureListener接口在Netty中是一个基础类型接口。在网络编程的异步回调中,一般使用Netty中提供的某个接口,如ChannelFutureListener接口。
Channel通道主键是Netty中非常重要的主键,反应器模式和通道紧密相关,反应器的查询和分发的IO事件都来自于Channel通道组件。
Netty中不直接使用Java NIO的Channel通道组件,对Channel通道组件进行了自己的封装。在Netty中,有一系列的Channel通道组件,为了支持多种通信协议,或者说对于每一种通信连接协议,Netty都实现了自己的通道。
另外一点就是除了Java的NIO,Netty还能处理Java的面向流的OIO。总的来说Netty中每一种协议的通道,都有NIO和OIO两个版本。
一般来说,服务器端编程用到最多的通信协议还是TCP协议,对应的传输通道类型是NioSocketChannel,服务器监听类为NioServerSocketChannel。在主要使用的方法上,其他的通道类型和这个NioSocketChannel类在原理上基本是相通的。在Netty的NioSocketChannel内部封装了一个Java NIO的SelectableChannel成员,通过这个内部的Java NIO通道,Netty的NioSocketChannel通道上的IO操作,最终会落地到Java NIO的SelectableChanne底层通道。
在反应器模式中,一个反应器会负责一个事件处理线程,不断地轮询,通过Selector选择器不断查询注册过的IO事件(选择键)。如果查询到IO事件,则分发给Handler业务处理器。
Netty中的反应器有多个实现类,与Channel通道类有关系。对应于NioSocketChannel通道,Netty的反应器类为NioEventLoop。NioEventLoop类绑定了两个重要的Java成员属性:一个是Thread线程类的成员,一个是Java NIO选择器的成员属性。
也就是说一个NioEventLoop拥有一个Thread线程,负责一个Java NIO Selector选择器的IO事件轮询。在Netty中,一个EventLoop反应器和Channel通道是一对多的关系,一个反应器可以注册成千上万的通道。
Java NIO可供选择器监控的通道IO事件类型包括以下4种:
在Netty中,EventLoop反应器内部有一个Java NIO选择器成员执行以上事件的查询,然后进行对应的事件分发。事件分发的目标就是Netty自己的Handler处理器。
**Netty的Handler处理器分为两大类,第一类是ChannelInboundHandler通道入站处理器,第二类ChannelOutboundHandler通道出站处理器。二者都继承了ChannelHandler处理器接口。**Netty中的入站处理,不仅仅是OP_READ输入事件的处理,还是从通道底层触发,由Netty通过层层传递,调用ChannelInboundHandler通道入站处理器进行的某个处理。以底层的Java NIO中的OP_READ输入事件为例:在通道中发生了OP_READ事件后会被EventLoop查询到,然后分发给ChannelInboundHandler通道入站处理器,调用它的入站处理的方法read。在ChannelInboundHandler通道入站处理器内部的read方法可以从通道中读取数据。
Netty中的入站处理,触发的方向为:从通道到ChannelInboundHandler通道入站处理器。
Netty中的出站处理,本来就包括Java NIO的OP_WRITE可写事件。不过OP_WRITE可写事件是Java NIO的底层概念,它和Netty的出站处理的概念不是一个维度的,Netty的出站处理是应用层维度的,具体指的是从ChannelOutboundHandler通道出站处理器到通道的某次IO操作。在应用程序完成业务处理后,可以通过ChannelOutboundHandler通道出站处理器将处理的结果写入底层通道,它的最常用的一个方法就是write()方法,把数据写入到通道。
这两个业务处理接口都有各自的默认实现:ChannelInboundHandler的默认实现为ChannelInboundHandlerApater,叫作通道入站处理适配器。ChannelOutboundHandler的默认实现ChannelOutboundHandlerAdapter,叫作通道出站处理适配器。这两个默认的通道处理器适配器,分别实现了入站操作和出站的基本功能。如果要实现自己的业务处理器,不需要从零开始去实现处理器的接口,只需要继承通道处理器适配器即可。
首先梳理一下Netty反应器模式中各个组件之间的关系:
问题是通道和Handler处理器实例之间的绑定关系,Netty是如何组织的呢?
Netty设计了一个特殊的组件,叫做ChannelPipeline(通道流水线),它像一条管道,将绑定到一个通道的多个Handler处理器实例,串在一起形成一条流水线。ChannelPipeline的默认实现实际上被设计成一个双向链表。所有的Handler处理器实例被包装成了双向链表的节点,被加入到了ChannelPipeline中。也就是说一个Netty通道拥有一条Handler处理器流水线,成员的名称叫作pipeline。
以入站处理为例,每一个来自通道的IO事件,都会进入一次ChannelPipeline通道流水线,在进入第一个Handler处理器后,这个IO事件将按照既定的从前往后次序,在流水线上不断地向后流动,流向一个Handler处理器。在向后流动的过程中,会出现3种情况:
总之流水线是通道的大管家,为通道管理好了它的一大堆Handler。
Bootstrap类是Netty提供的一个便利的工厂类,可以通过它来完成Netty的客户端或服务器端的Netty组件的组装以及Netty程序的初始化。当然我们也可以不用这个Bootstrap启动器,但是一点点去手动创建通道、完成各种设置和启动、并且注册到EventLoop,这个过程会非常麻烦。
在Netty中有两个启动器类,分别用在服务器和客户端,这两个启动器仅仅是使用的地方不同,它们大致的配置和使用方法都是相同的。
在Netty中每一个NioSocketChannel通道所封装的是Java NIO通道,再往下就对应到操作系统底层的socket描述符。理论上来说,操作系统底层的socket描述符分为两类:连接监听类型和传输数据类型。在Netty中,异步非阻塞的服务器端监听通道NioServerSocketChannel,封装在Linux底层的描述符,是连接监听类型的socket描述符。而NioSocketChannel异步非阻塞TCP Socket传输通道,封装在底层Linux的描述符,是数据传输类型的socket描述符。
在Netty中,NioServerSocketChannel负责服务器连接监听和接收,也叫父通道。对于每一个接收到的NioSocketChannel传输类通道,也叫子通道。
Netty的Reactor反应器模式是多线程版本的反应器模式,在Netty中,一个EventLoop相当于一个子反应器,一个NioEventLoop子反应器拥有了一个线程,同时拥有一个Java NIO选择器。多个EventLoop线程组成一个EventLoopGroup线程组。
反过来说,Netty的EventLoopGroup线程组就是一个多线程版本的反应器。Netty的程序开发不会直接使用单个EventLoop线程,而是使用EventLoopGroup线程组。EventLoopGroup的构造函数有一个参数,用于指定内部的线程数。在构造器初始化时,会按照传入的线程数量,在内部构造多个Thread线程和多个EventLoop子反应器,进行多线程的IO时间查询和分发。如果使用无参构造函数或传入线程数为0,那么默认EventLoopGroup内部线程数为最大可用CPU处理器数量的两倍。
在服务器端一般有两个独立的反应器,一个负责新连接的监听和接收,一个负责IO事件处理。对应到Netty服务器程序中则是设置两个EventLoopGroup线程组。负责新连接的监听和接受的线程组查询父通道的IO事件,称为Boss线程组。另一个线程组负责查询所有子通道的IO事件,并且执行Handler处理器中的业务处理,例如数据的输入和输出,称为Worker线程组。
创建反应器线程组,并赋值给ServerBootstrap启动器实例
设置通道的IO类型
设置监听端口:bootstrap.localAddress(new InetSocketAddress(port))
设置传输通道的配置选项:bootstrap.option用于给父通道接收连接通道设置一些选项,bootstrap.childOption设置子通道设置一些通道选项
装配子通道的Pipeline流水线:装配子通道的Handler流水线调用childHandler()方法,传递一个ChannelInitializer通道初始化类的实例。在父通道成功接收一个连接,并创建成功一个子通道后,就会初始化子通道,这里配置的ChannelInitializer实例就会被调用
// 裝配子通道流水綫
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
// 有连接到达时会创建一个通道的子通道,并初始化
protected void initChannel(SocketChannel ch) throws Exception {
// 流水线管理子通道中的Handler业务处理器
// 向子通道流水线添加一个Handler业务处理器
ch.pipeline().addLast(new NettyDiscardHandler());
}
});
开始绑定服务器连接的监听端口
自我阻塞,直到通道关闭
关闭EventLoopGroup
无论是对于NioServerSocketChannel父通道类型,还是对于NioSocketChannel子通道类型,都可以设置一系列的ChannelOption选项,在ChannelOption类中,定义了一些通道选项:
SO_RCVBUF,SO_SNDBUF:此为TCP参数,每个TCP套接字在内核中都有一个发送缓冲区和一个接收缓冲区,这两个选项就是用来设置TCP连接的这两个缓冲区大小的
TCP_NODELAY:此为TCP参数,表示立即发送数据,默认值为true(Netty默认值为true,而操作系统默认值为false),该值用于设置Nagle算法的启用,该算法将小的碎片数据连接成更大的报文,来最小化所发送报文的数量。如果需要发送一些娇小的报文,则需要禁用该算法。
SO_KEEPALIVE:此为TCP参数,表示底层TCP协议的心跳机制,true为连接保持心跳,默认值为false。启用该功能时TCP会主动探测空闲连接的有效性(默认心跳间隔为7200s)。Netty默认关闭该功能
SO_REUSEADDR:此为TCP参数,设置为true时表示地址复用,默认值为false。由四种情况需要用到这个参数设置:
SO_LINGER:此为TCP参数,表示关闭socket的延迟时间,默认值为-1,表示禁用该功能。-1表示socke.close方法立即返回,但操作系统底层会将发送缓冲区全部发送到对端。0表示socket.close方法立即放回,操作系统放弃发送缓冲区的数据,直接向对端发送RST包,对端收到复位错误。非0整数值表示调用socket.close方法的线程被阻塞,直到延迟时间到来、发送缓冲区的数据发送完毕,若超时,则对端会收到服务复位错误。
SO_BACKLOG:此为TCP参数,表示服务器端接收连接的队列长度,如果队列已满,客户端连接将被拒绝。在Windows中默认值为200,其他操作系统为128.如果连接建立频繁,服务器处理新连接较慢,可以适当调大这个参数
SO_BROADCAST:此为TCP参数,表示设置广播模式
在Netty中,通道是其中的核心概念之一,代表着网络连接。它负责同对端进行网路通信,可以写入数据到对端,也可以从对端读取数据。
通道的抽象类AbstractChannel的构造函数如下:
protected abstract Channel(Channel parent) {
this.parent = parent; // 父通道
id = newId();
unsafe = newUnsafe() // 底层NIO通道,完成实际的IO操作
pipeline = new ChannelPipeline(); // 一条通道,拥有一条流水线
}
AbstractChannel内部有一个pipeline属性,表示处理器的流水线。Netty在对通道进行初始化的时候,会将pipeline属性初始化为DefaultChannelPipeline的实例。
AbstractChannel内部有一个parent属性,表示通道的父通道。对于连接监听通道来说,其父通道为null,而对于每一个传输通道,其parent属性的值为接收到该连接的服务器连接监听通道。
几乎所有的通道实现类都继承了AbstractChannel抽象类,都拥有上面的parent和pipeline两个属性成员。
再来看一下,在通道接口中所定义的几个重要方法:
在Netty的实际开发中,通信的基础工作已经由Netty完成了。实际上大量的工作时设计和开发ChannelHandler通道业务处理器,而不是开发Outbound出站处理器,换句话就是开发Inbound入站处理器。开发完成之后需要投入单元测试,将Handler业务处理器加入到通道的Pipeline流水线中,然后启动Netty服务器、客户端程序,相互发送消息,测试业务处理器的效果。如果每开发一个业务处理器,都进行服务器和客户端的重复启动,那么是非常繁琐和浪费时间的。
因此Netty提供了一个专用通道EmbededChannel,它仅仅是模拟入站和出站的操作,底层不进行实际的传输,不需要启动Netty服务器和客户端。除了不进行传输之外,EmbeddedChannel的其他的事件机制和处理流程和真正的传输通道是一模一样的。因此开发人员可以在开发过程中方便快捷地进行ChannelHandler业务处理器的单元测试。具体提供的方法如下:
最重要的两个方法为writeInbound和readOutbound方法
在Reactor反应器经典模型中,反应器查询到IO事件后,分发到Handler业务处理器,由Handler完成IO操作和业务处理。整个的IO处理操作包括:从通道读取数据包、数据包解码、业务处理、目标数据编码、把数据包写到通道,然后由通道发送到对端。前后两个环节,从通道读取数据包和由通道发送到对端由Netty的底层完成,不需要用户程序负责。
用户程序主要在Handler业务处理器中,主要负责数据包解码、业务处理、目标数据编码、把数据包写入到通道中。前面两个环节属于入站处理器的工作,后面两个环节属于出站处理器的工作。
当数据或信息入站到Netty通道时,Netty将触发入站处理器ChannelInboundHandler所对应的入站API,进行入站处理操作。ChannelInboundHandler的主要操作如下:
上述为ChannelInboundHandler的部分重要方法。在Netty中它的默认实现为ChannelInboundHandlerAdapter,在实际开发中只需要继承这个类的默认实现,重写自己需要的方法即可。
当业务处理完成后,通过一系列的ChannelOutboundHandler通道出站处理器,完成Netty通道到底层通道的操作。包括建立底层连接、断开底层连接、写入底层Java NIO通道等。ChannelOutboundHandler定义了大部分的出站操作,具体如下:
上述为ChannelOutboundHandler的部分重要方法,在Netty中它的默认实现为ChannelOutboundHandlerAdapter,在实际开发中只需要继承这个类的默认实现,重写自己需要的方法即可。
一条Netty的通道拥有一条Handler业务处理器流水线,负责装配自己的Handler业务处理器。装配Handler的工作发生在通道开始工作之前。那么如何向流水线中装配业务处理器呢?这就得借助通道的初始化类ChannelInitializer。
initChannel方法是ChannelInitializer定义的一个抽象方法,这个抽象方法需要开发人员实现。在父通道调用initChannel方法时,会将新接收的通道作为参数,传递给initChannel方法。initChannel方法内部大致的业务代码是拿到新连接通道作为实际参数,往它的流水线中装配Handler业务处理器。
Netty的业务处理器流水线ChannelPipeline是基于责任链设计模式来设计的,内部是一个双向链表结构,能够支持动态地添加和删除Handler业务处理器。
public class InPipeline {
static class SimpleInHandlerA extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("HandlerA");
super.channelRead(ctx, msg);
}
}
static class SimpleInHandlerB extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("HandlerB");
super.channelRead(ctx, msg);
}
}
static class SimpleInHandlerC extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("HandlerC");
super.channelRead(ctx, msg);
}
}
public static void main(String[] args) {
ChannelInitializer<EmbeddedChannel> initializer = new ChannelInitializer<EmbeddedChannel>() {
@Override
protected void initChannel(EmbeddedChannel ch) throws Exception {
ch.pipeline().addLast(new SimpleInHandlerA())
.addLast(new SimpleInHandlerB())
.addLast(new SimpleInHandlerC());
}
};
EmbeddedChannel channel = new EmbeddedChannel(initializer);
ByteBuf buf = Unpooled.buffer();
buf.writeInt(1);
channel.writeInbound(buf);
}
}
在channelRead()方法中调用了父类的channelRead方法,父类的channelRead方法会自动调用下一个inBoundHandler的channelRead方法,并且会把当前inBoundHandler入站处理器中处理完毕的对象传递到下一个inBoundHandler入站处理器。
在入站/出站的过程中,如果由于业务条件不满足,需要阶段流水线的处理,不让流水线进入下一站。以channelRead为例,我们只需要删除掉子类的super.channelRead()方法,不在子类中调用父类的channelRead入站方法,即可实现截断。此外入站处理传入下一站还有一种方法是调用Context上下文的ctx.fireChannelRead()方法,所以想要截断还不能调用该方法。
入站通道处理器的调用顺序会按照加入流水线的顺序调用,而对于出站通道处理器而言,先加入的处理器会在最后被调用。
对于出站处理流程而言,只要开始执行,就不能被截断。强行阶段的话Netty会抛出异常。如果业务条件不满足,可以不启动出站处理。
不管我们定义的是哪种类型的Handler业务处理器,最终它们都是以双向链表的方式保存在流水线中。这里流水线的节点类型,并不是前面的Handler业务处理器基类,而是一个新的Netty类型:ChannelHandlerContext,它代表了ChannelHandler通道处理器和ChannelPipeline通道流水线之间的关联。
ChannelHandlerContext中包含了很多方法,主要分为两类:第一类是获取上下文关联的Netty组件实例,如所关联的通道、所关联的流水线、上下文内部Handler业务处理器实例,第二类是入站和出站方法。
在Channel、ChannelPipeline、ChannelHandlerContext三个类中,会有同样的入栈和出站处理方法。如果通过Channel或ChannelPipeline的实例来调用这些方法,他们就会在整条流水线中传播。然而如果是通过ChannelHandlerContext通道处理器上下文来进行调用,就只会从当前节点开始执行Handler处理器,并传播到同类型的处理器的下一站。
Channel、Handler、ChannelHandlerContext三者的关系为:Channel通道拥有一条ChannelPipeline通道流水线,每一个流水线节点为一个ChannelHandlerContext通道处理器上下文对象,每一个上下文中报过了一个ChannelHandler通道处理器。在ChannelHandler通道处理器的入站和出站处理方法中,Netty都会传递一个Context上下文实例作为实际参数。通过Context实例的实惨,在业务处理中,可以获取ChannelPipeline通道流水线的实例或者Channel通道的实例。
在程序执行过程中,可以动态进行业务处理器的热拔插:动态地增加、删除流水线上的业务处理器。主要的热拔插方法声明在ChannelPipeline接口中,如下:
Netty提供了ByteBuf来替代Java NIO的ByteBuffer缓冲区,以操纵内存缓冲区。
与Java NIO的ByteBuffer相比,ByteBuf的优势如下:
ByteBuf是一个字节容器,内部是一个字节数组。从逻辑上来分,字节容器内部可以分为四个部分:
ByteBuf通过三个整型的属性有效区分可读数据和可写数据,使得读写之间相互没有冲突。这三个属性定义在AbstractByteBuf抽象类中,分别是:
小于readerIndex指针的的部分为废弃部分
此外AbstractByteBuf中还有两个属性:markedWriterIndex和markedReaderIndex,相当于一个暂存属性
容量系列
写入系列
读取系列
Netty的ByteBuf的内存回收工作是通过引用计数的方式管理的。JVM中使用计数器(一种GC方法)来标记对象是否不可达进而回收,Netty也使用这种手段来对ByteBuf的引用进行计数。Netty采用计数器来追踪ByteBuf的生命周期,一是对Pooled ByteBuf的支持,二是能够尽快地发现那些可以回收的ByteBuf(非Pooled),以便提升ByteBuf的分配和销毁的效率。
什么是池化的ByteBuf缓冲区?
在通信程序的执行过程中,Buffer缓冲区实例会被频繁创建、使用、释放。频繁创建对象、内存分配、释放内存会使系统的开销大、性能低。因此Netty4开始新增了对象池化的机制。即创建一个Buffer对象池,将没有被引用的Buffer对象,放入对象缓冲池中。当需要时则重新从对象缓冲池中取出,而不需要重新创建。
在默认情况下,当创建完一个ByteBuf时它的引用为1。每次调用retain方法,它的引用就加1,每次调用release方法,它的引用就减1.如果引用为0,再次访问这个ByteBuf对象,将会抛出异常。如果引用为0,表示这个ByteBuf没有哪个进程引用它,它占用的内存需要回收。
为了确保引用计数不会混乱,在Netty的业务处理器开发过程中,应该坚持一个原则:retain和release方法应该结对使用。简单地说,在一个方法中,调用了retain就应该调用release。
如果retain和release这两个方法一次都不调用,那么在缓冲区使用完成后调用一次release就是释放一次。例如在Netty流水线上,中间所有的Handler业务处理器处理完ByteBuf之后直接传递给下一个,由最后一个Handler负责调用release来释放缓冲区的内存空间。
当引用计数已经为0,Netty会进行ByteBuf的回收。分为两种情况:
Pooled池化的ByteBuf内存,回收方法是放入可以重新分配的ByteBuf池子,等待下一次分配
Unpooled未池化的ByteBuf缓冲区,回收分两种情况:
Netty通过ByteBufAllocator分配器来创建缓冲区和分配内存空间。Netty提供了ByteBufAllocator的两种实现:PoolByteBufAllocator和UnpooledByteAllocator。
PoolByteBufAllocator将ByteBuf实例放入池中,提高了性能,将内存碎片减少到最小,这个池化分配器采用了jemalloc高效内存分配策略,该策略被好几种现代操作系统所采用。
UnpooledByteBufAllocator是普通的未池化ByteBuf分配器,它没有把ByteBuf放入池中,每次被调用时返回一个新的ByteBuf实例,通过Java的垃圾回收机制回收。
在Netty中默认的分配器为ByteBufAllocator.DEFAULT,可以通过Java系统参数的选项io.netty.allocator.type进行配置,配置时使用字符串值“unpooled”和“pooled”。不同Netty版本对于分配器的默认使用策略是不同的,在Netty4.0中默认的分配器为UnpooledByteBufAllocator,而在Netty4.1中默认的分配器为PooledByteBufAllocator。可以在Netty程序中设置启动器Bootstrap的时候将PooledByteBufAllocator设置为默认的分配器。
使用分脾气分配ByteBuf的方法有多种:
根据内存的管理方式不同,分为堆缓冲区和直接缓冲区,也就是Heap ByteBuf和Direct ByteBuf。另外为了方便缓冲区进行组合,提供了一种组合缓冲区:
Heap ByteBuf:内部数据为一个Java数组,存储在JVM的堆空间中,通过hasArray来判断是不是堆缓冲区。
Direct ByteBuf:内部数据存储在操作系统的物理内存中
CompositeBuffer:多个缓冲区的组合表示
上面三种缓冲区的类型,无论哪一种,都可以通过池化、非池化两种分配器来创建和分配内存空间。
Direct Memory(直接内存)不属于Java堆内存,所分配的内存其实是调用操作系统malloc函数来获得的,由Netty的本地内存堆Native堆进行管理。Direct Memory容量可以通过-XX:MaxDirectMemorySize来指定,如果不指定则默认与Java对的最大值-Xmx一样。
Direct Memory的使用避免了Java堆和Native堆之间来回复制数据,在某些应用场景中提高了性能。
在需要频繁创建缓冲区的场合,由于创建和销毁Direct Buffer的代价比较高昂,因此不宜使用Direct Buffer,也就是说Direct Buffer尽量在池化分配器中分配和回收。如果能将Direct Buffer进行复用,在读写频繁的情况下可以大幅度改善性能。
在Java的垃圾回收机制回收Java堆时,Netty框架也会释放不再使用的Direct Buffer缓冲区,因为它的内存为堆外内存,所以清理的工作不会为Java虚拟机带来压力。注意一下垃圾回收的应用场景:
- 垃圾回收仅在Java堆被填满以至于无法为新的堆分配请求提供服务时发生
- 在Java应用程序中调用System.gc()函数来释放内存
对比Heap ByteBuf和Direct ByteBuf两类缓冲区的使用:
public static void main(String[] args) {
ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer();
heapBuf.writeBytes("你好".getBytes(Charset.forName("UTF-8")));
if (!heapBuf.hasArray()) {
// 获取内部数组
byte[] array = heapBuf.array();
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
int length = heapBuf.readableBytes();
System.out.println(new String(array, offset, length, Charset.forName("UTF-8")));
}
heapBuf.release();
ByteBuf directBuffer = ByteBufAllocator.DEFAULT.directBuffer();
directBuffer.writeBytes("你好Direct".getBytes());
if (!directBuffer.hasArray()) {
int length = directBuffer.readableBytes();
byte[] array = new byte[length];
// 把数据读到堆内存
directBuffer.getBytes(directBuffer.readerIndex(), array);
System.out.println(new String(array));
}
directBuffer.release();
}
Netty的Reactor反应器线程会在底层的Java NIO通道读数据,也就是AbstractNioByteChannel.NioByteUnsafe.read(),调用ByteBufAllocator方法,创建ByteBuf实例,从操作系统缓冲区把数据读取到ByteBuf实例中,然后调用pipeline.fireChannelRead(byteBuf)方法将读取到的数据包送入到入站处理流水线中。
以上是ByteBuf的创建方式,而入站的ByteBuf的释放方式有以下两种:
Netty默认会在ChannelPipeline通道流水线的最添加一个TailHeader末尾处理器,它实现了默认的处理方法,在这些方法中会帮助完成ByteBuf内存释放的工作。在默认情况下,如果每个InboundHandler入站处理器,把最初的ByteBuf数据包一路往下传,那么TailHandler默认处理器会自动释放入站的ByteBuf实例。
总体来说如果自定义的InboundHandler入站处理器继承自ChannelInboundHandlerAdapter适配器,那么可以调用以下两种方法来释放ByteBuf内存:
如果Handler业务处理器需要阶段流水线的处理流程,不将ByteBuf数据包送入后边的InboundHandler入站处理器,这是流水线末端的TailHandler末尾处理器自动释放缓冲区的工作自然就失效了。
在这种场景下,Handler业务处理器有两种选择:
以入站读数据为例,Handler业务处理器必须继承自SimpleChannelInboundHandler基类,并且业务处理器的代码必须移动到重写的channelRead0方法中。SimpleChannelInboundHandler类的channelRead等入站处理方法,会在调用完实际的channelRead方法后帮忙释放ByteBuf实例。
至于出站处理,在出站处理流程中,申请分配到的ByteBuf主要是通过HeadHandler完成自动释放的。出站处理用到的ByteBuf缓冲区一般是要发送的消息,通常由Handler业务处理器所申请而分配的,在每一个出站Handler业务处理器的处理完成后,最后数据包回来到出站的最后一棒HeadHandler,在数据输出完成后,ByteBuf会被释放一次,如果计数器为0,将被彻底释放掉。
在Netty开发中,必须密切关注ByteBuf缓冲区的释放,如果释放的不及时,会造成Netty的内存泄露,最终导致内存耗尽。
浅层复制是一种非常重要的操作,可以很大程度地避免内存复制。ByteBuf的浅层复制分为切片浅层复制和整体浅层复制两种。
ByteBuf的slice方法可以获取到一个ByteBuf的一个切片。一个ByteBuf可以进行多次的切片浅层复制,多次切片后的ByteBuf对象可以共享一个存储区域。
slice方法有两个重载版本:
第一个是不带参数的slice方法,在内部是调用了buf.slice(buf.readerIndex(), buf.readableBytes()),也就是说第一个无参数slice方法的返回值是ByteBuf实例中可读部分的切片。
调用slice()方法后,返回的切片是一个新的ByteBuf对象,该对象的几个重要属性,大致如下:
切片后的心ByteBuf有两个特点:
切片后的新ByteBuf和源ByteBuf的关联性:
从根本上说,slice无参数方法所生成的切片就是源ByteBuf可读部分的浅层复制
和slice切片不同,duplicate返回的源ByteBuf的整个对象的一个浅层复制,包括以下内容:
duplicate和slice方法都是浅层复制,不同的是slice方法是切取一段的浅层复制,而duplicate是整体的浅层复制
浅层复制方法不会实际去复制数据,也不会改变ByteBuf的引用计数,这就会导致一个问题:在源ByteBuf调用release之后,一旦引用计数为零就比拿的不能访问了。在这种场景下,源ByteBuf的所有浅层复制实例也不能进行读写了,如果强行对浅层复制实例进行读写则会报错。
因此在调用千层复制实例时,可以通过一次retain方法来增加引用,表示它们对应的底层内存多了一次引用。
public class NettyEchoServer {
private final int serverPort;
ServerBootstrap bootstrap = new ServerBootstrap();
public NettyEchoServer(int serverPort) {
this.serverPort = serverPort;
}
public void runServer() {
// 创建反应器线程组
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
try {
// 1.设置反应器线程组
bootstrap.group(bossLoopGroup, workerLoopGroup);
// 2.设置nio类型的通道
bootstrap.channel(NioServerSocketChannel.class);
// 3.设置监听端口
bootstrap.localAddress(serverPort);
// 4.设置通道的参数
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
// 5.装配子通道流水线
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
// 有连接到达时会创建一个通道
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 流水线管理子通道中的Handler处理器
// 向子通道流水线添加一个handler处理器
socketChannel.pipeline().addLast(NettyEchoServerHandler.INSTANCE);
}
});
// 6.开始绑定服务器
// 通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = bootstrap.bind().sync();
// 7.等待通道关闭的异步任务结束
// 服务监听通道会一直等待通道关闭的异步任务结束
ChannelFuture closeFuture = channelFuture.channel().closeFuture();
closeFuture.sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 8.关闭EventLoopGroup
// 释放掉所有资源包括创建的线程
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new NettyEchoServer(8819).runServer();
}
}
@ChannelHandler.Sharable
public class NettyEchoServerHandler extends ChannelInboundHandlerAdapter {
public static final NettyEchoServerHandler INSTANCE = new NettyEchoServerHandler();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
System.out.println("msg type:" + ((in.hasArray()) ? "堆内存": "直接内存"));
int len = in.readableBytes();
byte[] arr = new byte[len];
in.getBytes(0, arr);
System.out.println("server received:" + new String(arr, "UTF-8"));
System.out.println("写回前, msg.refCnt:" + ((ByteBuf) msg).refCnt());
ChannelFuture f = ctx.writeAndFlush(msg);
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
System.out.println("写回后,msg.refCnt:" + ((ByteBuf) msg).refCnt());
}
});
}
}
这里的NettyEchoServerHandler在前面加了一个特殊的Netty注解:@ChannelHandler.Sharable。这个注解的作用是标注一个Handler实例可以被多个通道安全地共享,也就是说这个通道的流水线可以加入同一个Handler业务处理器实例,而这种操作Netty默认是不允许的。
但是如果一个服务器处理很多的通道,每个通道都新建很多重复的Handler实例,就需要很多重复的Handler实例,这就会浪费很多空间。所以在Handler实例中没有与特定通道强相关的数据或者状态,建议设计成共享的模式,即在前面加上注解@ChannelHandler.Sharable。反过来如果没有加@ChannelHandler.Sharable注解,试图将一个Handler实例添加到多个ChannelPipeline通道流水线时,Netty将会抛出异常。
默认同一个通道上的所有业务处理器,只能被同一个线程处理。所以不是@Sharable共享类型的业务处理器,在线程的层面是安全的,不需要进行线程的同步控制。而不同的通道可能绑定到多个不同的EventLoop反应器线程,因此加上@ChannelHandler.Sharable注解后的共享业务处理器的实例,可能被多个线程并发执行。这样就会导致一个结果@Sharable共享实例不是线程层面安全的,因此@Sharable共享的业务处理器,如果需要操作的数据不仅仅是局部变量,则需要进行线程的同步控制,以保证操作是线程层面安全的。
ChannelHandlerAdapater提供了使用方法isSharable(),如果其对应的实现加上了@Sharable注解,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline通道流水线中。
public class NettyEchoClient {
private int serverPort;
private String serverIp;
Bootstrap b = new Bootstrap();
public NettyEchoClient(String ip, int port) {
this.serverPort = port;
this.serverIp = ip;
}
public void runClient() {
EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
try {
b.group(workerLoopGroup);
b.channel(NioSocketChannel.class);
b.remoteAddress(serverIp, serverPort);
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(NettyEchoClientHandler.INSTANCE);
}
});
ChannelFuture f = b.connect();
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (f.isSuccess()) {
System.out.println("连接成功");
} else {
System.out.println("连接失败");
}
}
});
f.sync();
Channel channel = f.channel();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入发送内容:");
while (scanner.hasNext()) {
String next = scanner.next();
ByteBuf buffer = channel.alloc().buffer();
buffer.writeBytes(next.getBytes("UTF-8"));
channel.writeAndFlush(buffer);
System.out.println("请输入发送内容:");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} finally {
workerLoopGroup.shutdownGracefully();
}
}
}
public class NettyEchoClientHandler extends ChannelInboundHandlerAdapter {
public static final NettyEchoClientHandler INSTANCE = new NettyEchoClientHandler();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
int len = buf.readableBytes();
byte[] arr = new byte[len];
buf.getBytes(0, arr);
System.out.println("client received:" + new String(arr, "UTF-8"));
buf.release();
}
}