看啥推荐读物
专栏名称: 小圣996
目录
相关文章推荐
今天看啥  ›  专栏  ›  小圣996

【Netty】Netty重点一锅端

小圣996  · 简书  ·  · 2020-02-18 22:59

首先来张网上盛传的netty框架参考图,以供读者把握Netty的整体框架及核心组件,继而发散出Netty的重点知识讲解:

netty框架参考图.jpg

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)
Reactor模型.png

当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。

future状态含义.png

4.ByteBuffer、ByteBuf、UnpooledByteBuf、PooledByteBuf关系,及ByteBuf引用计数
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可以在多线程环境下使用,那么在使用时,需要注意它的使用安全。




原文地址:访问原文地址
快照地址: 访问文章快照