一、整体结构
Netty 是一个 NIO 客户端服务器框架,可快速轻松地开发网络应用程序,例如协议服务器和客户端。它极大地简化和简化了网络编程,例如 TCP 和 UDP 套接字服务器。
“快速简便”并不意味着最终的应用程序将遭受可维护性或性能问题的困扰。Netty 经过精心设计,结合了许多协议(例如FTP,SMTP,HTTP 以及各种基于二进制和文本的旧式协议)的实施经验。
结果,Netty 成功地找到了一种无需妥协即可轻松实现开发,性能,稳定性和灵活性的方法。
Netty 是一个设计非常用心的网络基础组件,结构一共分为三个模块:
- Core 核心层
提供了底层网络通信的通用抽象和实现,包括可扩展的事件模型、通用的通信 API、支持零拷贝的 ByteBuf
等。 - Protocol Support 协议支持层
覆盖了主流协议的编解码实现
,如HTTP、SSL、Protobuf、压缩、大文件传输、WebSocket、文本、二进制等主流协议,此外 Netty 还支持自定义应用层协议。
- Transport Service 传输服务层
提供了网络传输能力的定义和实现方法,支持 Socket、HTTP 隧道、虚拟机管道等传输方式
。Netty 对 TCP、UDP 等数据传输做了抽象和封装。具备较高的通用性和可扩展性,它不仅是一个优秀的网络框架,还可以作为网络编程的工具箱。
核心逻辑架构
- Reactor 通信调度层:
由一系列辅助类组成,包括 Reactor 线程 NioEventLoop 及其父类,NioSocketChannel 和 NioServerSocketChannel等等。
该层的职责就是监听网络的读写和连接操作,负责将网络层的数据读到内存缓冲区,然后触发各自网络事件,例如连接创建、连接激活、读事件、写事件等。将这些事件触发到 pipeline 中,由 pipeline 管理的职责链来进行后续的处理。 - 职责链 ChannelPipeline:
负责事件在职责链中的有序传播,以及负责动态地编排职责链。
职责链可以选择监听和处理自己关心的事件,拦截处理和向后传播事件。 - 业务逻辑编排层:
业务逻辑编排层通常有两类,一类是纯粹的业务逻辑编排,一类是应用层协议插件,用于特定协议相关的会话和链路管理。
由于应用层协议栈往往是开发一次到处运行,并且变动较小,故而将应用协议到 POJO 的转变和上层业务放到不同的 ChannelHandler 中,就可以实现协议层和业务逻辑层的隔离,实现架构层面的分层隔离。
二、逻辑架构
- 网络通信层
核心组件包含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 连接
基本状态:
- 连接建立
- 连接注册
- 数据读写
- 连接销毁
常见事件:
- channelRegistered:Channel 创建后被注册到 EventLoop 上
- channelUnregistered:Channel 创建后未注册或者从 EventLoop 取消注册
- channelActive:Channel 处于就绪状态,可以被读写
- channelInactive:Channel 处于非就绪状态
- channelRead:Channel 可以从远端读取到数据
- channelReadComplete:Channel 读取数据完成
- 事件调度层
事件调度层的职责是通过 Reactor 线程模型对各类事件进行聚合处理,通过 Selector 主循环线程集成多种事件( I/O 事件、信号事件、定时事件等)
,实际的业务处理逻辑是交由服务编排层中相关的 Handler 完成。
- EventLoopGroup & EventLoop
一个 EventLoopGroup 往往包含一个或者多个 EventLoop
。EventLoop用于处理 Channel 生命周期内的所有 I/O 事件
,如 accept、connect、read、write 等 I/O 事件。EventLoop 同一时间会与一个线程绑定,每个 EventLoop 负责处理多个 Channel
。每新建一个 Channel,EventLoopGroup 会选择一个 EventLoop 与其绑定。该 Channel 在生命周期内都可以对 EventLoop 进行多次绑定和解绑。
EventLoop 则是 EventLoopGroup 的子接口,可以将其看做只有一个EventLoop的Group.
NioEventLoopGroup 是 Netty 中最被推荐使用的线程模型
,基于 NIO 模型开发,每个线程负责处理多个Channel,而同一个 Channel 只会对应一个线程。
EventLoopGroup 是 Netty Reactor 线程模型的具体实现方式。
- Reactor 的三种线程模型
- 单线程模型:EventLoopGroup
只包含一个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup
;- 多线程模型:EventLoopGroup
包含多个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup;
- 主从多线程模型:EventLoopGroup
包含多个 EventLoop,Boss 是主 Reactor,Worker 是从Reactor,它们分别使用不同的 EventLoopGroup
,主 Reactor 负责新的网络连接 Channel 创建,然后把 Channel 注册到从 Reactor。
- 服务编排层
负责组装各类服务,它是 Netty 的核心处理链,用以实现网络事件的动态编排和有序传播
。核心组件包括ChannelPipeline、ChannelHandler、ChannelHandlerContext。
- ChannelPipeline
负责组装各种 ChannelHandler,可以理解为ChannelHandler 的实例列表
——内部通过双向链表将不同的 ChannelHandler 链接在一起
。当 I/O 读写事件触发时,ChannelPipeline 会依次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理
。ChannelPipeline 是线程安全的,因为每一个新的 Channel 都会对应绑定一个新的 ChannelPipeline。一个 ChannelPipeline 关联一个 EventLoop,一个 EventLoop 仅会绑定一个线程。
ChannelPipeline 中包含入站 ChannelInboundHandler 和出站 ChannelOutboundHandler 两种处理器。
客户端和服务端都有各自的 ChannelPipeline
。以客户端为例,数据从客户端发向服务端,该过程称为出站,反之则称为入站。
数据入站会由一系列 InBoundHandler 处理,然后再以相反方向的 OutBoundHandler 处理后完成出站。我们经常使用的编码 Encoder 是出站操作,解码 Decoder 是入站操作。服务端接收到客户端数据后,需要先经过 Decoder 入站处理后,再通过 Encoder 出站通知客户端。
所以客户端和服务端一次完整的请求应答过程可以分为三个步骤:客户端出站(请求数据)、服务端入站(解析数据并执行业务逻辑)、服务端出站(响应结果)。
- ChannelHandler&ChannelHandlerContext
每创建
一个 Channel 都会绑定一个新的 ChannelPipeline
,ChannelPipeline 中每加入一个ChannelHandler 都会绑定一个 ChannelHandlerContext
。ChannelHandlerContext 用于保存
ChannelHandler 上下文。
通过 ChannelHandlerContext 我们可以知道 ChannelPipeline 和 ChannelHandler 的关联关系。ChannelHandlerContext 可以实现 ChannelHandler 之间的交互,ChannelHandlerContext 包含了 ChannelHandler 生命周期的所有事件,如 connect、bind、read、flush、write、close 等。
三、组件关系梳理
- 服务端启动初始化时有 Boss EventLoopGroup 和 Worker EventLoopGroup 两个组件,其中 Boss 负责监听网络连接事件。当有新的网络连接事件到达时,则将 Channel 注册到 Worker EventLoopGroup。
- Worker EventLoopGroup 会被分配一个 EventLoop 负责处理该 Channel 的读写事件。每个 EventLoop 都是单线程的,通过 Selector 进行事件循环。
- 当客户端发起 I/O 读写事件时,服务端 EventLoop 会进行数据的读取,然后通过 Pipeline 触发各种监听器进行数据的加工处理。
- 客户端数据会被传递到 ChannelPipeline 的第一个 ChannelInboundHandler 中,数据处理完成后,将加工完成的数据传递给下一个 ChannelInboundHandler。
- 当数据写回客户端时,会将处理结果在 ChannelPipeline 的 ChannelOutboundHandler 中传播,最后到达客户端。
四、Netty 的数据容器(ByteBuf)
前面介绍了 Netty 的几个核心组件,服务器在数据传输的时候,产生事件,并且对事件进行监控和处理。接下来看看数据是如何存放以及是如何读写的。Netty 将 ByteBuf 作为数据容器,来存放数据。
- ByteBuf 工作原理
从结构上来说,ByteBuf 由一串字节数组构成。数组中每个字节用来存放信息
。ByteBuf 提供了两个索引,一个用于读取数据,一个用于写入数据
。这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置。当从 ByteBuf 读取时,它的 readerIndex(读索引)将会根据读取的字节数递增。同样,当写 ByteBuf 时,它的 writerIndex 也会根据写入的字节数进行递增。
ByteBuf 读写索引图例
需要注意的是极限的情况是 readerIndex 刚好读到了 writerIndex 写入的地方。如果 readerIndex 超过了 writerIndex 的时候,Netty 会抛出 IndexOutOf-BoundsException 异常
。
- ByteBuf 使用模式
谈了 ByteBuf 的工作原理以后,再来看看它的使用模式。
根据存放缓冲区的不同分为三类:
堆缓冲区
,ByteBuf将数据存储在 JVM 的堆中
,通过数组实现,可以做到快速分配。 由于在堆上被 JVM管理,在不被使用时可以快速释放
。可以通过 ByteBuf.array() 来获取 byte[] 数据。直接缓冲区
,在 JVM的堆之外直接分配内存,用来存储数据。其不占用堆空间,使用时需要考虑内存容量
。 它在使用 Socket传递时性能较好,因为间接从缓冲区发送数据,在发送之前 JVM 会先将数据复制到直接缓冲区再进行发送。
由于,直接缓冲区的数据分配在堆之外,通过 JVM 进行垃圾回收,并且分配时也需要做复制的操作,因此使用成本较高。
复合缓冲区
,顾名思义就是将上述两类缓冲区聚合在一起。Netty 提供了一个CompsiteByteBuf,可以将堆缓冲区和直接缓冲区的数据放在一起,让使用更加方便。
- 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 等。
- Protocol Support 协议支持层模块
- netty-codec
负责编解码工作,通过编解码实现原始字节数据与业务实体对象之间的相互转化
。提供了抽象的编解码类 ByteToMessageDecoder 和 MessageToByteEncoder,通过继承这两个类我们可以轻松实现自定义的编解码逻辑。 - netty-handler
netty-handler 模块提供了开箱即用的 ChannelHandler 实现类,例如日志、IP 过滤、流量整形等,如果需要这些功能,仅需在 pipeline 中加入相应的 ChannelHandler 即可
。
- Transport Service 传输服务层模块
- netty-transport
Netty 提供数据处理和传输的核心模块,提供了很多非常重要的接口
,如 Bootstrap、Channel、ChannelHandler、EventLoop、EventLoopGroup、ChannelPipeline 等。其中 Bootstrap 负责客户端或服务端的启动工作,包括创建、初始化 Channel 等;EventLoop 负责向注册的 Channel 发起 I/O 读写操作;ChannelPipeline 负责 ChannelHandler 的有序编排
。
六、总结
从整体结构、逻辑架构以及源码结构对 Netty 的整体架构进行了初步介绍,可见 Netty 的分层架构设计非常合理,实现了各层之间的逻辑解耦,对于开发者来说,只需要扩展业务逻辑即可。