IO模型资料整理
Contents
图片和资料来自自己购买的电子书
<Netty, Redis, Zookeeper 高并发实战>
四种 IO 模型
同步阻塞 IO
Blocking IO
阻塞 IO
: 需要内核 IO 操作彻底完成后, 才返回到用户空间执行用户的操作. 阻塞指的是用户空间程序的执行状态.
同步 IO
: 是一种用户空间与内核空间的 IO 发起方式. 同步 IO 是指用户空间的线程是主动发起 IO 请求的一方, 内核空间是被动接受方.
异步 IO
: 是指系统内核是主动发起 IO 请求的一方, 用户空间的线程是被动接受方.
优点
- 开发简单
- 阻塞期间, 用户线程基本不会占用 CPU 资源
缺点
- 一般一条线程维护一个连接的 IO 操作
- 高并发下需要大量线程, 内存, 线程切换开销大
同步非阻塞 IO
Non-blocking IO
非阻塞 IO
: 是用户空间的程序不需要等待内核 IO 操作彻底完成, 可以立即返回用户空间执行用户操作, 即处于非阻塞状态. 与此同时, 内核会立即返回给用户一个状态值.
Socket 连接默认是阻塞模式的, 在 Linux 系统下可以将 socket 设置为非阻塞模式. 这时就是同步非阻塞 IO.
特点:
- 在内核缓冲区没有数据的情况下, 系统调用会立即返回一个调用失败信息
- 有数据的情况下, 是阻塞的.直到数据从内核缓冲区复制到用户进程缓存区. 完成后调用返回成功, 用户线程才会执行.
优点
- 每次发起 IO 系统调用, 在内核等待数据过程中, 可以立即返回. 用户线程不会阻塞, 实时性好.
缺点
- 要不断地轮询内核, 这将占用大量的 CPU 时间, 效率低
IO 多路复用
IO Multiplexing. 对应 Java 的 NIO 包
即 Reactor 反应器设计模式, 有时也称为异步阻塞 IO
. 例如 Java中的 Selector 和 Linux 的 Epoll
这种避免了同步非阻塞中不断要轮询的问题
对应的系统调用为 select/epoll
. 这种系统调用中, 一个进程可以监视多个文件描述符, 一旦某个描述符就绪(一般是内核缓冲区可读/写), 内核能够将就绪的状态返回给应用程序. 然后应用程序根据就绪的状态, 进行相应的 IO 系统调用.
特点 : 涉及两种系统调用. 一种是 select/epoll
操作, 一种是 IO 操作. 它与同步非阻塞 IO 有密切关系. 对于注册在选择器上的每一个可查询的 socket 连接, 一般都设置为同步非阻塞模型.
优点
- 一个 selector 线程可以同时处理成千上万的连接
- 系统不必创建大量线程
- 减小系统资源开销
缺点
- 本质上,
select/epoll
系统调用是阻塞式的, 属于同步 IO. - 都需要在读写事件就绪后, 由系统调用本身负责进行读写, 这个过程是阻塞的
异步 IO
Asynchronous IO
异步 IO
: 是指系统内核是主动发起 IO 请求的一方, 用户空间的线程是被动接受方. 类似回调.
基本流程: 用户线程通过系统调用, 向内核注册某个 IO 操作. 内核在整个 IO 操作完成后, 通知用户程序, 用户执行后续的业务操作.
有时也被称为 信号驱动 IO
Windows 通过 IOCP 实现了真正的异步 IO.
Linux 2.6
才引入异步IO, 但底层还是通过 epoll 实现, 所以目前 Linux 基本都是 epoll
Java NIO
核心组件
- Channel
- Buffer
- Selector
NIO (New IO) 与 OIO (old IO) 对比
- OIO : 是面向流(顺序操作). NIO 是面向缓冲区(Buffer 任意位置)
- OIO : 阻塞(一直阻塞, 直到有数据 read 后返回). NIO 是非阻塞(有数据, 则读取返回. 没有则直接返回)
- OIO : 没有 selector 概念. 而 NIO 有.
Channel
在 OIO , 同一个网络连接会关联到两个流: Input Stream 和 Output Stream.
在 NIO , 同 一个网络连接使用 Channel 表示
. 代表 Linux 中的底层文件描述符.
主要类型
FileChannel : 文件
SocketChannel : TCP 客户端
ServerSocketChannel : TCP 服务端
DatagramChannel : UDP
操作
FileChannel fileChannel = new FileInputStream("mtp/").getChannel();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
while(!socketChannel.finishConnect()) {
//一直判断
}
//到这里表示连接成功
Selector
它是一个 IO 事件查询器. 通过它, 可以一条线程查询多个 Channel 的 IO 事件就绪状态.
开发时, 将 Channel 注册到 Selector 中, 然后选择内部机制, selector 查询就绪事件就调用相应处理.
与 Channel 的关系
一个 channel 表示一条连接. 而 selector 可以同时监控多个 channel 的 IO. 所以 selector 和 channel 是监控和被监控的关系.
channel 和 selector 之间的关系, 通过 register
(注册器) 的方式完成. 调用 Channel.register(Selector sel, in ops)
方法, 可以将 channel 实例注册到一个 selector 中. Ops 表示事件类型. 事件类型有
SelectionKey.OP_ACCEPT
SelectionKey.OP_CONNECT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
要监控多个事件, 则用位或运算符来实现:
SelectionKey.OP_ACCEPT | SelectionKey.OP_CONNECT
注意, 并不是所有的 channel 都是可以被 selector 监控的. 能被监控的 channel 都继承了 SelectableChannel
类.
使用流程
Channel 的
validOps()
返回的是可支持的事件类型.
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class TestNIO {
@Test
public void test() throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//配置TCP 服务器信息
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8080));
//注册到selector
serverSocketChannel.validOps();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
Set<SelectionKey> selectKey = selector.selectedKeys();
System.out.println(selectKey.size());
Iterator<SelectionKey> iter = selectKey.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
//有新连接进来, 这时可用 serverSocketChannel.accept() 获取客户端 channel
System.out.println("有新连接进来 " + key.channel());
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
iter.remove();
}
}
}
}
注意, 每处理完一个事件, 后, 要更新 channel 为不同的状态. 否则会一直重复处理
Buffer
应用程序与 Channel 的主要交互操作, 利用 Buffer 进行数据的读写. 这是非阻塞的重要前提和基础之一.
它是非线程安全的
直接继承的类有
ByteBuffer (java.nio)
CharBuffer (java.nio)
DoubleBuffer (java.nio)
FloatBuffer (java.nio)
IntBuffer (java.nio)
LongBuffer (java.nio)
ShortBuffer (java.nio)
重要属性
capacity
: 容量大小.一旦写入的对象数量超过了它, 缓冲区就表示满了.初始化后就不能改变.position
: 表示当前位置. 它与读写模式
有关. 在不同模式下, 它的值是不同的.- 写模式 : 进入写模式时(默认分配后是进入这个模式), position 为 0. 每写一个对象数据, 则会移到下一个可写位置.
- 读模式 : 进入读模式时, position 为 0. 每读一个对象数据, 则会移到下一个可读位置.
- 切换模式: 调用
flip
方法
limit
: 在不同的模式下有不同的含义.- 在写入模式中, 它表示可以写入的最大上限.
- 在读模式中, 表示最多能读多少数据.
- 刚进入写模式时, limit 与 capacity 的值一样.
- 从写模式切换为读模式时, limit 值为写模式时的 position 值. 即之前写入的最大数量, 作为可以读取的上限值.
重要方法
allocate()
: 创建缓冲区. 调用子类的该方法来分配, 而不是new
put()
写入缓冲区flip()
切换模式get()
读取缓冲区数据rewind()
倒带. 如果已经读写一次数据, 想再读一遍, 这时可调用该方法position 重置为 0
limit 保持不变
mark 标记被清理
public final Buffer rewind() { position = 0; mark = -1; return this; }
mark()和 reset()
: mark 表示将当前 position 保存起来. reset 表示将 mark 的值恢复到 position 中clear()
在读模式下调用的话会切换为写入模式. 并会将 position 清 0, limit 为 capacity.
Reactor
定义
Reactor也可以称作反应器模式,它有以下几个特点:
Reactor模式中会通过分配适当的handler(处理程序)来响应IO事件,类似与AWT 事件处理线程
每个handler执行
非阻塞
的操作,类似于AWT ActionListeners 事件监听通过将handler绑定到事件进行管理,类似与AWT addActionListener 添加事件监听
演化
传统方式
while (true) {
socket = accept();
handle(socket);
}
- 如果 handle 方法没处理完, 则后面的连接没法接收处理
- 于是出现了
Connection Per Thread
reactor 方式
Reactor 类似事件驱动的 dispatcher 分发器. 它有两个重要组件
- Reactor : 负责查询 IO 事件. 然后发送到相应的 Handler 处理器处理.
- Acceptor : 负责连接
- IOHandler : IO 事件处理器. 完成真正的连接建立, 通道的读取, 业务的处理等.
单线程版本
SelectionKey 中的方法
attach(Object o)
: 将 Handler 处理器, 设置到 SelectionKey 实例Object attacment()
: 获取attach(Object o)
设置的 Handler
Reactor 与 Handler 在同一条线程中. 一般极少使用
多线程版本
- 升级Handler , 改为用线程池执行
- 如果是多核CPU, 将 reactor 也拆分为多个