Netty 怎么切换三种 IO 模式
什么是经典的三种 IO 模式
BIO,阻塞 IO 模型(JDK 1.4 之前)
NIO,非阻塞 IO 模型(JDK 1.4(2002 年,java.nio包))
AIO,异步 IO 模型(JDK 1.7(2011 年)
阻塞和非阻塞
数据就绪前要不要等待?
- 阻塞:没有数据传过来时,读操作会阻塞到直到有数据;缓冲区满时,写操作也会阻塞。
- 非阻塞:遇到上面的情况都是直接返回。
同步和异步
数据就绪后,操作由谁来完成?
- 同步:数据就绪后自己去读。
- 异步:数据就绪后再自己回调给应用程序。
Netty 对三种 IO 模型的支持
Netty对三种IO模型的支持 表格汇总
为什么 Netty 仅支持 NIO 了?
Netty仅仅支持NIO的原因
- 为什么不建议(
Deprecate)阻塞 IO(BIO/OIO)?- 连接数高的情况下,也就是高并发情况下,阻塞 -> 耗资源、效率低。
- 为什么删掉已经做好的
AIO支持?Windows的AIO实现成熟,但是很少用来做服务器;Linux常常用来做服务器,但是AIO的实现不成熟;Linux下AIO相比较NIO的性能提升不明显。
为什么 Netty 有多种 NIO 实现
Netty对NIO的多种实现
通用的
NIO实现(Common)在Linux下面也是使用 epoll 函数,为什么要单独实现?
- 实现得更好
Netty暴露了更多的可控参数,例如:JDK的NIO是水平触发;Netty是边缘触发(默认)和水平触发可以切换。
Netty的实现垃圾回收更少、性能更好。
NIO 一定优于 BIO 么
- 不一定。
BIO代码简单(相对于NIO)。适用于特定场景:连接数少,并发度低,此时BIO性能不输NIO。
源码解读 Netty 怎么切换 IO 模型
问题一:怎么切换
IO模型
如上图,将前缀 Nio 修改为 Oio 即切换成功,非常简单。
切换
IO模型的原理是什么?
- 以
channel方法为例: channel方法源码为:- 很明显
ReflectiveChannelFactory从命名上来看是一个Channel的反射工厂; - 继续跟进去:
- 这个方法的逻辑就是获取参数的无参构造器,然后再赋值给自己的一个构造器属性。
- 看上去好像没什么,此时可以注意下面的一个方法:
- 这个方法的逻辑就是构造一个实例,可以看出这个方法是接口的方法,那么是谁的接口呢?
- 继续跟进去:
ChannelFactory,从命名上看,是一个构造Channel的工厂,刚刚的方法实现也证明了这一点,那么谁调用了这个方法呢?- 继续跟进去:
- 我们发现是
AbstractBootstrap抽象类的initAndRegister()方法,并且这个方法还是用final修饰的,意味着这是一个模板方法,子类不可更改,initAndRegister()方法里面执行了channel = channelFactory.newChannel();构造了一个Channel; AbstractBootstrap看上去有点陌生,我们看看它的子类我们有哪些呢:- 好像一切变得熟悉起来了,也就是说是服务端或者客户端启动的时候构建了
Channel,并且这个Channel的类型是根据你传入的类型进行构造的。总结就是:泛型 + 反射 + 工厂实现 IO 模型切换。
Netty 如何支持三种 Reactor 模型
什么是 Reactor 及三种版本
| BIO | NIO | AIO |
|---|---|---|
Thread-Per-Connection | Reactor | Proactor |
Reactor介绍
Reactor 是一种开发模型,模型的核心流程为:注册事件 -> 扫描事件是否发生 -> 事件发生后做出相应的处理。
OP_ACCEPT:请求操作;OP_CONNECT:连接操作;OP_WRITE:写数据操作;OP_READ:读数据操作;
Thread-Per-Connection模型
(1)核心思路
(2)代码实现(BIO)
Reactor模型 V1:单线程
对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下:
(1)一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU 负荷达到100%,也无法满足海量消息的编码、解码、读取和发送;
(2)当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
(3)可靠性问题:一旦 NIO 线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
为了解决这些问题,演进出了 Reactor 多线程模型。
Reactor模型 V2:多线程
在绝大多数场景下,Reactor 多线程模型都可以满足性能需求;但是,在极个别特殊场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。
例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个Acceptor 线程可能会存在性能不足问题,为了解决性能问题,产生了 主从 Reactor 多线程模型。
Reactor模型 V3:主从多线程
mainReactor 只负责处理连接,至于真正的事件处理则交给 subReactor 线程,这样分工的好处就是各不干扰,并且 main 那里。
如何在 Netty 中使用 Reactor 模型
Netty中使用Reactor模型相关API
源码解读 Netty 对 Reactor 模式支持的常见疑问
Netty是如何支持主从Reactor模型的?
主要思路就是,当接收连接的时候会建立一个 ServerSocketChannel 并注册到 mainReactor 中,同时 ServerSocketChannel 会创建一个 Channel 注册到 subReactor 中,这样就完成了主从 Recator 的绑定关系。
为什么说
Netty中的main reactor大多不能用到一个线程组,只能用到线程组里面的一个线程?
端口号只会绑定一次。
Netty给Channel分配NioEventLoop的规则是什么?
两种规则:
(1)取模
通过一个 AtomicInteger 原子变量进行自增,然后模除 NioEventLoop 的个数,注意这里 AtomicInteger 原子变量要取绝对值,因为在自增到一定情况下是会出现负数的。
(2)按位与 &
如果 NioEventLoop 的个数为 2 的幂次方,就可以通过 & 的方式来进行选择,就和 HashMap 元素的哈希值和索引的映射方法是一样的。
通用模式的
NIO实现多路复用器是如何跨平台的?
(1)NioEventLoop 的构造器
(2)进入 SelectorProvider.provider() 方法:
loadProviderFromProperty()方法的逻辑就是根据你的配置文件中的java.nio.channels.spi.SelectorProvider属性来加载并选择一个复用器,如果获取不到就返回false,一般情况下获取不到,返回false;loadProviderAsService()方法的逻辑就是根据你的META-INF文件夹下的配置文件来加载并选择一个复用器,如果获取不到就返回false,一般情况下获取不到,返回false;- 那么真正执行的就是:
sun.nio.ch.DefaultSelectorProvider.create()方法了
(3)进入 sun.nio.ch.DefaultSelectorProvider.create() 方法
我们可以发现它返回了一个 Window 的多路复用器,这就是说明这是和 JDK 的版本有关的,因为我的 JDK 是 Windows 版本的,我们可以看一下 Mac 版本的 JDK,这个方法它会返回什么?
所以这就是跨平台的思路,通过调用 nio 的方法,因为 nio 在不同的 JDK 版本都有不同的实现,这需要调用 nio 的方法就好了。