Netty就是一个NIO客户端服务器框架

  1. 基于IO多路复用模型。【依赖JDK NIO框架的多路复用器Selector,一个Selector同时轮询多个Channel,epoll模式只需要一个线程负责Selector轮询】
  2. 零拷贝
  3. 基于NIO的Buffer
  4. 基于内存池的缓冲区重用机制
  5. 无锁化串行设计理念
  6. IO操作异步处理
  7. 提供对protobuf等高性能序列化协议的支持
  8. 可对TCP进行更加灵活的配置

    为什么选择Netty?

    它作为NIO框架可以高效开发网络应用

  9. IO模型、线程模型(主从Reactor多线程模型)、事件处理机制

  10. 易用性API接口
  11. 对数据协议、序列化的支持

    整体结构

    Core核心层

    提供了底层网络通信的通用抽象和实现。可扩展的事件模型、通用的通信API、支持零拷贝的ByteBuf等。
    Netty-common包
  • 通用工具类。定时器TimeTask、时间轮HashedWheelTimer
  • 自定义并发
  • 异步模型Future&Promise、增强FastThreadLocal

Netty-buffer包
更加完备的ByteBuf工具类。ByteBuf动态性设计,缓存池设计、减少数据copy

Protocol Support 协议支持层

覆盖了主流协议的编解码器实现。支持自定义应用层协议。

Transport Service 传输服务层

提供了网络传输能力的定义和实现方法。它支持Socket、HTTP隧道等传输。

逻辑架构

网络通信层(执行网络IO操作)

ServerBootStrap 与 BootStrap(启动配置类)【客户端引导类】

引导类。使你的应用程序和网络层相隔离。
BootStrap是客户端引导类。它调用bind/connect时,会新创建一个Channel来实现所有的网络交换。
ServerBootStrap是服务端引导类。它调用bind时,会创建一个ServerChannel接受客户端的连接,同时这个ServerChannel会管理多个child Channel同客户端之间通信。

Channel【通道,对网络IO操作的载体,与底层Socket交互】

传入或者传出数据的载体。设置服务端【通道】实现类型 异步非阻塞TCP Socket连接。在它的生命周期里,它会有状态的变化,连接建立-链接注册-数据读写-连接销毁。

事件调度层(通过Reactor线程模型堆各类时间聚合处理Selector主循环线程)

EventLoop与EventLoopGroup【事件循环,Netty核心抽象,负责监听网络事件并调用事件处理器进行相关IO操作的处理】

EventLoopGroup是一个EventLoop线程池。它包含多个EventLoop
EventLoop定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件。每个Channel都会被分配一个EventLoop(一个EventLoop能有多个Channel绑定它),用于处理用户所有事件。EventLoop在其生命周期里面只会绑定一个线程【线程安全,无锁串行化设计思路】,该线程会处理一个Channel中所有的IO事件。
‘’单线程的执行避免了线程切换,但若有一个IO事件发生阻塞,后续IO事件将无法执行。ChannelHandler实现逻辑要注意。‘’

