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
的方法就好了。