一、整体结构

Netty 是一个 NIO 客户端服务器框架,可快速轻松地开发网络应用程序,例如协议服务器和客户端。它极大地简化和简化了网络编程,例如 TCP 和 UDP 套接字服务器。“快速简便”并不意味着最终的应用程序将遭受可维护性或性能问题的困扰。Netty 经过精心设计,结合了许多协议(例如FTP,SMTP,HTTP 以及各种基于二进制和文本的旧式协议)的实施经验。结果,Netty 成功地找到了一种无需妥协即可轻松实现开发,性能,稳定性和灵活性的方法。

Netty 是一个设计非常用心的网络基础组件,结构一共分为三个模块:

netty简易架构图_数据

  • Core 核心层
    提供了底层网络通信的通用抽象和实现,包括可扩展的事件模型、通用的通信 API、支持零拷贝的 ByteBuf 等。
  • Protocol Support 协议支持层
    覆盖了主流协议的编解码实现,如HTTP、SSL、Protobuf、压缩、大文件传输、WebSocket、文本、二进制等主流协议,此外 Netty 还支持自定义应用层协议。
  • Transport Service 传输服务层
    提供了网络传输能力的定义和实现方法,支持 Socket、HTTP 隧道、虚拟机管道等传输方式。Netty 对 TCP、UDP 等数据传输做了抽象和封装。具备较高的通用性和可扩展性,它不仅是一个优秀的网络框架,还可以作为网络编程的工具箱。

核心逻辑架构

netty简易架构图_tcp/ip_02

  • Reactor 通信调度层:由一系列辅助类组成,包括 Reactor 线程 NioEventLoop 及其父类,NioSocketChannel 和 NioServerSocketChannel等等。该层的职责就是监听网络的读写和连接操作,负责将网络层的数据读到内存缓冲区,然后触发各自网络事件,例如连接创建、连接激活、读事件、写事件等。将这些事件触发到 pipeline 中,由 pipeline 管理的职责链来进行后续的处理。
  • 职责链 ChannelPipeline:负责事件在职责链中的有序传播,以及负责动态地编排职责链。职责链可以选择监听和处理自己关心的事件,拦截处理和向后传播事件。
  • 业务逻辑编排层:业务逻辑编排层通常有两类,一类是纯粹的业务逻辑编排,一类是应用层协议插件,用于特定协议相关的会话和链路管理。由于应用层协议栈往往是开发一次到处运行,并且变动较小,故而将应用协议到 POJO 的转变和上层业务放到不同的 ChannelHandler 中,就可以实现协议层和业务逻辑层的隔离,实现架构层面的分层隔离。

二、逻辑架构

netty简易架构图_http_03

  1. 网络通信层

核心组件包含BootStrap、ServerBootStrap、Channel三个组件

  • BootStrap & ServerBootStrap

Bootstrap 是“引导”的意思,它主要负责整个 Netty 程序的启动、初始化、服务器连接等过程,它相当于一条主线,串联了 Netty 的其他核心组件。
Netty 中的引导器共分为两种类型:

  • 一个为用于客户端引导的 Bootstrap
  • 另一个为用于服务端引导的ServerBootStrap,它们都继承自抽象类 AbstractBootstrap。

Bootstrap与ServerBootstrap 的区别?

  • Bootstrap 可用于连接远端服务器,只绑定一个 EventLoopGroup
  • ServerBootStrap则用于服务端启动绑定本地端口,会绑定两个 EventLoopGroup,这两个 EventLoopGroup 通常称为 Boss 和 Worker。这里的 Boss 和 Worker 可以理解为“老板”和“员工”的关系。每个服务器中都会有一个 Boss,也会有一群做事情的Worker。Boss 会不停地接收新的连接,然后将连接分配给一个个 Worker 处理连接。
  • Channel

Channel的字面意思是“通道”,是网络通信的载体,提供了基本的 API 用于网络 I/O 操作,如 register、bind、connect、read、write、flush 等

  • Netty实现的 Channel 是以 JDK NIO Channel 为基础的
  • 相比较于 JDK NIO,Netty 的 Channel 提供了更高层次的抽象,同时屏蔽了底层 Socket 的复杂性,赋予了 Channel 更加强大的功能

常用分类:

  • NioServerSocketChannel 异步 TCP 服务端
  • NioSocketChannel 异步 TCP 客户端
  • OioServerSocketChannel 同步 TCP 服务端
  • OioSocketChannel 同步 TCP 客户端
  • NioDatagramChannel 异步 UDP 连接
  • OioDatagramChannel 同步 UDP 连接

基本状态:

  • 连接建立
  • 连接注册
  • 数据读写
  • 连接销毁

常见事件:

  1. channelRegistered:Channel 创建后被注册到 EventLoop 上
  2. channelUnregistered:Channel 创建后未注册或者从 EventLoop 取消注册
  3. channelActive:Channel 处于就绪状态,可以被读写
  4. channelInactive:Channel 处于非就绪状态
  5. channelRead:Channel 可以从远端读取到数据
  6. channelReadComplete:Channel 读取数据完成
  1. 事件调度层

