I/O 模型基本说明
1)
I/O
模
型简单的理解
:
就是
用什么样的通
道
进
行数据的发送和接收,很
大程度上决定
了
程
序通信的
性
能
2)
Java共支持3种网络编程
模
型
/IO
模式:
BIO
、
NIO
、
AIO
3)
Java BIO
: 同步并阻
塞
(
传统阻塞型
)
,
服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开
销
【
简单示意图
】
4)
Java NIO
:
同步非阻塞
,服务器实现模式为一个线程处理多个请求
(
连接
)
,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有
I/O
请求就进行处理
【
简单示意图
】
5)
Java AIO(NIO.2)
:
异步非阻塞
,
AIO
引入异步通道的概念,采用了
Proactor
模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
BIO、NIO、AIO适用场景分析
1)
BIO
方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,
JDK1.4
以前的唯一选择,但程
序简
单易理解
。
2)
NIO
方式适用于
连接数目多且连接比较短
(轻操作)的架构,比如聊天服务器
,
弹
幕系统,服务器间通讯等。编
程比较复杂,
JDK1.4
开始支持
。
3)
AIO
方式使用于
连接数目多且连接比较长
(重操作)的架构,比如相册服务器,充分调用
OS
参与并发操作,编程比较复杂,
JDK7
开始支持
。
l
Java NIO
基本介绍
1)
Java NIO
全称
java non-blocking IO
,是指
JDK
提供的新
API
。从
JDK1.4
开始,
Java
提供
了一
系列改进的输入
/
输出的新特性,被统称为
NIO(
即
New IO
)
,是
同步非阻塞
的
2)
NIO
相关类都被放在
java.nio
包及子包下,并且对原
java.io
包中的很多类进行改写
。
3)
NIO
有三大核心部分:
Channel(
通道
)
,
Buffer(
缓冲区
)
,
Selector(
选择器
)
4)
NIO
是 面
向
缓
冲区 ,或者面向
块
编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使
用它可以提供
非阻塞
式的高伸缩性网
络
5)Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而
不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。【
后面有案例说明】
6)
通
俗理解:
NIO
是可以做到用一个线程来处理多个操作的。假设有
10000
个请求过来
,
根据实际情况,可以分配
50
或者
100
个线程来处理。不像之前的阻塞
IO
那样,非得分配
10000
个
。
7)
HTTP2.0
使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比
HTTP1.1
大了好几个数量级。
l
NIO
和
BIO
的比较
1)
BIO 以流的方式处理数据,而 NIO 以块的方式处理数据
,
块
I/O
的效率比流
I/O
高很
多
2)
B
IO
是阻
塞的,
NIO
则
是非
阻塞
的
3)
BIO
基
于
字节流和字符流
进行操作,而
NIO
基于
Channel(
通道
)
和
Buffer(
缓冲区
)
进行操作,数
据总
是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Selector(
选
择
器
)
用于监听多个
通道
的事件(比如:连接请求,数据到达等),因此使用
单个线程就可以监听多个客户端
通
道
Buffer
基本介绍
1)
Buffer
就是一个内存块 ,
底
层是有一个数组
2)
数
据的读取写入是通过
Buffer,
这个和
BIO , BIO
中要么是输入流,或者是
输
出流
,
不能双向,但是
NIO
的
Buffer
是可以读也可以写
,
需要
flip
方法
切
换channel 是双向的
,
可以返回底层操作系统的情况
,
比如Linux , 底层的操作系统通道就是双向的
.
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer,如图: 【后面举例说明】
l通道(Channel)
1)NIO的通道类似于流,但有些区别如下:
•通道可以同时进行读写,而流只能读或者只能写
•通道可以实现异步读写数据
•通道可以从缓冲读数据,也可以写数据到缓冲
2)BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
3)Channel在NIO中是一个接口
public interface Channel extends Closeable{}
4)常用的 Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel。【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】
5)FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
l
Selector
(
选择器
)
1)Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
2)Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
3)只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
4)避免了多线程之间的上下文切换导致的开销
零拷贝的再次理解
1)
我
们说零拷贝,是从
操作系统的角度来说的
。因为内核缓冲区之间,没有数据是重复的(只有
kernel buffer
有一份数
据)。
2)
零
拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的
CPU
缓存伪共享以及无
CPU
校验和计算。
lJava AIO 基本介绍
1)
JDK
7
引入了
Asynchronous I/O
,即
AIO
。在进行
I/O
编程中,常用到两种模式:
Reactor和 Proactor
。
Java 的 NIO 就是 Reactor
,当有事件触发时,服务器端得到通知,进行相应的处理
2)
AIO
即
NIO2.0
,叫做异步不阻塞的
IO
。
AIO
引入异步通道的概念,采用了
Proactor
模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
●
| BIO | NIO | AIO |
IO 模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |
l
原生
NIO
存在的问题
1)
NIO
的类库和
API
繁杂,使用麻烦
:需
要熟练掌握
Selector
、
ServerSocketChannel
、
SocketChannel
、
ByteBuffer
等
。
2)
需
要具备其他的额外技
能:
要
熟
悉
Java
多线程编程,因为
NIO
编程涉及到
Reactor
模式,你必须对多线程
和
网络
编
程非常熟悉,才能编写出高质量的
NIO
程序
。
3)
开
发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异
常流
的处理等
等。
4)
JDK
NIO
的
Bug
:例如臭名昭著的
Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%
。直
到
JDK 1.7
版本该问题仍旧存在
,没
有被根本解决。
l
Netty
的优点
Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。
1)
设
计优雅:适用于各种传输类型的统一
API
阻塞和非阻塞
Socket
;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型
-
单线程,一个或多个线程
池
.
2)
使
用方便:详细记录的
Javadoc
,用户指南和示例;没有其他依赖项,
JDK 5
(
Netty
3.x
)或
6
(
Netty
4.x
)就足够了
。
3)
高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
4)
安
全:完整的
SSL/TLS
和
StartTLS
支持
。
5)
社
区活跃、不断更新:社区活跃,版本迭代周期短,发现的
Bug
可以被及时修复,同时,更多的新功能会被加入
目前存在的线程模型有:
1)
Reactor
模式
2)
根据
Reactor
的数量和处理资源池线程的数量不同,
有 3 种典型的实现
•
单 Reactor 单线程;
•单 Reactor 多线程;
•主从 Reactor 多线程
4)
Netty
线程模式
(
Netty
主要
基于主从
Reactor
多线程模型
做了一定的改进,其中主从
Reactor
多线程模型有多个
Reactor)
lReactor 模式
针对传统阻塞 I/O 服务模型的 2 个缺点,解决方案:
1)
基于
I/O
复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
Reactor 对应的叫法: 1. 反应器模式 2. 分发者模式(Dispatcher) 3. 通知者模式(notifier)
2)
基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务
。
I/O 复用结合线程池,就是 Reactor 模式基本设计思想,如图:
说明:
1)
Reactor
模式,通过一个或多个输入同时传递给服务处理器的模式
(
基于事件驱动
)
2)
服务器端程序处理传入的多个请求
,
并将它们同步分派到相应的处理线程, 因此
Reactor
模式也叫
Dispatcher
模式
3)
Reactor
模式使用
IO
复用监听事件
,
收到事件后,分发给某个线程
(
进程
),
这点就是网络服务器高并发处理关键
Reactor 模式中 核心组成:
1)
Reactor
:
Reactor
在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对
IO
事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;
2)
Handlers
:处理程序执行
I/O
事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。
Reactor
通过调度适当的处理程序来响应
I/O
事件,处理程序执行非阻塞操作。
l单 Reactor 单线程 方案优缺点分析:
1)
优点:
模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
2)
缺点:
性能问题,只有一个线程,无法完全发挥多核
CPU
的性能。
Handler
在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致
性能瓶颈
3)
缺点:
可靠性问题
,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
4)
使用场景:
客户端的数量有限,业务处理非常快速,比如
Redis
在业务处理的时间复杂度
O(1)
的情况
l单Reactor多线程 方案优缺点分析:
1)
优点:
可以充分的利用多核
cpu
的处理能力
2)
缺点:
多线程数据共享和访问比较复杂, reactor 处理所有的事件的监听和响应,在单线程运行, 在高并发场景容易出现性能瓶颈.
l
主从
Reactor
多线程
主从:
注意下面是多个subReactor. Main也只是处理acceptor
方案优缺点说明:
1)
优点:
父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
2)
优点:
父线程与子线程的数据交互简单,
Reactor
主线程只需要把新连接传给子线程,子线程无需返回数据。
3)
缺点:
编程复杂度较高
结合实例:这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持
Reactor 模式具有如下的优点:
1)
响应快,不必为单个同步时间所阻塞,虽然
Reactor
本身依然是同步的
2)
可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程
/
进程的
切换开销
3)
扩展性好
,可以方便的通过增加
Reactor
实例个数来充分利用
CPU
资源
4
)复用性好
,
Reactor
模型本身与具体事件处理逻辑无关,具有很高的复用性
l
Netty
模型
简单版
Netty 主要基于主从 Reactors 多线程模型(如图)做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor
1)
BossGroup
线程维护
Selector ,
只关注
Accecpt
2)
当接收到
Accept
事件,获取到对应的
SocketChannel
,
封装成
NIOScoketChannel
并注册到
Worker
线程
(
事件循环
),
并进行维护
3)
当
Worker
线程监听到
selector
中通道发生自己感兴趣的事件后,就进行处理
(
就由
handler)
, 注意
handler
已经加入到通道
进阶版
Netty 主要基于主从 Reactors 多线程模型(如图)做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor
详细版
1)
Netty
抽象出两组线程池
BossGroup
专门负责接收客户端的连接
,
WorkerGroup
专门负责网络的读写
2)
BossGroup
和
WorkerGroup
类型都是
NioEventLoopGroup
3)
NioEventLoopGroup
相当于一个事件循环组
,
这个组中含有多个事件循环 ,每一个事件循环是
NioEventLoop
4)
NioEventLoop
表示一个不断循环的执行处理任务的线程, 每个
NioEventLoop
都有一个
selector ,
用于监听绑定在其上的
socket
的网络通讯
5)
NioEventLoopGroup
可以有多个线程
,
即可以含有多个
NioEventLoop
6)
每个
Boss
NioEventLoop
循环执行的步骤有
3
步
1.
轮询
accept
事件
2.
处理
accept
事件
,
与
client
建立连接
,
生成
NioScocketChannel
,
并将其注册到某个
worker
NIOEventLoop
上的
selector
3.
处理任务队列的任务 , 即
runAllTasks
7) 每个 Worker NIOEventLoop 循环执行的步骤
1.
轮询
read, write
事件
2.
处理
i/o
事件, 即
read , write
事件,在对应
NioScocketChannel
处理
3.
处理任务队列的任务 , 即
runAllTasks
8) 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了 channel , 即通过pipeline 可以获取到对应通道, 管道中维护了很多的 处理器
============