NioEventLoop和NioEventLoopGroup

  1. NioEventLoop就是一个单线程Executor。(从继承关系去看)
  2. NioEventLoop内部封装这一个新线程Executor成员。
  3. NioEventLoop有两个execute()方法。本身的execute()与成员属性Executor对应的execute()方法。
  4. NioEventLoopGroup 是一个线程池线程Executor。默认线程数:CPU核心数*2
  5. NioEventLoopGroup 也封装了一个线程Executor。
  6. NioEventLoopGroup 也有两个execute()方法。

    EventLoop最佳实践

  7. 网络连接建立连接过程中三次握手、安全认证采用Boss和Worker两个NioEventLoopGroup分担Reactor线程的压力。

  8. 耗时较长的ChannelHander可以维护一个线程池,将编解码后的数据封装成Task进行异步处理,避免ChannelHander阻塞。
  9. 不宜设计过多的ChannelHander。需要明确业务分层和Netty分层之间的界限。

    服务编排层(组装各类服务)

    ChannelHandler(消息处理器)/ChannelHandlerContext与ChannelPipline【双向链表结构】(ChannelHandler的链)

    ChannelHandler是对Channel中数据的处理器,pipline会按照顺序对channel的数据依次处理。
    ChannelHandlerContext会保存ChannelHandler上下文(1对1),ChannelPipline维护的是与ChannelHandlerContext的关系。

    writeAndFlush处理流程

  • writeAndFlush属于出站操作,从Pipeline的Tail节点开始进行事件传播,一直向前传播到Head节点。不管是在write还是flush,Head节点都很重要。
  • write方法没有将数据写到Socket缓冲区,只是将数据写入到ChannelOutBoundBuffer缓存中(单向链表)
  • flush方法会将数据写入到Socket缓冲区。

    ChannelFuture【通过这个接口监听返回结果】

    Netty所有IO操作都是异步的,它的异步编程模型都是建立在Future回调概念上的。

    一、 EventLoop是Netty的精髓

    Netty线程模型

    Netty主要靠NioEventLoopGroup线程池来实现具体的线程模型的。
    Reactor模式基于事件驱动,采用多路复用将事件分发给相应的handler处理,适合处理海量IO的场景。

    单线程模型

    EventLoopGroup eventGroup = NioEventLoopGroup(1);
    一个线程处理所有的accept、read、decode、process、encode、send事件。高负债场景下不适用。

    多线程模型

    1. // parentGroup监听客户端连接,负责与客户端创建连接,把连接注册到workerGroup的Selector上
    2. EventLoopGroup parentGroup = new NioEventLoopGroup(1);
    3. // childGroup处理每一个连接发生的读写事件
    4. EventLoopGroup childGroup = new NioEventLoopGroup();
    一个Acceptor线程只负责监听客户端的连接,一个NIO线程池负责处理accept、read、decode、process、encode、send事件。并发连接大时可能有瓶颈。

    主从多线程模型

    1. EventLoopGroup bossGroup= new NioEventLoopGroup();
    2. EventLoopGroup workerGroup= new NioEventLoopGroup();
    从一个主线程NIO线程池中选择一个线程作为Acceptor线程xxx。

    EventLoop概念是什么

    它是一种事件等待和处理的程序模型,解决多线程消耗高的问题。
    每当事件发生,应用程序将事件放入到事件队列中,然后EventLoop轮询从队列中取出事件执行。

    Netty如何实现EventLoop

    EventLoop是Reactor线程池模型的事件处理引擎。每个EventLoop线程都维护一个Selector选择器和任务队列taskQueue。
    NioEventLoop事件处理机制是无锁串行化的设计思路。
  1. parentGroup监听客户端的Accept事件,事件触发时,将事件注册到childGroup中的一个EventLoop上,不同的EventLoop线程之间没交集。【线程独立】
  2. EventLoop完成数据读取后,调用绑定的ChannelPipline事件传播。【串行化执行,无线程上下文切换】

    Netty-Server的Reactor线程池模型

    ServerBootStrap 实现服务端需要两个EventLoopGroup,parentGroup用于接收客户端的连接,在parentGroup接收到连接后只是将当前转给了childGroup去处理后续操作。childGroup只需要关心处理连接后的操作,无需关心channel的连接任务。

    JDK中,Epoll即使Selector轮询的事件列表为空,NIO线程一样会被唤醒,导致CPU 100%。Netty在每次执行select前都会记录当前时间,去判断事件轮询的持续时间。如果阻塞时间太短,会废弃这个Selector。(规避)

二、Pipline如何协调各类Handler

ChannelPipline内部结构【双向链表结构】(ChannelHandler的链)
它是ChannelHandler的容器载体,当有IO读写事件触发,ChannelPipline依次调用ChannelHandler列表对Channel数据进行拦截和处理。
ChannelHandlerContext会保存ChannelHandler上下文(1对1),ChannelPipline维护的是与ChannelHandlerContext的关系。

事件传播机制

  1. 入站 Inbound 事件(Head-》Tail)和 出战 Outbound 事件(Tail-》Head)
  2. 异常事件顺序与ChannelHandler添加顺序相同,与Inbound / Outbound 无关。异常处理需要在自定义处理器的末端添加统一的异常处理器。

    默认情况下,如果不重写exceptionCaught方法,那么会把该异常继续向后传播,最终会传播到tail节点,tail节点会打印一条日志表明该异常未被处理 如果重写了exceptionCaught方法,并且想将该异常继续向后传播,那么需要调用fireExceptionCaught方法

Netty服务端和客户端启动过程