事件调度层的职责是通过 Reactor 线程模型对各类事件进行聚合处理,通过 Selector 主循环线程集成多种事件( I/O 事件、信号事件、定时事件等),实际的业务处理逻辑是交由服务编排层中相关的 Handler 完成。

  • EventLoopGroup & EventLoop
  1. 一个 EventLoopGroup 往往包含一个或者多个 EventLoop。EventLoop 用于处理 Channel 生命周期内的所有 I/O 事件,如 accept、connect、read、write 等 I/O 事件。
  2. EventLoop 同一时间会与一个线程绑定,每个 EventLoop 负责处理多个 Channel
  3. 每新建一个 Channel,EventLoopGroup 会选择一个 EventLoop 与其绑定。该 Channel 在生命周期内都可以对 EventLoop 进行多次绑定和解绑。

EventLoop 则是 EventLoopGroup 的子接口,可以将其看做只有一个EventLoop的Group.

  • NioEventLoopGroup 是 Netty 中最被推荐使用的线程模型,基于 NIO 模型开发,每个线程负责处理多个Channel,而同一个 Channel 只会对应一个线程。

EventLoopGroup 是 Netty Reactor 线程模型的具体实现方式。

  • Reactor 的三种线程模型
  1. 单线程模型:EventLoopGroup 只包含一个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup
  2. 多线程模型:EventLoopGroup 包含多个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup;
  3. 主从多线程模型:EventLoopGroup 包含多个 EventLoop,Boss 是主 Reactor,Worker 是从Reactor,它们分别使用不同的 EventLoopGroup,主 Reactor 负责新的网络连接 Channel 创建,然后把 Channel 注册到从 Reactor。
  1. 服务编排层
    负责组装各类服务,它是 Netty 的核心处理链,用以实现网络事件的动态编排和有序传播。核心组件包括 ChannelPipeline、ChannelHandler、ChannelHandlerContext。
  • ChannelPipeline

负责组装各种 ChannelHandler,可以理解为ChannelHandler 的实例列表——内部通过双向链表将不同的 ChannelHandler 链接在一起。当 I/O 读写事件触发时,ChannelPipeline 会依次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理。ChannelPipeline 是线程安全的,因为每一个新的 Channel 都会对应绑定一个新的 ChannelPipeline。一个 ChannelPipeline 关联一个 EventLoop,一个 EventLoop 仅会绑定一个线程。

netty简易架构图_udp_04

ChannelPipeline 中包含入站 ChannelInboundHandler 和出站 ChannelOutboundHandler 两种处理器。客户端和服务端都有各自的 ChannelPipeline。以客户端为例,数据从客户端发向服务端,该过程称为出站,反之则称为入站。数据入站会由一系列 InBoundHandler 处理,然后再以相反方向的 OutBoundHandler 处理后完成出站。我们经常使用的编码 Encoder 是出站操作,解码 Decoder 是入站操作。服务端接收到客户端数据后,需要先经过 Decoder 入站处理后,再通过 Encoder 出站通知客户端。所以客户端和服务端一次完整的请求应答过程可以分为三个步骤:客户端出站(请求数据)、服务端入站(解析数据并执行业务逻辑)、服务端出站(响应结果)。

netty简易架构图_数据_05

  • ChannelHandler&ChannelHandlerContext

每创建一个 Channel 都会绑定一个新的 ChannelPipeline,ChannelPipeline 中每加入一个ChannelHandler 都会绑定一个 ChannelHandlerContext。ChannelHandlerContext 用于保存
ChannelHandler 上下文。

通过 ChannelHandlerContext 我们可以知道 ChannelPipeline 和 ChannelHandler 的关联关系。ChannelHandlerContext 可以实现 ChannelHandler 之间的交互,ChannelHandlerContext 包含了 ChannelHandler 生命周期的所有事件,如 connect、bind、read、flush、write、close 等。

三、组件关系梳理

netty简易架构图_tcp/ip_06

  1. 服务端启动初始化时有 Boss EventLoopGroup 和 Worker EventLoopGroup 两个组件,其中 Boss 负责监听网络连接事件。当有新的网络连接事件到达时,则将 Channel 注册到 Worker EventLoopGroup。
  2. Worker EventLoopGroup 会被分配一个 EventLoop 负责处理该 Channel 的读写事件。每个 EventLoop 都是单线程的,通过 Selector 进行事件循环。
  3. 当客户端发起 I/O 读写事件时,服务端 EventLoop 会进行数据的读取,然后通过 Pipeline 触发各种监听器进行数据的加工处理。
  4. 客户端数据会被传递到 ChannelPipeline 的第一个 ChannelInboundHandler 中,数据处理完成后,将加工完成的数据传递给下一个 ChannelInboundHandler。
  5. 当数据写回客户端时,会将处理结果在 ChannelPipeline 的 ChannelOutboundHandler 中传播,最后到达客户端。

