Netty就是一个NIO客户端服务器框架
- 基于IO多路复用模型。【依赖JDK NIO框架的多路复用器Selector,一个Selector同时轮询多个Channel,epoll模式只需要一个线程负责Selector轮询】
- 零拷贝
- 基于NIO的Buffer
- 基于内存池的缓冲区重用机制
- 无锁化串行设计理念
- IO操作异步处理
- 提供对protobuf等高性能序列化协议的支持
-
为什么选择Netty?
它作为NIO框架可以高效开发网络应用
IO模型、线程模型(主从Reactor多线程模型)、事件处理机制
- 易用性API接口
- 对数据协议、序列化的支持
整体结构
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
- NioEventLoop就是一个单线程Executor。(从继承关系去看)
- NioEventLoop内部封装这一个新线程Executor成员。
- NioEventLoop有两个execute()方法。本身的execute()与成员属性Executor对应的execute()方法。
- NioEventLoopGroup 是一个线程池线程Executor。默认线程数:CPU核心数*2
- NioEventLoopGroup 也封装了一个线程Executor。
NioEventLoopGroup 也有两个execute()方法。
EventLoop最佳实践
网络连接建立连接过程中三次握手、安全认证采用Boss和Worker两个NioEventLoopGroup分担Reactor线程的压力。
- 耗时较长的ChannelHander可以维护一个线程池,将编解码后的数据封装成Task进行异步处理,避免ChannelHander阻塞。
- 不宜设计过多的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事件。高负债场景下不适用。多线程模型
一个Acceptor线程只负责监听客户端的连接,一个NIO线程池负责处理accept、read、decode、process、encode、send事件。并发连接大时可能有瓶颈。// parentGroup监听客户端连接,负责与客户端创建连接,把连接注册到workerGroup的Selector上
EventLoopGroup parentGroup = new NioEventLoopGroup(1);
// childGroup处理每一个连接发生的读写事件
EventLoopGroup childGroup = new NioEventLoopGroup();
主从多线程模型
从一个主线程NIO线程池中选择一个线程作为Acceptor线程xxx。EventLoopGroup bossGroup= new NioEventLoopGroup();
EventLoopGroup workerGroup= new NioEventLoopGroup();
EventLoop概念是什么
它是一种事件等待和处理的程序模型,解决多线程消耗高的问题。
每当事件发生,应用程序将事件放入到事件队列中,然后EventLoop轮询从队列中取出事件执行。Netty如何实现EventLoop
EventLoop是Reactor线程池模型的事件处理引擎。每个EventLoop线程都维护一个Selector选择器和任务队列taskQueue。
NioEventLoop事件处理机制是无锁串行化的设计思路。
- parentGroup监听客户端的Accept事件,事件触发时,将事件注册到childGroup中的一个EventLoop上,不同的EventLoop线程之间没交集。【线程独立】
- 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的关系。
事件传播机制
- 入站 Inbound 事件(Head-》Tail)和 出战 Outbound 事件(Tail-》Head)
- 异常事件顺序与ChannelHandler添加顺序相同,与Inbound / Outbound 无关。异常处理需要在自定义处理器的末端添加统一的异常处理器。
默认情况下,如果不重写exceptionCaught方法,那么会把该异常继续向后传播,最终会传播到tail节点,tail节点会打印一条日志表明该异常未被处理 如果重写了exceptionCaught方法,并且想将该异常继续向后传播,那么需要调用fireExceptionCaught方法
Netty服务端和客户端启动过程
服务端
- 创建两个NioEventLoopGroup实例,bossGroup和workerGroup。
- 创建服务端启动引导类ServerBootStrap。
- 通过.group方法给引导类配置两大线程组,确定线程模型。
- 通过 channel方法 给引导类指定IO模型为NIO
- 通过childHandler给引导类创建一个ChannelInitializer,然后指定了服务端消息的业务处理逻辑ServerHandler对象。
调用ServerBootStrap类的bind()方法绑定端口。
客户端
创建一个NioEventLoopGroup对象实例
- 创建一个客户端的引导类是BootStrap
- 通过.group方法给引导类配置线程组
- 通过 channel方法给引导类指定IO模型为NIO
- 通过handler给引导类创建一个ChannelInitializer,然后指定了客户端消息的业务处理逻辑对象。
-
三、粘包拆包问题-如何获取一个完整的数据包?
为什么有粘包拆包
TCP面向数据流,没有数据包界限。
基于TCP发送数据时,出现了多个字符被“粘”在一起,或一个字符串被拆开的问题。(有MTU传输单元大小限制、MSS最大分段、滑动窗口)Nagle算法-批量发送
Netty默认禁用(使数据传输延迟最小化),Linux系统默认开启。等到缓冲区积攒到一定大小再把数据包发送出去。
粘包拆包解决办法 -》 定义应用层的通信协议
使用Netty自带的解码器。【固定长度解码器(基于长度编码的二进制协议) / 特殊分隔符解码器 (Redis使用分隔符)/ 自定义分隔符 / 消息长度+消息内容(常用)】
- 自定义序列化解码器。一般使用ProtoStuff、json序列方式比较多。
Netty长连接、心跳机制
TCP长连接
省去了较多的TCP建立和关闭的操作,降低了对网络资源的依赖,节约时间。适用于对频繁请求资源的客户端。
为什么需要心跳机制
TCP长连接过程中,可能出现断网等异常,client与server之间没有交互的话,它们无法发现对方已经掉线。
当它们处于一定时间内没有交互的情况下,服务器或客户端会发送一个特殊的数据包给对方,接收方也需要回应一个报文。
TCP自带的心跳包机制不够灵活,需要在应用层去实现心跳机制。IdleStateHandler
NIO效率高的原理之零拷贝与直接内存映射
零拷贝
操作系统层面中,零拷贝避免了用户态与内核态之间来回拷贝数据。
传统IO读取数据并通过网络发送
- read调用导致上下文从 用户态到内核态。 内核通过sys_read从文件读取数据。DMA引擎执行第一次拷贝,从文件读取数据存储到内核空间缓冲区。
- 数据从内核空间缓冲区拷贝到 用户缓存区,然后read方法返回。从 内核态到用户态,数据存储在用户空间缓存区。
- send调用导致 用户态到内核态。数据从用户空间缓存区到内核套接字缓冲区。
- send调用导致第四次上下文切换。DMA引擎将数据从内核套接字缓冲区传输到协议引擎缓冲区。
内核缓冲区:使得内核可以提前预读部分数据,所需数据大小小于内核缓冲区大小时,提高性能。
DMA拷贝:直接内存存取。外部设备不通过CPU而直接与系统内存交换数据。
NIO零拷贝(不需要进行数据文件操作)
NIO零拷贝由transferTo方法实现,依赖底层操作系统的支持。Linux中,会引起sendfile系统调用,数据会直接从内核的读缓冲区传输到套接字缓冲区(现在只有少数描述符copy到这里,减少CPU拷贝操作),避免了用户态和内核态之间的数据拷贝。
直接内存映射
Linux提供了mmap系统调用,可以将一段用户空间映射到内核空间。它们的修改会互相反映,不需要用户态和内核态之间的拷贝。
JDK1.4以后提供了NIO机制和直接内存,NIO直接在Native堆使用。
直接内存的优点
- 减少垃圾回收对应用的影响。
- 减少数据JVM copy到native堆的次数。
- 突破JVM内存限制。
-
Netty的零拷贝
Zero-copy:计算机执行操作时,CPU不需要将数据从某处内存复制到另一个特定区域。用于网络传输文件,节约CPU周期和内存带宽。
Netty零拷贝完全是在用户态,对数据操作的优化。 堆外内存。避免JVM堆内存到堆外内存的数据拷贝。
- Netty接受发送ByteBuffer使用直接内存进行Socket读写。(NIO)
- 使用Netty提供的CompositeByteBuf类,可以将多个ByteBuf(例如HTTP的header+body)合并成一个逻辑上的ByteBuf,避免ByteBuf之间的拷贝。
- ByteBuf支持Slice,可以将它分解为多个共享同一个存储区域的ByteBuf,避免内存拷贝。
通过FileRegion包装的FileChannel.tranferTo(JDK NIO)实现文件传输,直接可以将文件缓冲区的数据发送到目标channel,避免了循环write方式导致的内存拷贝。
合理管理Netty堆外内存
堆外内存不受JVM控制,降低GC对应用程序运行带来的影响。但堆外内存需要手动释放。
- 网络IO、文件读写时,堆内内存都需要转换成堆外内存,再与底层设备交互。可以减少一次内存拷贝。
-
堆外内存的分配
Netty会使用DirectByteBuffer对象分配堆外内存(通过Unsafe),它会在堆内也创建一个对象,同时创建对应的cleaner对象。当堆内DirectByteBuffer对象被GC时,Cleaner对象会回收堆外内存。
堆外内存的回收
通过GC参数-XX:MaxDirectMemorySize指定堆外内存的上限大小。
cleaner对象属于虚引用,它需要和引用队列ReferenceQueue联合使用。 初始化堆外内存时,cleaner对象会加入cleaner链表中,DirectByteBuffer对象(包含堆外内存地址、大小、CLeaner对象),ReferenceQueue会保存需要回收的cleaner对象。
- 发生GC,DirectByteBuffer对象被回收,cleaner对象没有任何引用关系。
下一次GC发生,该cleaner对象添加到ReferenceQueue,执行clean方法。
Netty数据传输载体ByteBuf
优点:容量按需扩展、读写指针分离、
ByteBuf包含三个指针:读指针readIndex、写指针writeIndex、最大容量maxCapacity。废弃字节。读指针readIndex之前。
- 可读字节。读指针readIndex与写指针writeIndex之间。
- 可写字节。写指针writeIndex与容量Capacity之间。
- 可扩容字节。容量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数据。
- 高效定位。FastThreadLocal在定位数据时,直接使用数组下标即可。此外,它的扩容直接以index为基准取整到2的次幂就好。但ThreadLocal采用hash表,扩容后需要rehash。
安全性更高。线程池情况下,ThreadLocal只能通过主动检测的方式防止内存泄露。FastThreadLocalRunnable最后会执行removeAll将Set集合中的FastThreadLocal清除。
Netty时间轮-HashedWheelTimer
长时间没有到期任务,会存在时间轮空推现象。
- Worker是单线程的,适合处理耗时短的任务。
- 内存占用比传统定时器大。
Kafka的层级时间轮时间粒度更好控制,能处理复杂的定时任务。