首先来张网上盛传的netty框架参考图,以供读者把握Netty的整体框架及核心组件,继而发散出Netty的重点知识讲解:
1.Netty Reactor模型
Reactor模型是对传统阻塞IO模型的巨大改进,实现了向异步非阻塞的飞跃,节省了频繁创建线程和切换线程的开销,极大的提高了IO效率,是现代高性能网络读写处理采用的主要模型。
Reactor模型的核心思想是:事件驱动+分而治之。
它们的Channel注册,及监听关心事件(OP_ACCEPT、OP_READ、OP_WRITE、OP_CONNECT),及事件触发时处理流程,请见《Netty的启动过程二》和《从Java.IO到Java.NIO再到Netty》。
Reactor有三种模型,分别为:
1).单线程Reactor
所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。
代码实现大致为:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)
2).多线程Reactor
一个Acceptor负责接收请求,一个Reactor Thread Pool负责处理I/O操作。
代码实现大致为:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
3).主从多线程Reactor
一个Acceptor负责接收请求,一个Main Reactor Thread Pool负责连接,一个Sub Reactor Thread Pool负责处理I/O操作。
代码实现大致为:
EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
当Netty boss线程组和worker线程组都启动后,一个EventLoopGroup可对应多个EventLoop,每个EventLoop相对应一个Selector。
2.Channel、ChannelPipeline、ChannelHandler、ChannelHandlerContext关系
Channel是客户端与服务端所有I/O操作的通道,当客户端请求连接服务端时,boss线程就会为此连接创建一个SocketChannel注册到worker线程组的一个EventLoop上,并监听读事件。同时,在创建SocketChannel时,也会创建一个ChannelPipeline,ChannelPipeline其实是一个维护ChannelHandlerContext的双向链表,ChannelPipeline创建时,会默认增加HeadContext和TailContext各一个放入其中,最终在SocketChannel注册到worker线程的EventLoop上时,会将childHandler转为ChannelHandlerContext加入ChannelPipeline中,因此ChannelHandlerContext与childHandler也是一一对应的。
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();//创建channel同时创建ChannelPipeline
}
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);//handler转为context,ChannelPipeline实际维护的是handlerContext链
addLast0(newCtx);
}
callHandlerAdded0(newCtx);
return this;
}
因此,最后在用户自定义的handler中和客户端的交互数据其实都是ChannelHandlerContext,如
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
ChannelHandlerContext可以看成是ChannelHandler实例与ChannelPipeline之间的桥梁,在ChannelHandlerContext中可以获取客户端相应的Channel,与之对应的childHandler,及所在的pipeline。
此外,ctx.channel().writeAndFlush(msg)与ctx.writeAndFlush(msg)的区别是,ctx.channel().writeAndFlush(msg)会从出站方向ChannelPipeline的最后一个childHandler把数据发出去,ctx.writeAndFlush(msg)是把数据发给出站方向该handlerContext的下一个childHandler。
因此,一个EventLoop可以监听多个Channel,每个Channel都有一个ChannelPipeline,ChannelPipeline里维护多个ChannelHandlerContext,每个ChannelHandlerContext都有一个相对应的childHandler。
3.Future、ChannelFuture、ChannelPromise区别
Future、ChannelFuture、ChannelPromise相同点是都是Netty异步操作的结果结构。
Netty中所有的I/O操作都是异步的,所有的I/O调用在调用结束时会立即返回,但并不保证所有的I/O操作都完成了,当需要知道某些异步操作结果是否成功或完成或失败时,Future便存在了它的使用价值。Netty Future继承自java.util.concurrent.Future,并扩展增加了自己的一些方法,使得获得异步操作结果更为方便和实用性。比如isSuccess()、Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener)、sync()方法等。ChannelFuture又继承自Netty Future,此外还加入了Channel channel(),即在ChannelFuture中可以获取该客户端连接Channe。ChannelPromise又继承自ChannelFuture,且实现了Promise接口,但是它与ChannelFuture不同的是,它是可写的,如setSuccess()、setFailure(Throwable cause)等方法,它可以标记Futrue的状态,并通知所有的监听者listeners,而listeners是通过addListener方法添加的,同样的,ChannelPromise中也可以获取该客户端连接Channel。
4.ByteBuffer、ByteBuf、UnpooledByteBuf、PooledByteBuf关系
ByteBuffer为Java NIO的数据容器,它长度固定,一旦分配完成后,它的容量不能动态扩展和收缩;它只有一个标识位控的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败。
ByteBuf为Netty的数据容器,ByteBuf支持动态扩容,且通过两个位置指针来协助缓冲区的读写操作,由于写操作不修改readerIndex指针,读操作不修改writerIndex指针,因此读写之间不再需要调整位置指针,这极大地简化了缓冲区的读写操作。
Netty的ByteBuf分为3种类型:
1).heap buffers(堆缓冲区)
这种模式是将数据存储在JVM的堆空间中。 这种模式也称为 backing array,在未使用池的情况下提供快速分配和释放。这种类型ByteBuf在用hasArray()方法判断时返回为true。
2).direct buffers(直接缓冲区)
非堆内存,它在JVM堆外进行内存分配。直接缓冲区的主要缺点是分配和释放它们比堆缓冲区更昂贵,因为它不受JVM垃圾回收管控。如果需要解析直接缓冲区的ByteBuf数据内容,那它需要额外做一次内存复制,这种情况性能会有一些下降。这种类型ByteBuf在用hasArray()方法判断时返回为false。
netty官方有一句描述了使用直接缓冲区的风险:allocating many short-lived direct NIO buffers often causes an OutOfMemoryError。为了更高效地使用堆外缓冲区,netty通过内存池和引用计数很好地绕开了Direct Buffer的劣势,发扬了它的优势。
使用堆缓冲区还是直接缓冲区的最佳实践,应该是根据我们的业务类型来,如果我们需要频繁解析ByteBuf的数据内容,那我们可以选择使用堆缓冲区,而对于I/O通信线程在读写缓冲区时,那可以选择直接缓冲区。
3).composite buffers(复合缓冲区)
它提供了多个或多种类型的ByteBuf的聚合视图,在这里,可以根据需要添加和删除ByteBuf实例,这是JDK的ByteBuffer实现中完全没有的功能。如果某个消息含有消息头和消息体,而消息头不变,就可以使用此类型,从而消除了消息头和消息体不必要的复制。
从内存回收角度看,ByteBuf也分为两类:
1).基于对象池的PooledByteBuf
2).非对象池的UnpooledByteBuf
两者的主要区别就是基于对象池的ByteBuf可以重用ByteBuf对象,它自己维护了一个内存池,可以循环利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC。测试表明使用内存池后的Netty在高负载、大并发的冲击下内存和GC更加平稳。尽管推荐使用基于内存池的ByteBuf,但是内存池的管理和维护更加复杂,使用起来也需要更加谨慎,因此,Netty提供了灵活的策略供使用者来做选择。
通过一个Channel或ChannelHandlerContext的alloc()方法可以获得一个ByteBuf分配的工具,即ByteBufAllocator,该工具默认使用池化的ByteBuf对象分配,可见Netty是推荐使用PooledByteBuf分配ByteBuf的,如下:
static final ByteBufAllocator DEFAULT_ALLOCATOR;
static {
String allocType = SystemPropertyUtil.get(
"io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
} else {
alloc = PooledByteBufAllocator.DEFAULT;
}
DEFAULT_ALLOCATOR = alloc;
}
如果因为使用ByteBuf不当导致内存泄露,可以使用参数'-Dio.netty.leakDetectionLevel=advanced' 定位ByteBuf内存泄露问题。
5.ByteBuf Zero-copy
所谓的 Zero-copy,就是在操作数据时,不需要将数据从一个内存区域拷贝到另一个内存区域。因为少了一次内存的拷贝, 因此 CPU 的效率就得到的提升。在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。
Netty的零拷贝体现在三个方面:
1).Direct Buffers
Netty的接收和发送ByteBuf采用direct buffers,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(heap buffers)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
2).Composite Buffers
Netty提供了复合Buffer对象,可以聚合多个ByteBuf对象,用户可以像操作一个Buffer那样方便的对复合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
3).FileChannel.transferTo
Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。
(摘自李林锋《Netty 系列之 Netty 高性能之道》)
6.各种预置的ChannelHandler、及编解码器使用
ChannelInboundHandlerAdapter
入站处理器,需要注意的是,消息在调用channelRead(ChannelHandlerContext, Object)方法返回后不会自动释放(内存引用),如果你需要找一个入站实现在消息接收后能自动释放,请查看SimpleChannelInboundHandler。ChannelInboundHandlerAdapter是非常常用的一个消息入站处理器,常常用作消息解码器的父类。比如在《使用Netty+Protobuf实现游戏WebSocket通信》一文中,它就作为websocket的解码器解析BinaryWebSocketFrame。
SimpleChannelInboundHandler
一种入站处理器,常用于显式的处理某种特定类型的消息,继承自ChannelInboundHandlerAdapter。需要注意的是,它会自动释放已经处理的消息,如果你想把消息传给下个处理器处理,那么需要调用ReferenceCountUtil#retain(Object)方法保留消息。它也是一种常用的入站消息处理器。
ByteToMessageDecoder
任何数据类型想在网络中进行传输,都得经过编解码转换成字节流。该入站处理器会负责字节流的累加工作,但是具体如何进行解码,则交由不同的子类(用户自定义的处理器)去实现。如在《使用Netty+Protobuf实现游戏TCP通信》一文中,它就作为tcp协议的解码器解析用户自定义数据包。因为tcp就是个流的协议。
IdleStateHandler
它的作用是用于检测channel在指定时间内是否有数据流通,如果没有的话,则触发一个IdleStateEvent,该Event是用于通知本channel的,而不是用于通知对方,所以,我们可以根据收到的Event来决定处理逻辑,常用于心跳处理。
此外,还有HttpServerCodec、HttpObjectAggregator、WebSocketServerProtocolHandler、DelimeterBasedFrameDecoder、LineBasedFrameDecoder、FixedLenghtFrameDecoder、LengthFieldBasedFrameDecoder等等,这些可以自行百度查看如何使用。更多的见netty源码包下handler.codec。
7.@ChannelHandler.Sharable有何用?
通常每个channel都有一个ChannelPipeline对应,而每个ChannelPipeline下channelHandler都是该Channel私有的,但是,有些情况下,需要将某个channelHandler共有,这时,可以将该channelHandler标记为@ChannelHandler.Sharable。
比如游戏服中,客户端请求连接时,需要将所有的客户端Channel缓存起来,这时它的消息handler便会标记为@ChannelHandler.Sharable。再比如,需要对客户端某些ip过滤,也可以用此标记;或者客户端报错统计等等。
该注解表明这个handler可以在多线程环境下使用,那么在使用时,需要注意它的使用安全。
8.Channel的isOpen()、isRegistered()、isActive()和isWritable()状态含义及转换
open表示Channel的开放状态,True表示Channel可用,False表示Channel已关闭不再可用。registered表示Channel的注册状态,True表示已注册到一个EventLoop,False表示没有注册到EventLoop。active表示Channel的激活状态,对于ServerSocketChannel,True表示Channel已绑定到端口;对于SocketChannel,表示Channel可用(open)且已连接到对端。Writable表示Channel的可写状态,当Channel的写缓冲区outboundBuffer非null且可写时返回True。
一个正常结束的Channel状态转移有以下两种情况:
REGISTERED->CONNECT/BIND->ACTIVE->CLOSE->INACTIVE->UNREGISTERED
REGISTERED->ACTIVE->CLOSE->INACTIVE->UNREGISTERED
其中第一种是服务端用于绑定的Channel或者客户端用于发起连接的Channel,第二种是服务端接受的SocketChannel。一个异常关闭的Channel则不会服从这样的状态转移。
(摘自《自顶向下深入分析Netty(六)--Channel总述》)
9.ServerBootstrap的option、childOption的一些常见参数
ChannelOption.SO_BACKLOG
ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小。(在游戏服务器中常用)
ChannelOption.TCP_NODELAY
TCP参数,立即发送数据,默认值为Ture(Netty默认为True而操作系统默认为False)。该值设置Nagle算法的启用,改算法将小的碎片数据连接成更大的报文来最小化所发送的报文的数量,如果需要发送一些较小的报文,则需要禁用该算法。Netty默认禁用该算法,从而最小化报文传输延时。(在游戏服务器中常用)
ChanneOption.SO_REUSEADDR
ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。(在游戏服务器中常用)
ChannelOption.SO_KEEPALIVE
Socket参数,连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。
ChannelOption.SO_LINGER
Netty对底层Socket参数的简单封装,关闭Socket的延迟时间,默认值为-1,表示禁用该功能。-1以及所有<0的数表示socket.close()方法立即返回,但OS底层会将发送缓冲区全部发送到对端。0表示socket.close()方法立即返回,OS放弃发送缓冲区的数据直接向对端发送RST包,对端收到复位错误。非0整数值表示调用socket.close()方法的线程被阻塞直到延迟时间到或发送缓冲区中的数据发送完毕,若超时,则对端会收到复位错误。
(摘自《自顶向下深入分析Netty(六)--Channel总述》及《Netty ChannelOption参数详解》)