Netty 的网络 IO 模型 - Reactor

Netty 怎么切换三种 IO 模式

什么是经典的三种 IO 模式

BIO,阻塞 IO 模型(JDK 1.4 之前)

NIO,非阻塞 IO 模型(JDK 1.4(2002 年,java.nio 包))

AIO,异步 IO 模型(JDK 1.7(2011 年)

网络通信 IO 模型

阻塞和非阻塞

数据就绪前要不要等待?

  • 阻塞:没有数据传过来时,读操作会阻塞到直到有数据;缓冲区满时,写操作也会阻塞。
  • 非阻塞:遇到上面的情况都是直接返回。

同步和异步

数据就绪后,操作由谁来完成

  • 同步:数据就绪后自己去读。
  • 异步:数据就绪后再自己回调给应用程序。

Netty 对三种 IO 模型的支持

Netty 对三种 IO 模型的支持 表格汇总

image-20220707185002850.png

为什么 Netty 仅支持 NIO 了?

Netty 仅仅支持 NIO 的原因

  1. 为什么不建议(Deprecate)阻塞 IO(BIO/OIO)?
    1. 连接数高的情况下,也就是高并发情况下,阻塞 -> 耗资源、效率低。
  2. 为什么删掉已经做好的 AIO 支持?
    1. WindowsAIO 实现成熟,但是很少用来做服务器;
    2. Linux 常常用来做服务器,但是 AIO 的实现不成熟;
    3. LinuxAIO 相比较 NIO 的性能提升不明显。

为什么 Netty 有多种 NIO 实现

NettyNIO 的多种实现

image-20220707185527772.png

通用的 NIO 实现(Common)在 Linux 下面也是使用 epoll 函数,为什么要单独实现?

  • 实现得更好
  • Netty 暴露了更多的可控参数,例如:
    • JDKNIO 是水平触发;
    • Netty 是边缘触发(默认)和水平触发可以切换。
  • Netty 的实现垃圾回收更少、性能更好。

细说八股 | NIO的水平触发和边缘触发到底有什么区别?

NIO 一定优于 BIO 么

  • 不一定。
  • BIO 代码简单(相对于 NIO)。适用于特定场景:连接数少,并发度低,此时 BIO 性能不输 NIO

源码解读 Netty 怎么切换 IO 模型

问题一:怎么切换 IO 模型

image-20220707190022426.png

如上图,将前缀 Nio 修改为 Oio 即切换成功,非常简单。

切换 IO 模型的原理是什么?

  1. channel 方法为例:image-20220707193119218.png
  2. channel 方法源码为:image-20220707193214791.png
  3. 很明显 ReflectiveChannelFactory 从命名上来看是一个 Channel 的反射工厂;
  4. 继续跟进去: image-20220707193756009.png
  5. 这个方法的逻辑就是获取参数的无参构造器,然后再赋值给自己的一个构造器属性。
  6. 看上去好像没什么,此时可以注意下面的一个方法:image-20220707194317622.png
  7. 这个方法的逻辑就是构造一个实例,可以看出这个方法是接口的方法,那么是谁的接口呢?
  8. 继续跟进去:image-20220707194443672.png
  9. ChannelFactory,从命名上看,是一个构造 Channel 的工厂,刚刚的方法实现也证明了这一点,那么谁调用了这个方法呢?
  10. 继续跟进去:image-20220707194642831.png
  11. 我们发现是 AbstractBootstrap 抽象类的 initAndRegister() 方法,并且这个方法还是用 final 修饰的,意味着这是一个模板方法,子类不可更改,initAndRegister() 方法里面执行了 channel = channelFactory.newChannel(); 构造了一个 Channel
  12. AbstractBootstrap 看上去有点陌生,我们看看它的子类我们有哪些呢:image-20220707195011199.png
  13. 好像一切变得熟悉起来了,也就是说是服务端或者客户端启动的时候构建了 Channel,并且这个 Channel 的类型是根据你传入的类型进行构造的。总结就是:泛型 + 反射 + 工厂实现 IO 模型切换。

Netty 如何支持三种 Reactor 模型

什么是 Reactor 及三种版本

BIO NIO AIO
Thread-Per-Connection Reactor Proactor

Reactor 介绍

Reactor 是一种开发模型,模型的核心流程为:注册事件 -> 扫描事件是否发生 -> 事件发生后做出相应的处理

  • OP_ACCEPT:请求操作;OP_CONNECT:连接操作;OP_WRITE:写数据操作;OP_READ:读数据操作;

image-20220707190609511.png

Thread-Per-Connection 模型

(1)核心思路 image-20220707190917219.png

(2)代码实现(BIO image-20220707191041095.png

Reactor 模型 V1:单线程

对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下:

(1)一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU 负荷达到100%,也无法满足海量消息的编码、解码、读取和发送;

(2)当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;

(3)可靠性问题:一旦 NIO 线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

为了解决这些问题,演进出了 Reactor 多线程模型。

image-20220707191127812.png

Reactor 模型 V2:多线程

在绝大多数场景下,Reactor 多线程模型都可以满足性能需求;但是,在极个别特殊场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。

例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个Acceptor 线程可能会存在性能不足问题,为了解决性能问题,产生了 主从 Reactor 多线程模型。

image-20220707191209402.png

Reactor 模型 V3:主从多线程

mainReactor 只负责处理连接,至于真正的事件处理则交给 subReactor 线程,这样分工的好处就是各不干扰,并且 main 那里。

image-20220707191241178.png

如何在 Netty 中使用 Reactor 模型

Netty 中使用 Reactor 模型相关 API

image-20220707191323208.png

源码解读 Netty 对 Reactor 模式支持的常见疑问

Netty 是如何支持主从 Reactor 模型的?

主要思路就是,当接收连接的时候会建立一个 ServerSocketChannel 并注册到 mainReactor 中,同时 ServerSocketChannel 会创建一个 Channel 注册到 subReactor 中,这样就完成了主从 Recator 的绑定关系。

为什么说 Netty 中的 main reactor 大多不能用到一个线程组,只能用到线程组里面的一个线程?

端口号只会绑定一次。

NettyChannel 分配 NioEventLoop 的规则是什么?

两种规则:

image-20220711151856258.png

(1)取模

通过一个 AtomicInteger 原子变量进行自增,然后模除 NioEventLoop 的个数,注意这里 AtomicInteger 原子变量要取绝对值,因为在自增到一定情况下是会出现负数的。

image-20220711152422631.png

(2)按位与 &

如果 NioEventLoop 的个数为 2 的幂次方,就可以通过 & 的方式来进行选择,就和 HashMap 元素的哈希值和索引的映射方法是一样的。

image-20220711152217190.png

通用模式的 NIO 实现多路复用器是如何跨平台的?

(1)NioEventLoop 的构造器

image-20220711144711195.png

(2)进入 SelectorProvider.provider() 方法:

  • loadProviderFromProperty() 方法的逻辑就是根据你的配置文件中的 java.nio.channels.spi.SelectorProvider 属性来加载并选择一个复用器,如果获取不到就返回 false,一般情况下获取不到,返回 false
  • loadProviderAsService() 方法的逻辑就是根据你的 META-INF 文件夹下的配置文件来加载并选择一个复用器,如果获取不到就返回 false,一般情况下获取不到,返回 false
  • 那么真正执行的就是:sun.nio.ch.DefaultSelectorProvider.create() 方法了

image-20220711144802601.png

(3)进入 sun.nio.ch.DefaultSelectorProvider.create() 方法

image-20220711145409462.png

我们可以发现它返回了一个 Window 的多路复用器,这就是说明这是和 JDK 的版本有关的,因为我的 JDKWindows 版本的,我们可以看一下 Mac 版本的 JDK,这个方法它会返回什么?

源码地址

image-20220711145631514.png

所以这就是跨平台的思路,通过调用 nio 的方法,因为 nio 在不同的 JDK 版本都有不同的实现,这需要调用 nio 的方法就好了。

猜你喜欢

转载自juejin.im/post/7119017102593228808