ByteBuf 组件
当我们进行数据传输的时候,往往需要使用到缓冲区,常用的缓冲区就是 JDK NIO类库 提供的 Buffer组件,7 种基本数据类型 ( Boolean 除外 ) 都有自己的缓冲区实现。对于 NIO编程 而言,我们主要使用的是 ByteBuffer。从功能角度而言,ByteBuffer 完全可以满足 NIO编程 的需
要,但是由于 NIO编程 的复杂性,ByteBuffer 也有其局限性,它的主要缺点如下。
- ByteBuffer 长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的 POJO对象 大于 ByteBuffer 的容量时,会发生索引越界异常;
- ByteBuffer 只有一个标识位置的 指针position,读写的时候需要手工调用 flip() 和 rewind() 等,使用者必须小心谨慎地处理这些 API,否则很容易导致程序处理失败;
- ByteBuffer 的 API 功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。
为了弥补这些不足,Netty 提供了自己的 ByteBuffer实现,即 ByteBuf, 下面我们看一下 ByteBuf 的原理和主要功能。
ByteBuf 工作原理
首先,ByteBuf 依然是个 Byte数组 的缓冲区,它的基本功能应该与 JDK 的 ByteBuffer 一致,提供以下几类基本功能。
- 7 种 Java基本数据类型、byte 数组、ByteBuffer ( ByteBuf ) 等的读写;
- 缓冲区自身的 copy 和 slice 等;
- 设置网络字节序;
- 构造缓冲区实例;
- 操作位置指针等方法。
由于 JDK 的 ByteBuffer 已经提供了这些基础能力的实现,因此,Netty ByteBuf 的实现可以有两种策略。 - 参考 JDK ByteBuffer 的实现,增加额外的功能,解决原 ByteBuffer 的缺点;
- 聚合 JDK ByteBuffer,通过 Facade模式 对其进行包装,可以减少自身的代码量,降低实现成本。
JDK ByteBuffer 由于只有一个位置指针用于处理读写操作,因此每次读写的时候都需要额外调用 flip() 和 clear() 等方法。而 Netty 提供了两个指针变量用于支持 顺序读取 和 写入操作。readerIndex 用于标识读取索引,writerlndex 用于标识写入索引。两个位置指针将 ByteBuf缓冲区分割成三个区域,如下图所示。
调用 ByteBuf 的 read操作 时,从 readerIndex 处开始读取。readerIndex 到 writerIndex 之间的空间为可读的字节缓冲区;从 writerlndex 到 capacity 之间为可写的字节缓冲区;0 到 readerIndex 之间是已经读取过的缓冲区。
Channel 和 Unsafe组件
Channel 是 JDK 的 NIO类库 的一个重要组成部分,就是 java.nio.SocketChannel 和 java.nio.ServerSocketChannel,它们用于非阻塞的 IO操作。类似于 NIO 的 Channel,Netty 也提供了自己的 Channel 和其子类实现,用于异步 IO操作和其他相关的操作。
Unsafe 是个内部接口,聚合在 Channel 中协助进行网络读写相关的操作,因为它的设计初衷就是 Channel 的内部辅助类,不应该被 Netty框架 的上层使用者调用,所以被命名为 Unsafe。
Channel功能说明
io.netty.channel.Channel 是 Netty网络操作抽象类,它聚合了一组功能,包括但不限于网路的读、写,客户端发起连接,主动关闭连接,链路关闭,获取通信双方的网络地址等。它也包含了 Netty框架 相关的一些功能,包括获取该 Chanel 的 EventLoop,获取缓冲分配器 ByteBufAllocator 和 pipeline 等。
Channel工作原理
Channel 是 Netty 抽象出来的网络 I/O 读写相关的接口,为什么不使用 JDK NIO 原生的 Channel 而要另起炉灶呢,主要原因如下。
- JDK 的 SocketChannel 和 ServerSocketChannel 没有统一的 Channel接口 供业务开发者使用,对于用户而言,没有统一的操作视图,使用起来并不方便。
- JDK 的 SocketChannel 和 ServerSocketChannel 的主要职责就是 网络I/O操作,由于它们是 SPI类接口,由具体的虚拟机厂家来提供,所以通过继承 SPI功能类 来扩展其功能的难度很大;直接实现 ServerSocketChannel 和 SocketChannel 抽象类,其工作量和重新开发一个新的 Channel功能类 是差不多的。
- Netty 的 Channel 需要能够跟 Netty 的整体架构融合在一起,例如 I/O模型、基于 ChannelPipeline 的定制模型,以及基于元数据描述配置化的 TCP参数 等,对于这些功能,JDK 的 SocketChannel 和 ServerSocketChannel 都没有提供,需要重新封装。
- 自定义的 Channel,功能实现更加灵活。
基于上述 4 个原因,Netty 重新设计了 Channel接口,并且给予了很多不同的实现。它的设计原理比较简单,但是功能却比较繁杂,主要的设计理念如下。
- 在 Channel 接口层,采用 Facade模式 进行统一封装,将网络I/O操作、网络I/O相关联的其他操作 封装起来,统一对外提供。
- Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。
- 具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,由 Channel 统一负责分配和调度,功能实现更加灵活。
Unsafe功能说明
Unsafe接口 实际上是 Channel接口 的辅助接口,它不应该被用户代码直接调用。实际的I/O读写操作都是由Unsafe接口负责完成的。
ChannelPipeline 和 ChannelHandler 组件
Netty 的 ChannelPipeline 和 ChannelHandler 机制类似于 Servlet 和 Filter过滤器,这类拦截器实际上是职责链模式的一种变形,主要是为了方便事件的拦截和用户业务逻辑的定制。
Servlet 和 Filter 是 J2EE Web 应用程序级的 Java 代码组件,它能够以声明的方式插入到 HTTP请求响应 的处理过程中,用于拦截请求和响应,以便能够查看、提取或以某种方式操作正在客户端和服务器之间交换的数据。拦截器封装了业务定制逻辑,能够实现对 Web 应用程序的预处理和事后处理。
过滤器提供了一种面向对象的模块化机制,用来将公共任务封装到可插入的组件中。这些组件通过 Web 部署配置文件 ( web.xml ) 进行声明,可以方便地添加和删除过滤器,无须改动任何应用程序代码或 JSP页面,由 Servlet 进行动态调用。通过在请求/响应链中使用过滤器,可以对应用程序的 Servlet 或 JSP页面 提供的核心处理进行补充,而不破坏 Servlet 或 JSP页面 的功能。由于是纯Java实现,所以 Servlet 过滤器具有跨平台的可重用性,使得它们很容易被部署到任何符合 Servlet规范 的 J2EE环境 中。
Netty 的 Channel过滤器 实现原理与 Servlet Filter 机制一致,它将 Channel 的数据管道抽象为 ChannelPipeline,消息在 ChannelPipeline 中流动和传递。ChannelPipeline 持有 I/O事件拦截器 ChannelHandler 的链表,由 ChannelHandler 对 I/O事件 ( 读/写 ) 进行拦截和处理,可以方便地通过新增和删除 ChannelHandler 来实现不同的业务逻辑定制,不需要对已有的 ChannelHandler 进行修改,能够实现对修改封闭和对扩展的支持。
ChannelPipeline 组件
ChannelPipeline 是 ChannelHandler 的容器,它负责 ChannelHandler 的管理和事件拦截与调度。
下图展示了一个消息被 ChannelPipeline 的 ChannelHandler链 拦截和处理的全过程,消息的读取和发送处理全流程描述如下。
- 底层的 SocketChannel read() 方法读取 ByteBuf,触发 ChannelRead 事件,由 I/O线程 NioEventLoop 调用 ChannelPipeline 的 fireChannelRead(Object msg)方法,将消息(ByteBuf)传输到 ChannelPipeline 中。
- 消息依次被 Inbound Handler 链拦截和处理,在这个过程中,任何 ChannelHandler 都可以中断当前的流程,结束消息的传递。
- 调用 ChannelHandlerContext 的 write()方法 发送消息,消息从 Outbound Handler 1 开始,沿着拦截器链,最终被添加到消息发送缓冲区(ByteBuf)中等待刷新和发送。
Netty 中的事件分为 inbound事件 和 outbound事件。inbound事件 通常由 I/O线程 触发,例如 TCP链路建立事件、链路关闭事件、读事件、异常通知事件等。Outbound事件 通常是由用户主动发起的 网络I/O操作,例如用户发起的连接操作、绑定操作、消息发送等操作。
ChannelPipeline 通过 ChannelHandler 接口来实现事件的拦截和处理,由于 ChannelHandler 中的事件种类繁多,不同的 ChannelHandler 可能只需要关心其中的某一个或者几个事件,所以,通常 ChannelHandler 只需要继承 ChannelHandlerAdapter类 覆盖自己关心的方法即可。
ChannelHandler 组件
ChannelHandler 类似于 Servlet 的 Filter 过滤器,负责对 I/O事件 或者 I/O操作 进行拦截和处理,它可以选择性地拦截和处理自已感兴趣的事件,也可以终止事件的传递。基于 ChannelHandler接口,用户可以方便地进行业务逻辑定制,例如打印日志、统一封装异常信息、性能统计和消息编解码等。
EventLoop 和 EventLoopGroup
EventLoop 翻译过来就是 “事件循环” ,可以理解为:循环监听 所有 “已建立连接的链路” 的 IO事件,如:SelectionKey.OP_READ / OP_WRITE / OP_CONNECT / OP_ACCEPT。
从本章开始我们将学习 Netty 的线程模型。Netty框架 的主要线程就是 I/O线程,线程模型设计的好坏,决定了系统的吞吐量、并发性和安全性等架构质量属性。Netty的线程模型被精心地设计,既提升了框架的并发性能,又能在很大程度避免锁,局部实现了无锁化设计。下面我们来看一下 Netty 的线程模型,同时对它的 NIO线程 NioEventLoop 进行详尽地源码分析,从中学到更多 I/O 相关的多线程设计原理和实现。
Netty 线程模型
当我们讨论 Netty线程模型 的时候,一般首先会想到的是经典的 Reactor线程模型,尽管不同的 NIO框架 对于 Reactor模式 的实现存在差异,但本质上还是遵循了 Reactor 的基础线程模型。下面让我们一起回顾经典的 Reactor线程模型。
Reactor 单线程模型
Reactor单线程模型,是指所有的 I/O操作 都在同一个 NIO线程 上面完成。NIO线程 的职责 及 模型图如下。
- 作为 NIO服务端,接收客户端的 TCP连接;
- 作为 NIO客户端,向服务端发起 TCP连接;
- 读取通信对端的请求或者应答消息;
- 向通信对端发送消息请求或者应答消息。
由于 Reactor模式 使用的是 异步非阻塞I/O,所有的 I/O操作 都不会导致阻塞,理论上一个线程可以独立处理所有 I/O相关的操作。从架构层面看,一个 NIO线程 确实可以完成其承担的职责。例如,通过 Aceptor类 接收客户端的 TCP连接请求消息,当链路建立成功之后,通过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上,进行消息解码。用户线程 完成消息编码后通过 NIO线程 将消息发送给客户端。在一些小容量应用场景下,可以使用单线程模型。但是这对于高负载、大并发的应用场景却不合适,主要原因如下。 - 一个 NIO线程 同时处理成百上千的链路,性能上无法支撑,即便 NIO线程 的 CPU 负荷达到 100%,也无法满足海量消息的编码、解码、读取和发送。
- 当 NIO线程 负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO线程 的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
- 可靠性问题。一旦 NIO线程 意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
为了解决这些问题,演进出了 Reactor多线程模型。下面我们一起看一下 Reactor多线程模型。
Reactor 多线程模型
Rector多线程模型 与 单线程模型 最大的区别就是有一组 NIO线程 来处理 I/O操作,它的原理如下图所示。
Reactor多线程模型的特点如下。
- 有专门一个 NIO线程 —— Acceptor线程 用于监听服务端,接收客户端的 TCP连接请求。
- 网络 I/O操作 —— 读、写等由一个 NIO线程池 负责,线程池可以采用标准的 JDK线程池 实现,它包含一个 任务队列 和 N个可用的线程,由这些 NIO线程 负责消息的读取、解码、编码和发送。
- 一个 NIO线程 可以同时处理 N 条链路,但是一个链路只对应一个 NIO线程,防止发生并发操作问题。
在绝大多数场景下,Reactor 多线程模型可以满足性能需求。但是,在个别特殊场景中,一个 NIO线程 负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万的客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个 Acceptor线程 可能会存在性能不足的问题,为了解决性能问题,产生了第三种 Reactor线程模型 —— 主从Reactor多线程模型。
主从 Reactor 多线程模型
主从Reactor线程模型 的特点是,服务端用于接收客户端连接的不再是一个单独的 NIO线程,而是一个独立的 NIO线程池。Acceptor 接收到客户端 TCP连接请求 并处理完成后 ( 处理过程可能包含接入认证等 ),将新创建的 SocketChannel 注册到 I/O线程池 的某个 I/O线程上,由它负责 SocketChannel 的读写和编解码工作。Acceptor线程池 仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端 Reactor线程池 的 I/O线程上,由 I/O线程 负责后续的 I/O操作。它的线程模型如下图所示。
利用 主从NIO线程模型,可以解决一个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在 Netty 的官方 Demo 中,推荐使用该线程模型。
Netty 的线程模型
Netty 的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty 可以同时支持 Reactor单线程模型、多线程模型和主从Reactor多线层模型。下面让我们通过其原理图,来快速了解 Netty 的线程模型。
可以通过下面的 Netty服务端启动代码,来了解它的线程模型。
EventLoopGroup parentGroup = new NioEventLoopGroup();
EventLoopGroup childGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
......
}
});
......
服务端启动的时候,创建了两个 NioEventLoopGroup对象,它们实际是两个独立的 Reactor线程池。一个用于接收客户端的 TCP连接,另一个用于处理 I/O相关的读写操作,或者执行 系统Task、定时任务Task 等。Netty 用于接收客户端请求的线程池职责如下。
- 接收客户端 TCP连接,初始化 Channel参数;
- 将链路状态变更事件通知给 ChannelPipeline。
Netty 处理 I/O操作 的 Reactor线程池 职责如下。
- 异步读取通信对端的数据报,发送读事件到 ChannelPipeline;
- 异步发送消息到通信对端,调用 ChannelPipeline 的消息发送接口;
- 执行系统调用 Task;
- 执行定时任务 Task,例如链路空闲状态监测定时任务。
通过调整线程池的线程个数、是否共享线程池等方式,Netty 的 Reactor线程模型 可以在单线程、多线程和主从多线程间切换,这种灵活的配置方式可以最大程度地满足不同用户的个性化定制。
为了尽可能地提升性能,Netty 在很多地方进行了无锁化的设计,例如在 I/O 线程 内部进行串行操作,避免多线程竞争导致的性能下降问题。表面上看,串行化设计似乎 CPU利用率 不高,并发程度不够。但是,通过调整 NIO线程池 的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比 “ 一个队列 —— 多个工作线程 ” 的模型性能更优。它的设计原理如下图所示。
Netty 的 NioEventLoop 读取到消息之后,直接调用 ChannelPipeline 的 fireChannelRead(Object msg)方法。只要用户不主动切换线程,一直都是由 NioEventLoop 调用用户的 Handler,期间不进行线程切换。这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
最佳实践
Netty 的多线程编程最佳实践如下。
- 创建两个 NioEventLoopGroup,用于逻辑隔离 NIO Acceptor线程池 和 NIO I/O操作线程池。
- 尽量不要在 ChannelHandler 中启动 用户线程。
- 解码要放在 NIO线程 调用的 解码Handler 中进行,不要切换到用户线程进行消息的解码。
- 如果业务逻辑操作非常简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网路操作等,可以直接在 NIO线程 上完成业务逻辑编排,不需要切换到用户线程。
- 如果业务逻辑处理复杂,不要在 NIO线程 上完成,建议将解码后的 POJO消息 封装成 Task,派发到业务线程池中由业务线程执行,以保证 NIO线程 尽快被释放,处理其他的 I/O操作。
NioEventLoop 源码分析
Netty 的 NioEventLoop 并不是一个纯粹的 I/O线程,它除了负责 I/O 的读写之外,还兼顾处理以下两类任务。
- 系统Task:通过调用 NioEventLoop 的 execute(Runnable task)方法 实现,Netty 有很多 系统Task,创建它们的主要原因是,当 I/O线程 和 用户线程 同时操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成 Task 放入消息队列中,由 I/O线程 负责执行,这样就实现了局部无锁化。
- 定时任务:通过调用 NioEventLoop 的 schedule(Runnable command, long delay, TimeUnit unit)方法 实现。
话不多说,直接上源码。
public final class NioEventLoop extends SingleThreadEventLoop {
/**
* 作为 NIO框架 的 Reactor线程,NioEventLoop 需要处理 网络I/O读写事件,因此它
* 必须聚合一个多路复用器对象
*/
private Selector selector;
private Selector unwrappedSelector;
private SelectedSelectionKeySet selectedKeys;
private final SelectorProvider provider;
/**
* 初始化 Selector
*/
private SelectorTuple openSelector() {
final Selector unwrappedSelector;
try {
unwrappedSelector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
}
if (DISABLE_KEY_SET_OPTIMIZATION) {
return new SelectorTuple(unwrappedSelector);
}
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
return Class.forName(
"sun.nio.ch.SelectorImpl",
false,
PlatformDependent.getSystemClassLoader());
} catch (Throwable cause) {
return cause;
}
}
});
if (!(maybeSelectorImplClass instanceof Class) ||
// ensure the current selector implementation is what we can instrument.
!((Class<?>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) {
if (maybeSelectorImplClass instanceof Throwable) {
Throwable t = (Throwable) maybeSelectorImplClass;
logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, t);
}
return new SelectorTuple(unwrappedSelector);
}
final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) {
// Let us try to use sun.misc.Unsafe to replace the SelectionKeySet.
// This allows us to also do this in Java9+ without any extra flags.
long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField);
long publicSelectedKeysFieldOffset =
PlatformDependent.objectFieldOffset(publicSelectedKeysField);
if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) {
PlatformDependent.putObject(
unwrappedSelector, selectedKeysFieldOffset, selectedKeySet);
PlatformDependent.putObject(
unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet);
return null;
}
// We could not retrieve the offset, lets try reflection as last-resort.
}
Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
if (cause != null) {
return cause;
}
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
if (cause != null) {
return cause;
}
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
return null;
} catch (NoSuchFieldException e) {
return e;
} catch (IllegalAccessException e) {
return e;
}
}
});
if (maybeException instanceof Exception) {
selectedKeys = null;
Exception e = (Exception) maybeException;
logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e);
return new SelectorTuple(unwrappedSelector);
}
selectedKeys = selectedKeySet;
logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
return new SelectorTuple(unwrappedSelector,
new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
}
/**
* 所有的逻辑操作都在for循环体内进行,只有当 NioEventLoop 接收到退出指令的时候,
* 才退出循环,否则一直执行下去,这也是通用的 NIO线程 实现方式。
*/
@Override
protected void run() {
for (;;) {
try {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
}
} catch (IOException e) {
rebuildSelector0();
handleLoopException(e);
continue;
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
}
Future 和 Promise 组件
从名字可以看出,Future 用于获取异步操作的结果,而 Promise 则比较抽象,无法直接猜测出其功能。
Future 组件
Future 最早来源于 JDK 的 java.util.concurrent.Future,它用于代表异步操作的结果。可以通过 get()方法 获取操作结果,如果操作尚未完成,则会同步阻塞当前调用的线程;如果不允许阻塞太长时间或者无限期阻塞,可以通过带超时时间的 get()方法 获取结果;如果到达超时时间操作仍然没有完成,则抛出 TimeoutException。
Promise 组件
Promise 是可写的 Future, Future 自身并没有写操作相关的接口,Netty 通过 Promise 对 Future 进行扩展,用于设置 I/O操作 的结果。
来源:CSDN
作者:YupyMan
链接:https://blog.csdn.net/qq_38038396/article/details/104459777