Netty权威指南
TCP 粘包/拆包
TCP 是一种流协议(Stream Protocol)。所谓“流”,即为没有界限的一串数据。可以想象河流中的流水,它们连成一片,其间并没有明显的分界线。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分。所以在业务层面上,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 拆包和粘包问题。
发生原因
问题产生的原因主要有以下三点:
- 应用程序写入的字节大小大于套接字发送缓冲区大小;
- 进行 MSS(Maximum Segment Size)大小的 TCP 分段;
- 以太网帧 payload 大于 MTU(Maximum Transmission Unit)进行 IP 分片。

解决策略
由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的。这个问题只能通过上层的应用层协议栈设计来解决。根据业界主流协议的解决方案,可以归纳为以下几种方式:
- 消息定长:例如每个报文的大小为固定长度 200 字节,如果不够,空位补空格;
- 特殊分隔符:在包尾增加回车换行符进行分割,例如 FTP 协议;
- 长度字段:将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。通常设计思路为消息头的第一个字段使用
int32来表示消息的总长度; - 复杂应用层协议:更复杂的应用层协议设计。
为了解决 TCP 粘包/拆包导致的半包读写问题,Netty 默认提供了多种编解码器用于处理半包。只要能熟练掌握这些类库的使用,TCP 粘包问题将变得非常容易,甚至不需要去关心它们,这也是其他 NIO 框架和 JDK 原生的 NIO API 所无法匹敌的。
UDP 不发生粘包的原因
UDP 不存在粘包问题,主要原因如下:
- 无 Nagle 算法优化:UDP 发送的时候,没有经过 Nagle 算法的优化,不会将多个小包合并成一个大包发送出去。
- 接收机制:在 UDP 协议的接收端,采用了链式结构来记录每一个到达的 UDP 包。这样接收端应用程序一次
recv只能从 socket 接收缓冲区中读出一个数据包。也就是说,发送端send了几次,接收端必须recv几次。
WebSocket 协议开发
长期以来,存在着各种技术让服务器得知有新数据可用时,立即将数据发送给客户端。这些技术种类繁多,例如“推送”或 Comet。最常见的一种手段是对服务器发起连接创建假象,被称为长轮询。利用长轮询,客户端可以打开指向服务器的 HTTP 连接,而服务器一直保持连接打开,直到发送响应。服务器只要实际拥有新数据,就会发送响应。长轮询和其他技术都非常好用,在 Gmail 聊天应用中会经常使用它们。但是,这些解决方案都存在一个共同的问题:由于 HTTP 协议的开销,导致它们不适合用于低延迟应用。
为了解决这些问题,WebSocket 将网络套接字引入到了客户端和服务器。浏览器和服务器之间可以通过套接字建立持久的连接,双方随时都可以互发数据给对方,而不是之前由客户端控制的请求——应答模式。
HTTP 协议的主要弊端
- 半双工协议:数据可以在客户端和服务器端两个方向上传输,但是不能同时传输。它意味着在同一时刻,只有一个方向上的数据传送;
- 消息冗长而繁琐:HTTP 消息包含消息头、消息体、换行符等,通常情况下采用文本方式传输,相比其他的二进制通信协议,冗长而繁琐;
- 针对服务器推送的低效:例如长时间轮询。
传统的轮询模式需要浏览器不断向服务器发出请求,然而 HTTP 请求的请求头冗长,可用数据非常低,会占用很多的带宽和服务器资源。比较新的一种轮询技术是 Comet,使用了 AJAX。这种技术虽然可达到双向通信,但依然需要发出请求,而且在 Comet 中,普遍采用了长连接,这也会大量消耗服务器带宽和资源。
为了解决 HTTP 协议效率低下的问题,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽并达到实时通信。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道,两者可以直接互相传送数据。WebSocket 基于 TCP 双向全双工进行消息传递,在同一时刻,既可以发送消息,也可以接收消息,相比 HTTP 的半双工协议,性能得到很大提升。
WebSocket 的特点
- 单一的 TCP 连接,采用全双工模式通信;
- 对代理、防火墙和路由器透明;
- 无头部信息、Cookie 和身份验证;
- 无安全开销;
- 通过"ping/pong"帧保持链路激活;
- 服务器可以主动传递消息给客户端,不再需要客户端轮询。
Netty 私有协议栈可靠性设计
心跳机制
在凌晨等业务低谷期时段,如果发生网络闪断、连接被挂起(Hang)等网络问题时,由于没有业务消息,应用进程很难发现。到了白天业务高峰期时,会发生大量的网络通信失败,严重的会导致一段时间进程内无法处理业务消息。为了解决这个问题,在网络空闲时采用心跳机制来检测链路的互通性,一旦发现网络故障,立即关闭链路,主动重连。具体的设计思路如下:
- 当网络处于空闲状态持续时间达到 T(连续周期 T 没有读写消息)时,客户端主动发送 Ping 心跳消息给服务端;
- 如果在下一个周期 T 到来时客户端没有收到对方发送的 Pong 心跳应答消息或者读取到服务端发送的其他业务消息,则心跳失败计数器加 1;
- 每当客户端接收到服务端的业务消息或者 Pong 应答消息时,将心跳失败计数器清零;连续 N 次没有接收到服务端的 Pong 消息或者业务消息,则关闭链路,间隔 INTERVAL 时间后发起重连操作;
- 服务端网络空闲状态持续时间达到 T 后,服务端将心跳失败计数器加 1;只要接收到客户端发送的 Ping 消息或者其他业务消息,计数器清零;
- 服务端连续 N 次没有接收到客户端的 Ping 消息或者其他业务消息,则关闭链路,释放资源,等待客户端重连。
通过 Ping-Pong 双向心跳机制,可以保证无论通信哪一方出现网络故障,都能被及时的检测出来。为了防止对方短时间内繁忙没有及时返回应答造成的误判,只有连续 N 次心跳检测都失败时才认定链路已经损害,需要关闭链路并重建链路。
当读或者写心跳消息发生 I/O 异常的时候,说明链路已经中断,此时需要立即关闭链路。如果是客户端,需要重新发起连接;如果是服务端,需要清空缓存的半包信息,等待客户端重连。
重连机制
- 如果链路中断,等待 INTERVAL 时间后,由客户端发起重连操作。如果重连失败,间隔周期 INTERVAL 后再次发起重连,直到重连成功;
- 为了保证服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待 INTERVAL 时间后再发起重连,而不是失败后立即重连;
- 为了保证句柄资源能够及时释放,无论什么场景下的重连失败,客户端都必须保证自己的资源能够被及时释放,包括但不限于
SocketChannel、Socket等; - 重连失败后,需要打印异常堆栈信息,方便后续的问题定位。
重复登录保护
当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽。
服务端接收到客户端的握手请求消息之后,首先对 IP 地址进行合法性检验。如果校验成功,在缓存的地址表中查看客户端是否已经登录。如果已经登录,则拒绝重复登录,返回错误码 -1,同时关闭 TCP 链路,并在服务端的日志中打印握手失败的原因。
客户端接收到握手失败的应答消息之后,关闭客户端的 TCP 连接,等待 INTERVAL 时间之后,再次发起 TCP 连接,直到认证成功。
为了防止服务端和客户端对链路状态理解不一致导致的客户端无法握手成功的问题,当服务端连续 N 次心跳超时之后需要主动关闭链路,清空该客户端的地址缓存信息,以保证后续该客户端可以重连成功,防止被重复登录保护机制拒绝掉。
消息缓存重发
无论客户端还是服务端,当发生链路中断之后,在链路恢复之前,缓存在消息队列中待发送的消息不能丢失。等链路恢复以后,重新发送这些消息,保证链路中断期间消息不丢失。
考虑到内存溢出的风险,建议消息队列设置上限。当达到上限之后,应该拒绝继续向消息队列中添加新的消息。
Netty 服务端创建
服务端创建时序图