四、Netty 的数据容器(ByteBuf)

前面介绍了 Netty 的几个核心组件,服务器在数据传输的时候,产生事件,并且对事件进行监控和处理。接下来看看数据是如何存放以及是如何读写的。Netty 将 ByteBuf 作为数据容器,来存放数据。

  1. ByteBuf 工作原理

从结构上来说,ByteBuf 由一串字节数组构成。数组中每个字节用来存放信息。ByteBuf 提供了两个索引,一个用于读取数据,一个用于写入数据。这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置。当从 ByteBuf 读取时,它的 readerIndex(读索引)将会根据读取的字节数递增。同样,当写 ByteBuf 时,它的 writerIndex 也会根据写入的字节数进行递增。ByteBuf 读写索引图例

netty简易架构图_http_07

需要注意的是极限的情况是 readerIndex 刚好读到了 writerIndex 写入的地方。如果 readerIndex 超过了 writerIndex 的时候,Netty 会抛出 IndexOutOf-BoundsException 异常

  1. ByteBuf 使用模式

谈了 ByteBuf 的工作原理以后,再来看看它的使用模式。

根据存放缓冲区的不同分为三类:

  • 堆缓冲区,ByteBuf 将数据存储在 JVM 的堆中,通过数组实现,可以做到快速分配。 由于在堆上被 JVM管理,在不被使用时可以快速释放。可以通过 ByteBuf.array() 来获取 byte[] 数据。
  • 直接缓冲区,在 JVM的堆之外直接分配内存,用来存储数据。其不占用堆空间,使用时需要考虑内存容量。 它在使用 Socket传递时性能较好,因为间接从缓冲区发送数据,在发送之前 JVM 会先将数据复制到直接缓冲区再进行发送。由于,直接缓冲区的数据分配在堆之外,通过 JVM 进行垃圾回收,并且分配时也需要做复制的操作,因此使用成本较高。
  • 复合缓冲区,顾名思义就是将上述两类缓冲区聚合在一起。Netty 提供了一个CompsiteByteBuf,可以将堆缓冲区和直接缓冲区的数据放在一起,让使用更加方便。
  1. ByteBuf 的分配

聊完了结构和使用模式,再来看看 ByteBuf 是如何分配缓冲区的数据的。

Netty 提供了两种 ByteBufAllocator 的实现,他们分别是:

  • PooledByteBufAllocator,实现了 ByteBuf 的对象的池化,提高性能减少内存碎片
  • Unpooled-ByteBufAllocator,没有实现对象的池化,每次会生成新的对象实例。

对象池化的技术和线程池,比较相似,主要目的是提高内存的使用率。池化的简单实现思路,是在 JVM 堆内存上构建一层内存池,通过 allocate 方法获取内存池中的空间,通过 release 方法将空间归还给内存池。对象的生成和销毁,会大量地调用 allocate 和 release 方法,因此内存池面临碎片空间回收的问题,在频繁申请和释放空间后,内存池需要保证连续的内存空间,用于对象的分配。

五、源码结构

1。 核心层模块

  • netty-common
    是 Netty 的核心基础包,提供了丰富的工具类
  • 通用工具类:比如定时器工具 TimerTask、时间轮 HashedWheelTimer 等。
  • 自定义并发包:比如异步模型Future & Promise、相比 JDK 增强的 FastThreadLocal 等。
  • netty-buffer
    实现了的一个更加完备的ByteBuf 工具类,用于网络通信中的数据载体
  • netty-resover
    提供了一些有关基础设施的解析工具,包括 IP Address、Hostname、DNS 等。
  1. Protocol Support 协议支持层模块
  • netty-codec
    负责编解码工作,通过编解码实现原始字节数据与业务实体对象之间的相互转化。提供了抽象的编解码类 ByteToMessageDecoder 和 MessageToByteEncoder,通过继承这两个类我们可以轻松实现自定义的编解码逻辑。
  • netty-handler
    netty-handler 模块提供了开箱即用的 ChannelHandler 实现类,例如日志、IP 过滤、流量整形等,如果需要这些功能,仅需在 pipeline 中加入相应的 ChannelHandler 即可
  1. Transport Service 传输服务层模块
  • netty-transport
    Netty 提供数据处理和传输的核心模块,提供了很多非常重要的接口,如 Bootstrap、Channel、ChannelHandler、EventLoop、EventLoopGroup、ChannelPipeline 等。其中 Bootstrap 负责客户端或服务端的启动工作,包括创建、初始化 Channel 等;EventLoop 负责向注册的 Channel 发起 I/O 读写操作;ChannelPipeline 负责 ChannelHandler 的有序编排

六、总结

从整体结构、逻辑架构以及源码结构对 Netty 的整体架构进行了初步介绍,可见 Netty 的分层架构设计非常合理,实现了各层之间的逻辑解耦,对于开发者来说,只需要扩展业务逻辑即可。