服务端

  1. 创建两个NioEventLoopGroup实例,bossGroup和workerGroup。
  2. 创建服务端启动引导类ServerBootStrap。
  3. 通过.group方法给引导类配置两大线程组,确定线程模型。
  4. 通过 channel方法 给引导类指定IO模型为NIO
  5. 通过childHandler给引导类创建一个ChannelInitializer,然后指定了服务端消息的业务处理逻辑ServerHandler对象。
  6. 调用ServerBootStrap类的bind()方法绑定端口。

    客户端

  7. 创建一个NioEventLoopGroup对象实例

  8. 创建一个客户端的引导类是BootStrap
  9. 通过.group方法给引导类配置线程组
  10. 通过 channel方法给引导类指定IO模型为NIO
  11. 通过handler给引导类创建一个ChannelInitializer,然后指定了客户端消息的业务处理逻辑对象。
  12. 调用BootStrap类的connect()方法绑定端口。

    三、粘包拆包问题-如何获取一个完整的数据包?

    为什么有粘包拆包

    TCP面向数据流,没有数据包界限。
    基于TCP发送数据时,出现了多个字符被“粘”在一起,或一个字符串被拆开的问题。(有MTU传输单元大小限制、MSS最大分段、滑动窗口

    Nagle算法-批量发送

    Netty默认禁用(使数据传输延迟最小化),Linux系统默认开启。等到缓冲区积攒到一定大小再把数据包发送出去。

    粘包拆包解决办法 -》 定义应用层的通信协议

  13. 使用Netty自带的解码器。【固定长度解码器(基于长度编码的二进制协议) / 特殊分隔符解码器 (Redis使用分隔符)/ 自定义分隔符 / 消息长度+消息内容(常用)】

  14. 自定义序列化解码器。一般使用ProtoStuff、json序列方式比较多。

Netty长连接、心跳机制

TCP长连接
省去了较多的TCP建立和关闭的操作,降低了对网络资源的依赖,节约时间。适用于对频繁请求资源的客户端。

为什么需要心跳机制

TCP长连接过程中,可能出现断网等异常,client与server之间没有交互的话,它们无法发现对方已经掉线。
当它们处于一定时间内没有交互的情况下,服务器或客户端会发送一个特殊的数据包给对方,接收方也需要回应一个报文。
TCP自带的心跳包机制不够灵活,需要在应用层去实现心跳机制。IdleStateHandler

NIO效率高的原理之零拷贝与直接内存映射

零拷贝

操作系统层面中,零拷贝避免了用户态与内核态之间来回拷贝数据。
传统IO读取数据并通过网络发送

  1. read调用导致上下文从 用户态到内核态。 内核通过sys_read从文件读取数据。DMA引擎执行第一次拷贝,从文件读取数据存储到内核空间缓冲区。
  2. 数据从内核空间缓冲区拷贝到 用户缓存区,然后read方法返回。从 内核态到用户态,数据存储在用户空间缓存区。
  3. send调用导致 用户态到内核态。数据从用户空间缓存区到内核套接字缓冲区。
  4. send调用导致第四次上下文切换。DMA引擎将数据从内核套接字缓冲区传输到协议引擎缓冲区。
    内核缓冲区:使得内核可以提前预读部分数据,所需数据大小小于内核缓冲区大小时,提高性能。
    DMA拷贝:直接内存存取。外部设备不通过CPU而直接与系统内存交换数据。

NIO零拷贝(不需要进行数据文件操作)
NIO零拷贝由transferTo方法实现,依赖底层操作系统的支持。Linux中,会引起sendfile系统调用,数据会直接从内核的读缓冲区传输到套接字缓冲区(现在只有少数描述符copy到这里,减少CPU拷贝操作),避免了用户态和内核态之间的数据拷贝。

直接内存映射

Linux提供了mmap系统调用,可以将一段用户空间映射到内核空间。它们的修改会互相反映,不需要用户态和内核态之间的拷贝。
JDK1.4以后提供了NIO机制和直接内存,NIO直接在Native堆使用。

直接内存的优点

  1. 减少垃圾回收对应用的影响。
  2. 减少数据JVM copy到native堆的次数。
  3. 突破JVM内存限制。
  4. 直接内存不足也会触发full gc。

    Netty的零拷贝

    Zero-copy:计算机执行操作时,CPU不需要将数据从某处内存复制到另一个特定区域。用于网络传输文件,节约CPU周期和内存带宽。
    Netty零拷贝完全是在用户态,对数据操作的优化。

  5. 堆外内存。避免JVM堆内存到堆外内存的数据拷贝。

  6. Netty接受发送ByteBuffer使用直接内存进行Socket读写。(NIO)
  7. 使用Netty提供的CompositeByteBuf类,可以将多个ByteBuf(例如HTTP的header+body)合并成一个逻辑上的ByteBuf,避免ByteBuf之间的拷贝。
  8. ByteBuf支持Slice,可以将它分解为多个共享同一个存储区域的ByteBuf,避免内存拷贝。
  9. 通过FileRegion包装的FileChannel.tranferTo(JDK NIO)实现文件传输,直接可以将文件缓冲区的数据发送到目标channel,避免了循环write方式导致的内存拷贝。

    合理管理Netty堆外内存

  10. 堆外内存不受JVM控制,降低GC对应用程序运行带来的影响。但堆外内存需要手动释放。

  11. 网络IO、文件读写时,堆内内存都需要转换成堆外内存,再与底层设备交互。可以减少一次内存拷贝。
  12. 堆外内存可以实现进程之间、JVM多实例间数据共享。

    堆外内存的分配

    Netty会使用DirectByteBuffer对象分配堆外内存(通过Unsafe),它会在堆内也创建一个对象,同时创建对应的cleaner对象。当堆内DirectByteBuffer对象被GC时,Cleaner对象会回收堆外内存。

    堆外内存的回收

    通过GC参数-XX:MaxDirectMemorySize指定堆外内存的上限大小。
    cleaner对象属于虚引用,它需要和引用队列ReferenceQueue联合使用。

  13. 初始化堆外内存时,cleaner对象会加入cleaner链表中,DirectByteBuffer对象(包含堆外内存地址、大小、CLeaner对象),ReferenceQueue会保存需要回收的cleaner对象。

  14. 发生GC,DirectByteBuffer对象被回收,cleaner对象没有任何引用关系。
  15. 下一次GC发生,该cleaner对象添加到ReferenceQueue,执行clean方法。

    Netty数据传输载体ByteBuf

    优点:容量按需扩展、读写指针分离、
    ByteBuf包含三个指针:读指针readIndex、写指针writeIndex、最大容量maxCapacity。

  16. 废弃字节。读指针readIndex之前。

  17. 可读字节。读指针readIndex与写指针writeIndex之间。
  18. 可写字节。写指针writeIndex与容量Capacity之间。
  19. 可扩容字节。容量Capacity与最大容量maxCapacity之间。

    引用计数

    ByteBuf的生命周期由引用计数所管理,只要计数大于0就代表还在使用。(避免了每次使用ByteBuf都需要重建。)

    轻量级对象回收站:Recycler对象池技术

    Stack【对象池顶层数据结构】:存储本线程回收的对象。每个线程通过FastThreadLocal实现每个线程的私有化。
    WeakOrderQueue:存储其他线程回收到当前线程所分配的对象。
    Link:每个WeakOrderQueue中都包含一个Link链表,回收对象会存在link链表中的节点上。
    DefaultHandler:保存实际回收的对象。

当需要某个对象时,优先从对象池获取实例。通过重用对象,不仅避免频繁地创建和销毁所带来的性能损耗,而且对GC友好。

  • 对象池有两个重要组成:Stack+WeakOrderQueue
  • 从Recycler获取对象时,优先从Stack查找,如果没有可用对象,尝试从WeakOrderQueue迁移部分对象到Stack中。
  • 从Recycler回收对象时,同一个线程回收直接向Stack添加对象。异线程回收需要向WeakOrderQueue中的Link添加对象。
  • 对象回收会控制回收速率,每8个对象回收一个,其它的全部丢弃。

    FastThreadLocal比ThreadLocal快在哪里?

    ThreadLocal

    一个线程里面能存在多个ThreadLocal对象。
    如果在ThreadLocal里面维护一个Map,记录线程与实例之间的关系,但高并发下操作map需要加锁。(不行)
    以Thread入手,在Thread中维护一个Map【ThreadLocal的内部类ThreadLocalMap】,记录了ThreadLocal与实例之间的映射关系。
    ThreadLocalMap(为ThreadLocal量身定制)
    线性探测法(冲突了就会向后查找一位。缺点:在数据密集时容易出现Hash冲突,需要O(n)的时间复杂读解决冲突)实现的哈希表,使用数组存储数据。

  • key(虚引用):ThreadLocal对象本身。当某个ThreadLocal不再使用,ThreadLocalMap还存在着对ThreadLocal强引用,无法被GC。

  • value(强引用):用户需要存储的值。【get/set方法时,会清除key为null的value。但最好手动remove】

    FastThreadLocal(需要配合FastThreadLocalThread)

    InternalThreadLocalMap没有采用线性探测法解决Hash冲突,而是在FastThreadLocal初始化时分配一个数组索引index,采用原子类AutomaticInteger保证顺序递增,读写数据会通过下标直接定位到FastThreadLocal的位置。
    使用Object数组替换了Entry数组,Object【0】存储一个Set>集合,其它下标直接存储的value数据。
  1. 高效定位。FastThreadLocal在定位数据时,直接使用数组下标即可。此外,它的扩容直接以index为基准取整到2的次幂就好。但ThreadLocal采用hash表,扩容后需要rehash。
  2. 安全性更高。线程池情况下,ThreadLocal只能通过主动检测的方式防止内存泄露。FastThreadLocalRunnable最后会执行removeAll将Set集合中的FastThreadLocal清除。

    Netty时间轮-HashedWheelTimer

  3. 长时间没有到期任务,会存在时间轮空推现象。

  4. Worker是单线程的,适合处理耗时短的任务。
  5. 内存占用比传统定时器大。

Kafka的层级时间轮时间粒度更好控制,能处理复杂的定时任务。