链路建立的时候创建并初始化 ChannelPipeline。ChannelPipeline 并不是 NIO 服务端所必需的,它本质就是一个负责处理网络事件的责任链,负责管理和执行 ChannelHandler。网络事件以事件流的形式在 ChannelPipeline 中流转,由 ChannelPipeline 根据 ChannelHandler 的执行策略调度 ChannelHandler 的执行。典型的网络事件如下:
- 链路注册;
- 链路激活;
- 链路断开;
- 接收到请求消息;
- 请求消息接收并处理完毕;
- 发送应答消息;
- 链路发生异常;
- 发生用户自定义事件。
初始化 ChannelPipeline 完成之后,添加并设置 ChannelHandler。ChannelHandler 是 Netty 提供给用户定制和扩展的关键接口。利用 ChannelHandler 用户可以完成大多数的功能定制,例如消息编解码、心跳、安全认证、TLS/SSL 认证、流量控制和流量整形等。Netty 同时也提供了大量的系统 ChannelHandler 供用户使用,比较常用的系统 ChannelHandler 总结如下:
- 系统编解码框架——
ByteToMessageCodec; - 通用基于长度的半包解码器——
LengthFieldBasedFrameDecoder; - 码流日志打印 Handler——
LoggingHandler; - SSL 安全认证 Handler——
SslHandler; - 链路空闲检测 Handler——
IdleStateHandler; - 流量整形 Handler——
ChannelTrafficShapingHandler; - Base64 编解码——
Base64Decoder和Base64Encoder。
TCP 参数设置完成后,用户可以为启动辅助类和其父类分别制定 Handler。两类 Handler 的用途不同:子类中的 Handler 是 NioServerSocketChannel 对应的 ChannelPipeline 的 Handler;父类中的 Handler 是客户端新接入的连接 SocketChannel 对应的 ChannelPipeline 的 Handler。
ByteBuf 功能说明
ByteBuffer 完全可以满足 NIO 编程的需要,但是由于 NIO 编程的复杂性,ByteBuffer 也有其局限性,它的主要缺点如下:
ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩。当需要编码的 POJO 对象大于ByteBuffer的容量时,会产生索引越界异常;ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎的处理这些 API,否则很容易导致程序处理失败;ByteBuffer的 API 功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。
ByteBuf 的工作原理
ByteBuf 依然是个 Byte 数组的缓冲区,它的基本功能应该与 JDK 的 ByteBuffer 一致。ByteBuf 通过两个位置指针来协助缓冲区的读写操作,读操作使用 readerIndex,写操作使用 writerIndex。
通常情况下,当我们对 ByteBuffer 进行 put 操作的时候,如果缓冲区剩余可写空间不够,就会发生 BufferOverflowException 异常。为了避免发生这个问题,通常在进行 put 操作的时候会对剩余可用空间进行校验。如果剩余空间不足,需要重新创建一个新的 ByteBuffer,并将之前的 ByteBuffer 复制到新创建的 ByteBuffer 中,最后释放老的 ByteBuffer。
从内存分配的角度看,ByteBuf 可以分为两类:
- 堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被 JVM 自动回收;缺点就是如果进行 Socket 的 IO 读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核 Channel 中,性能会有一定程度的下降;
- 直接内存(DirectByteBuf)字节缓冲区:非堆内存,它在堆外进行内存分配。相比于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从 Socket Channel 中读取时,由于少了一次内存复制,速度比堆内存快。
正是因为各有利弊,所以 Netty 提供了多种 ByteBuf 供开发者使用。经验表明,ByteBuf 的最佳实践是在 IO 通信线程的读写缓冲区使用 DirectByteBuffer,后端业务消息的编解码模块使用 HeapByteBuf,这样组合可以达到性能最优。
从内存回收角度看,ByteBuf 也分为两类:基于对象池的 ByteBuf 和普通 ByteBuf。两者的主要区别就是基于对象池的 ByteBuf 可以重用 ByteBuf 对象,它自己维护了一个内存池,可以循环利用创建的 ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁 GC。测试表明 使用内存池后的 Netty 在高负载、大并发的冲击下内存和 GC 更加平稳。
尽管推荐使用基于内存池的 ByteBuf,但是内存池的管理和维护更加复杂,使用起来也需要更加谨慎,因此,Netty 提供了灵活的策略供使用者来做选择。
Channel 的工作原理
Channel 是 Netty 抽象出来的网络 IO 读写相关的接口。为什么不使用 JDK NIO 原生的 Channel 而要另起炉灶呢?主要原因如下:
- JDK 的
SocketChannel和ServerSocketChannel没有统一的Channel接口供业务开发者使用,对于用户而言,没有统一的操作视图,使用起来并不方便; - JDK 的
SocketChannel和ServerSocketChannel的主要职责就是网络 IO 操作。由于它们是 SPI 类接口,由具体的虚拟机厂家来提供,所以通过继承 SPI 功能类来扩展其功能的难度很大;直接实现ServerSocketChannel和SocketChannel抽象类,其工作量和重新开发一个新的 Channel 功能类是差不多的; - Netty 的
Channel需要能够跟 Netty 的整体架构融合在一起,例如 IO 模型、基于ChannelPipeline的定制模型,以及基于元数据描述配置化的 TCP 参数等,这些 JDK 的SocketChannel和ServerSocketChannel都没有提供,需要重新封装; - 自定义的
Channel,功能实现更加灵活。
基于上述四个原因,Netty 重新设计了 Channel 接口,并且给予了很多不同的实现。它的设计原理比较简单,但是功能却比较复杂,主要的设计理念如下:
- 在
Channel接口层,采用门面模式进行统一封装,将网络 IO 操作、网络 IO 相关联的其他操作封装起来,统一对外提供; Channel接口的定义尽量大而全,为SocketChannel和ServerSocketChannel提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度的实现功能和接口的重用;- 具体实现采用聚合而非包含的方式,将相关的功能类聚合在
Channel中,由Channel统一负责分配和调度,功能实现更加灵活。
Reactor 线程模型
Reactor 单线程模型
Reactor 单线程模型,是指所有的 IO 操作都在同一个 NIO 线程上面完成。NIO 线程的职责如下:
- 作为 NIO 服务端,接收客户端的 TCP 连接;
- 作为 NIO 客户端,向服务端发起 TCP 连接;
- 读取通信对端的请求或者应答信息;
- 向通信对端发送消息请求或者应答消息。
Reactor 单线程模型如下图所示:

由于 Reactor 模式使用的是同步非阻塞 IO,所有的 IO 操作都不会导致阻塞,理论上一个线程可以独立处理所有 IO 相关的操作。从架构层面看,一个 NIO 线程确实可以完成其承担的职责。例如,通过 Acceptor 类接收客户端的 TCP 连接请求消息,当链路建立成功之后,通过 Dispatcher 将对应的 ByteBuffer 派发到指定的 Handler 上,进行消息解码。用户线程消息编码后通过 NIO 线程将消息发送给客户端。
在一些小容量应用场景下,可以使用单线程模型。但是这对于高负载、大并发的应用场景却不适合,主要原因如下:
- 一个 NIO 线程同时处理成百上千个的链路,性能无法支撑。即便 NIO 线程的 CPU 负荷达到 100%,也无法满足海量消息的编码、解码、读取和发送;
- 当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时。超时之后往往会选择重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
- 可靠性问题。一旦 NIO 线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
为了解决这些问题,演进出了 Reactor 多线程模型。
Reactor 多线程模型

Reactor 多线程模型的特点如下:
- 有专门一个 NIO 线程——
Acceptor线程用于监听服务端,接收客户端的 TCP 连接请求; - 网络 IO 操作——读、写等由一个 NIO 线程池负责。线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送;
- 一个 NIO 线程可以同时处理 N 条链路,但是一个链路只对应一个 NIO 线程,防止发生并发操作问题。
在绝大多数场景下,Reactor 多线程模型可以满足性能需求。但是,在个别特殊场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端进行安全认证,但是认证本身非常损耗性能。在这种场景下,单独一个 Acceptor 线程可能会存在性能不足的问题。为了解决性能问题,产生了第三种 Reactor 线程模型——主从 Reactor 多线程模型。
主从 Reactor 多线程模型

