这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战
首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…
一 . 前言
文档目的 :
- 大纲型文档 , 持续更新
- 梳理 Netty 的主要概念
- 梳理 Netty 的核心类
- 对 Netty 主要结构有一个基础的认知
后续会进行深入的源码分析 , 作为一个快查手册 ,将会逐步完善该文档
1.1 概念
Netty 是一个基于 JAVA NIO 类库的异步通信框架,用于创建异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性的网络客户端和服务器端
下面依次对这些概念做一个解析 :
1 | java复制代码// JAVA NIO 类库 |
1.2 Netty 模型
Netty 的线程模型是基于 Reactor 模式的一个实现 , 那么就首先要了解什么是 Reactor .
I/O 多路复用模式通常有2种实现 :
- Reactor : 采用同步IO
- Proactor : 采用异步IO
在Reactor中,当读写操作准备就绪后 ,将会就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作
Reactor 中有三种线程模型 :
- 单线程模型 : 所有的IO操作都在同一个NIO线程上面完成
- 多线程模型 : 有一组NIO线程处理IO操作
- 主从多线程模型 : 服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池
PS : 注意 , Netty 采用的是主从多线程模型 , 这个后面再专门深入
主从多线程的角色 :
- Main Reactor : 主要负责连接事件,并将IO读写请求转发到 SubReactor 线程池
- Acceptor : 不真正负责连接请求的建立,而只将其请求委托 Main Reactor 线程池来实现 , 类似于转发器
- Sub Reactor : Main Reactor将通道的读写转发到 Sub Reactor 线程池中一个线程(负载均衡),负责数据的读写
主从多线程主要流程 :
- MainReactor 监听 Server Socket
- 客户端连接 (OP_ACCEPT 事件)
- MainReactor 将通道的读写转发给 Sub Reactor
- 创建 NioSocketChannel对象 , 从SubReactor内挑选一个线程 , 发起 OP_READ事件
- 服务端通过 NioSocketChannel 从网卡中读取数据
后面的具体流程这一篇就不深入了
1.3 Netty 的功能特点
- 异步、非阻塞、基于事件驱动的NIO框架
- 支持多种传输层通信协议,包括TCP、UDP等
- 开发异步HTTP服务端和客户端应用程序
- 提供对多种应用层协议的支持,包括TCP私有协议、HTTP协议、WebSocket协议、文件传输等
- 默认提供多种编解码能力,包括Java序列化、Google的ProtoBuf、二进制编解码、Jboss marshalling、文本字符串、base64、简单XML等,这些编解码框架可以被用户直接使用
- 提供形式多样的编解码基础类库,可以非常方便的实现私有协议栈编解码框架的二次定制和开发
- 经典的ChannelFuture-listener机制,所有的异步IO操作都可以设置listener进行监听和获取操作结果
- 基于ChannelPipeline-ChannelHandler的责任链模式,可以方便的自定义业务拦截器用于业务逻辑定制
- 安全性:支持SSL、HTTPS
- 可靠性:流量整形、读写超时控制机制、缓冲区最大容量限制、资源的优雅释放等
- 简洁的API和启动辅助类,简化开发难度,减少代码量
Netty 中的常见事件
Netty 使用事件驱动的应用程序范例,因此数据处理 pipeline 是经过处理程序的事件链 , 入站事件可以是 :
- Channel activation and deactivation 通道激活和失活
- Read operation events 阅读操作事件
- Exception events 异常事件
- User events 用户事件
1.4 业务分层
第一层 : Reactor 通信调度层
由一系列辅助类组成,包括 Reactor 线程NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父类、ByteBuffer 以及由其衍生出来的各种 Buffer、Unsafe 以及其衍生出的各种内部子类等
第二层 :职责链 ChannelPipeLine
负责调度事件在职责链中的传播,支持动态的编排职责链,职责链可以选择性的拦截自己关心的事件,对于其它IO操作和事件忽略,Handler同时支持inbound和outbound事件
第三层 : 业务逻辑编排层
业务逻辑编排层通常有两类:一类是纯粹的业务逻辑编排,还有一类是应用层协议插件,用于协议相关的编解码和链路管理,例如 CMPP 协议插件
1.5 项目结构
项目结构
- common : 通用的工具类项目
- buffer : 对应 Zero-Copy-Capable Rich Byte Buffer , 自行实现的一个 Byte Buffer 字节缓冲区
- transport : 主要是 Transport Services、Universal Communication API 和 Extensible Event Model , 该项目是网络传输通道的抽象和实现
- codec : 对应 Protocol Support , 该项目是协议编解码的抽象与部分实现
- handler : 该项目是提供内置的连接通道处理器( ChannelHandler )实现类
- example : 该项目是提供各种 Netty 使用示例
1.6 功能特点
零拷贝
零拷贝 : CPU不需要为数据在内存之间的拷贝消耗资源 ,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式
1 | java复制代码Netty : |
内存池
Netty提供了多种内存管理策略,通过在启动辅助类中配置相关参数,可以实现差异化的定制
采用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右
Netty提供四种ByteBuf :
- 基于内存池可重复利用的非堆内存:PooledDirectByteBuf
- 基于内存池可重复利用的堆内存:PooledHeapByteBuf
- 朝生夕灭的非堆内存:UnpooledDirectByteBuf
- 朝生夕灭的堆内存:UnpooledHeapByteBuf
二 . Netty 基本功能点
2.1 核心组件
三个NIO核心概念 :
- Channel : 频道
- Buffer : 缓冲区
- Selector : 多路选择器
Netty 核心启动组件 :
- AbstractBootstrap
- Bootstrap & ServerBootstrap
- Channel : 它代表一个到实体的开放连接,如读操作和写操作 , 可以把Channel 看作是传入(入站)或者传出(出站)数据的载体
- ChannelFuture : 通过该接口的 #addListener(…) 方法,注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果
- EventLoop & EventLoopGroup
- ChannelHandler : 连接通道处理器 , ChannelHandler 主要用来处理各种事件
- ChannelInboundHandler 的实现类还包括一系列的 Decoder 类,对输入字节流进行解码。
- ChannelOutboundHandler 的实现类还包括一系列的 Encoder 类,对输入字节流进行编码。
- ChannelPipeline : ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API
组件关联 :
功能详述
1 | java复制代码// EventLoop & EventLoopGroup |
三 . Netty 主要的成员
以下来看一下 Netty 的主要成员 , 主要围绕三个维度 :
- Channel : Socket;
- EventLoop : 控制流、多线程处理、并发;
- ChannelFuture : 异步通知。
关联关系 @ Netty 实战
- 一个EventLoopGroup 包含一个或者多个EventLoop;
- 一个EventLoop 在它的生命周期内只和一个Thread 绑定;
- 所有由EventLoop 处理的I/O 事件都将在它专有的Thread 上被处理;
- 一个Channel 在它的生命周期内只注册于一个EventLoop;
- 一个EventLoop 可能会被分配给一个或多个Channel。
组件关系
3.1 EventLoop
作用 : EventLoop 是 Netty 基于 Reactor 模型的思想进行实现 , EventLoop 定义了Netty 的核心抽象,用于处理连接的生命周期中所发生的事件
EventLoop 处理所有注册到本线程多路复用器 Selector上的Channel, Selector的轮询操作由绑定的EventLoop线程run方法驱动, 在一个循环体内循环执行
3.2 ChannelPipeline
作用 : 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。
流程 : 在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler
1 | java复制代码// 处理流程 : |
3. ChannelHandler
作用 : Netty 的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器
初始化 : 在应用程序的初始化或者引导阶段被注册
流程 : 接收事件、执行它们所实现的处理逻辑 , 并将数据传递给链中的下一个ChannelHandler
排序 : 它们的执行顺序是由它们被添加的顺序所决定的
流程
入站和出站ChannelHandler 可以被安装到同一个ChannelPipeline中
如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline 的头部开始流动,并被传递给第一个ChannelInboundHandler。这个ChannelHandler 不一定会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。
最终,数据将会到达ChannelPipeline 的尾端,届时,所有处理就都结束了。
数据的出站运动(即正在被写的数据)在概念上也是一样的。
在这种情况下,数据将从 ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站
数据将会到达网络传输层,通常情况下,会触发一个写操作。
ChannelHandler 的容器
当ChannelHandler 被添加到ChannelPipeline 时,它将会被分配一个ChannelHandlerContext,其代表了ChannelHandler 和ChannelPipeline 之间的绑定
在Netty 中,有两种发送消息的方式。可以直接写到Channel 中,也可以写到和ChannelHandler相关联的ChannelHandlerContext 对象中。
- 前一种方式将会导致消息从Channel-Pipeline 的尾端开始流动
- 后者将导致消息从ChannelPipeline 中的下一个Channel-Handler 开始流动。
ChannelInboundHandler 和 ChannelOutboundHandler
Netty 定义了下面两个重要的ChannelHandler 子接口:
- ChannelInboundHandler——处理入站数据以及各种状态变化
- ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作
相关类
- C- ChannelInitializer : 特殊的ChannelInboundHandler 实现类,用于 Channel 注册到 EventLoop 后,执行自定义的初始化操作
- C- SimpleChannelInboundHandler : 处理指定类型的消息 , 实现 SimpleChannelInboundHandler 后,实现对指定类型的消息的自定义处理
- C- LoggerHandler
- C- IdleStateHandler : 当 Channel 的读或者写空闲时间太长时,将会触发一个 IdleStateEvent 事件。
- C- ReadTimeoutHandler : 当 Channel 的读空闲时间( 读或者写 )太长时,抛出 ReadTimeoutException 异常,并自动关闭该Channel
- C- WriteTimeoutHandler : 当一个写操作不能在指定时间内完成时,抛出 WriteTimeoutException 异常,并自动关闭对应 Channel
3. Channel
作用 : Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作
在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供
常见操作 : bind()、connect()、read()和write()
常见的实现类 :
- EmbeddedChannel
- LocalServerChannel
- NioDatagramChannel
- NioSctpChannel
- NioSocketChannel
3. ByteBuf
ByteBuf 是一个抽象类 , Java NIO 提供了ByteBuffer 作为它的字节容器 , 而 Netty 的ByteBuffer 替代品是ByteBuf , 既解决了JDK API 的局限性,
又为网络应用程序的开发者提供了更好的API。
ByteBuf 的特点
- 它可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 容量可以按需增长(类似于JDK 的StringBuilder)
- 在读和写这两种模式之间切换不需要调用ByteBuffer 的flip()方法
- 读和写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数;
- 支持池化
涉及组件
1 | java复制代码abstract class ByteBuf |
基础原理
ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从ByteBuf 读取时,它的readerIndex 将会被递增已经被读取的字节数。同样地,当你写入ByteBuf 时,它的 writerIndex 也会被递增。图5-1 展示了一个空ByteBuf 的布局结构和状态。
ByteBuf 使用模式
- 堆缓冲区 : 将数据存储在JVM 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放
- 直接缓冲区 : 允许JVM 实现通过本地调用来分配内存 , 直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外
- 复合缓冲区 : 为多个ByteBuf 提供一个聚合视图
- 派生缓冲区 : 派生缓冲区为ByteBuf 提供了以专门的方式来呈现其内容的视图
- Unpooled 缓冲区 : 提供了静态的辅助方法来创建未池化的ByteBuf实例
PS : 如果你的数据包含在一个在堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前,JVM将会在内部把你的缓冲区复制到一个直接缓冲区中
PS : 问题在于分配和释放都较为昂贵
ByteBufHolder
1 | java复制代码// ByteBuf 分配 |
ByteBufHolder 的主要方法 :
- content() : 返回由这个ByteBufHolder 所持有的ByteBuf
- copy() : 返回这个ByteBufHolder 的一个深拷贝,包括一个其所包含的ByteBuf 的非共享拷贝
- duplicate() : 返回这个ByteBufHolder 的一个浅拷贝,包括一个其所包含的ByteBuf 的共享拷贝
ByteBufAllocator
作用 : ByteBuf 的分配器,负责创建 ByteBuf 对象 , ByteBufAllocator 实现了池化 , 可以用来分配我们所描述过的任意类型的ByteBuf 实例
子类 :
- PreferHeapByteBufAllocator ,倾向创建 Heap ByteBuf 的分配器。
- PooledByteBufAllocator ,基于内存池的 ByteBuf 的分配器。
- UnpooledByteBufAllocator ,普通的 ByteBuf 的分配器。
主要方法 :
- buffer() : 返回一个基于堆或者直接内存存储的 ByteBuf
- heapBuffer() : 返回一个基于堆内存存储的 ByteBuf
- directBuffer() : 返回一个基于直接内存存储的 ByteBuf
- compositeBuffer() : 返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf
ByteBufUtil 类
作用 : 提供了用于操作ByteBuf 的静态的辅助方法
3. Jemalloc
作用 : Jemalloc 是内存管理算法
3. Decoder
正向处理 : 将 Byte 转换为 Message
1 | java复制代码Decoder 中最核心的对象是 ByteToMessageDecoder , 它可以将字节解码为消息(或者另一个字节序列) , 该对象提供了2个方法 : |
逆行处理 : 将 Message 转换为 Byte
1 | jaa复制代码该操作基于 MessageToByteEncoder , |
互相转换 : 在两个消息格式之间进行转换
1 | java复制代码MessageToMessageDecoder ,将消息解码成另一种消息。 |
未知转换, 我们需要将字节解码为某种形式的消息
1 | java复制代码• ByteToMessageCodec : ByteToMessageDecoder + MessageByteEncoder 的组合 |
1 | java复制代码所有由Netty 提供的编码器/解码器适配器类都实现了ChannelOutboundHandler 或者ChannelInboundHandler 接口 |
3. Util
1 | JAVA复制代码C- Future |
四 . Netty 核心引导类
1 | java复制代码// Bootstrap 类 |
4.1 Netty 服务端的创建流程
- 创建 ServerBootStrap 实例 , ServerBootStrap 是 服务端的启动辅助类
- 设置并绑定 Reactor 线程池 , Netty 的 Reactor 线程池 为 EventLoopGroup (EventLoop 数组)
- 设置并绑定服务端 Channel, Netty对原生的NIO类库进行了封装,对应实现是NioServerSocketChannel
- TCP 链路建立时创建 ChannelPipeline , 它本质就是一个负责处理网络事件的职责链, 负责管理和执行 ChannelHandler
- 添加并设置 ChannelHandler , 利用ChannelHandler用户可以完成大多数的 功能定制 , 例如消息编解码、 心跳、 安全认证、 TSL/SSL认证、 流量控制和流撮整形等
- 绑定监听端口并且启动服务端 , 在绑定监听端口之前系统会做一系列的初始化和检测工作,完成之后 ,会启动监听端口, 并将ServerSocketChannel注册到Selector上监听客户端连接
- Selector 轮询 , 由Reactor线程NioEventLoop负责调度和执行Selector轮询操作, 选择准备就绪的Channel其合
- 网络事件通知 , 由Reactor线程NioEventLoop执行 ChannelPipel ine的相应方法 , 最终调度并执行ChannelHandler
- 执行 Netty 系统和业务 ChannelHandler
4.2 Netty 客户端创建流程
- 用户线程创建 Bootstrap 实例, 通过 API 设置创建客户端相关的参数, 异步发起客户端连接
- 创建 NioEventLoopGroup 线程组, 用于 创建处理客户端连接 、I/O 读写的 Reactor
- 过 Bootstrap 的 ChannelFactory 和用户指定的 Channel 类型创建用于客户端连接的 NioSocketChannel
- 创建默认的ChannelHandlerPipeline,用于调度和执行网络事件
- 异步发起 TCP 连接, 判断连接是否成功。如果成功, 则直接将 NioSocketCbannel 注册到多路复用器上,监听读操作位, 用于数据报读取和消息发送 (如果没有立即连接成功, 则注册连接监听位到多路复用器, 等待连接结果)
- 注册对应的网络监听状态位到多路复用器
- 由多路复用器在I/O现场中轮询各Channel, 处理连接结果
- 如果连接成功 , 设置Future结果 , 发送连接成功事件,触发ChannelPipeline执行
- 由ChanneLPipeline调度执行系统和用户的ChannelHandler, 执行业务逻辑
@ Netty 权威指南
总结
这只是一个基础大纲 , 很多细节等后面代码深入分析后再逐步完善
参考文档
@ Introduction to Netty | Baeldung
@ HTTP Server with Netty | Baeldung
@ A Quick Guide to Java on Netty | Okta Developer
@ 主从多Reactor! - 知乎 (zhihu.com)
@ A Quick Guide to Java on Netty | Okta Developer
@ <Netty 实战>
@ <Netty 权威指南>
本文转载自: 掘金