主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP 连接请求并处理完成后(可能包含接入认证等),将新创建的 SocketChannel 注册到 IO 线程池(sub reactor 线程池)的某个 IO 线程上,由它负责 SocketChannel 的读写和编码工作。Acceptor 线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 IO 线程上,由 IO 线程负责后续的 IO 操作。
利用主从 NIO 线程模型,可以解决一个服务端监听线程无法有效处理客户端连接的性能不足问题。因此,在 Netty 的官方 Demo 中,推荐使用该线程模型。
Netty 的线程模型
Netty 的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty 可以同时支持 Reactor 单线程模型、多线程模型和主从 Reactor 多线程模型。

可以通过如下代码来了解它的线程模型:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {});服务端启动的时候,创建了两个 NioEventLoopGroup,它们实际是两个独立的 Reactor 线程池。一个用于接收客户端的 TCP 连接,另一个用于处理 IO 相关的读写操作,或者执行系统 Task、定时任务 Task 等。
Netty 用于接收客户端请求的线程池职责如下:
- 接收客户端 TCP 连接,初始化 Channel 参数;
- 将链路状态变更事件通知给
ChannelPipeline。
Netty 处理 IO 操作的 Reactor 线程池职责如下:
- 异步读写通信对端的数据报,发送读事件到
ChannelPipeline; - 异步发送消息通信对端,调用
ChannelPipeline的消息发送接口; - 执行系统调用 Task;
- 执行定时任务 Task,例如链路空闲状态监测定时任务。
最佳实践
Netty 的多线程编程最佳实践如下:
- 创建两个
NioEventLoopGroup,用于逻辑隔离 NIO Acceptor 和 NIO IO 线程; - 尽量不要在
ChannelHandler中启动用户线程(解码后用于将 POJO 消息派发到后端业务线程的除外); - 解码要放在 NIO 线程调用的解码 Handler 中进行,不要切换到用户线程中完成消息的解码;
- 如果业务逻辑操作非常简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网络操作等,可以直接在 NIO 线程上完成业务逻辑编写,不需要切换到用户线程;
- 如果业务逻辑处理复杂,不要在 NIO 线程上完成。建议将编码后的 POJO 消息封装成 Task,派发到业务线程池中由业务线程执行,以保证 NIO 线程尽快被释放,处理其他的 IO 操作。
推荐的线程数量计算公式有以下两种:
- 公式一:
线程数量 =(线程总时间 / 瓶颈资源时间)× 瓶颈资源的线程并行数 - 公式二:
QPS = 1000 / 线程总时间 × 线程数
NioEventLoop
NioEventLoop 继承关系类图如下:

Future 和 Promise
在 Netty 中,所有的 IO 操作都是异步的。这意味着任何 IO 调用都会立即返回,而不是像传统的 BIO 那样同步等待操作完成。异步操作带来一个问题:调用者如何获取异步操作的结果?ChannelFuture 就是为了解决这个问题而专门设计的。
ChannelFuture 有两种状态:uncompleted 和 completed。当开始一个 IO 操作时,一个新的 ChannelFuture 被创建,此时它处于 uncompleted 状态——非失败、非成功、非取消,因为 IO 操作此时还没有完成。一旦 IO 操作完成,ChannelFuture 将会被设置成 completed,它的结果有如下三种可能:
- 操作成功;
- 操作失败;
- 操作被取消。
ChannelFuture 的状态迁移图如下所示:

Netty 强烈建议直接通过添加监听器的方式获取 IO 操作结果,或者进行后续的相关操作。需要注意的是:不要在 ChannelHandler 中调用 ChannelFuture 的 await() 方法,这会导致死锁。 原因是发起 IO 操作之后,由 IO 线程负责异步通知发起 IO 操作的用户线程。如果 IO 线程和用户线程是同一个线程,就会导致 IO 线程等待自己通知操作完成,这就导致了死锁。这跟经典的两个线程互等待死锁不同,属于自己把自己挂死。
Promise 是可写的 Future。Future 自身并没有写操作相关的接口,Netty 通过 Promise 对 Future 进行扩展,用于设置 IO 操作的结果。
Netty 架构剖析
Netty 采用典型的三层网络架构进行设计和开发,逻辑架构如下图所示:

Reactor 通信调度层
它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop 及其父类,NioSocketChannel/NioServerSocketChannel 及其父类,ByteBuf 以及由其衍生出来的各种 Buffer,Unsafe 以及其衍生出来的各种内部类等。该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件,例如连接建立、连接激活、读事件、写事件等,将这些事件触发到 Pipeline 中,由 Pipeline 管理的责任链来进行后续的处理。
责任链 ChannelPipeline
它负责事件在职责链中的有序传播,同时负责动态的编排责任链。职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向后/向前传播事件。不同应用的 Handler 节点的功能也不同。通常情况下,往往会开发编解码 Handler 用于消息的编解码,它可以将外部的协议消息转换成内部的 POJO 对象。这样上层业务则只需要关心处理业务逻辑即可,不需要感知底层的协议差异和线程模型差异,实现了架构层面的分层隔离。
业务逻辑层(Service ChannelHandler)
业务逻辑编排层通常有两类:一类是纯粹的业务逻辑编排,还有一类是其他的应用层协议插件,用于特定协议相关的会话和链路管理。例如 CMPP 协议,用于管理和中国移动短信系统的对接。
架构的不同层面,需要关心和处理的对象都不同。通常情况下,对于业务开发者,只需要关心责任链的拦截与业务 Handler 的编排。因为应用层协议栈往往开发一次,到处运行,所以实际上对于业务开发者来说,只需要关心服务层的业务逻辑开发即可。各种应用协议以插件的形式提供,只有协议开发人员需要关注这些协议插件。对于其他业务开发人员来说,只需要关心业务逻辑定制。这种分层的架构设计理念实现了 NIO 框架各层之间的解耦,便于上层协议栈的开发和业务逻辑的定制。
核心特性
高性能
Netty 的架构设计通过如下方式实现高性能:
- 采用异步非阻塞 IO 类库,基于 Reactor 模式实现,解决了传统同步阻塞 IO 模式写一个服务端无法平滑的处理线性正常的客户端的问题;
- TCP 接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制,提升了 IO 读取和写入的性能;
- 支持通过内存池的形式循环利用
ByteBuf,避免了频繁创建和销毁ByteBuf带来的性能损耗; - 可配置的 IO 线程数、TCP 参数等,为不同的用户场景提供定制化的调优参数,满足不同的性能场景;
- 采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器或者锁;
- 合理的使用线程安全容器、原子类等,提升系统的并发处理能力;
- 关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来的锁竞争和额外的 CPU 资源消耗问题;
- 通过引用计数及时的申请释放不再被引用的对象,细粒度的内存管理降低了 GC 的频率,减少了频繁 GC 带来的时延和 CPU 损耗。
可靠性
- 链路有效性检测:周期性心跳;
- 内存保护机制;
- 优雅停机:优雅停机功能指的是当系统退出时,JVM 通过注册的关闭钩子拦截到退出信号,然后执行退出操作,释放相关模块的资源占用,将缓冲区的消息处理完成或者清空,将待刷新的数据持久化到磁盘或者数据库中,等到资源回收和缓冲区消息处理完成之后,再退出。
可定制性
- 责任链模式:
ChannelPipeline基于责任链模式开发,便于业务逻辑的拦截、定制和扩展; - 基于接口的开发:关键的类库都提供了接口或者抽象类,如果 Netty 自身的实现无法满足用户的需求,可以由用户自定义实现相关接口;
- 工厂类支持:提供了大量工厂类,通过重载这些工厂类可以按需创建用户实现的对象;
- 系统参数:提供了大量的系统参数供系统按需设置,增强系统的场景定制性。
可扩展性
基于 Netty 的基础 NIO 框架,可以方便的进行应用层协议定制,例如 HTTP 协议栈、Thrift 协议栈、FTP 协议栈。这些扩展不需要修改 Netty 的源码,直接基于 Netty 的二进制类库即可实现协议的扩展和定制。目前,业界大量存在的基于 Netty 框架开发的协议,例如基于 Netty 的 HTTP 协议、Dubbo 协议、RocketMQ 内部私有协议等。
说明:本文内容主要基于 Netty 4.x 版本特性整理,部分设计模式与原理在后续版本中依然适用,具体 API 使用请参考官方最新文档。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/netty-quan-wei-zhi-nan.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。