通常的IO操作,只要不是操作系统内存的数据,基本都是IO操作,常见的IO操作,一般都是 操作磁盘、网卡这些(串口这些用的少不考虑),对于应用而言读取网络上的数据和读取文件里的数据没有什么不同。对于IO操作,分为几个层面来看这个问题:一是怎么表征IO的数据;二是IO操作的模型
首先澄清几个概念
同步or异步
指的是消息交互的方式。在这里一般是指 用户态和系统态:
同步:向系统发送了消息后,需要等待系统返回,进行交互处理。比如FileInputStream.read 需要等待 返回,一次交互才结束。需要 client自己处理/判断消息。
异步:向系统发送了消息后,不需要等待系统,继续其他操作,等系统操作完成,以消息通知处理结果,比如 AsynchronousFileChannel.read 后,不需要等系统结果返回。
阻塞 or 非阻塞
指的是线程执行时的状态
阻塞:执行时,方法没有交出当前的控制权.
非阻塞:在执行时,方法立即交出控制权。比如 Future.get
白话来说就是
同步:可以理解为主动,我去要东西,一直把东西拿回家
异步:可以理解为被动,我打个电话,别人把东西送到我家
阻塞:说完,我脑子一直在想这个东西,等他,其他啥也没干
非阻塞:说完,我就去干其他事情了
1、BIO - 同步阻塞 流Stream
可以把流理解为 一个存取数据的通道。 根据流向,可以分为输出和输入流;根据数据类型,分为字符流和字节流
I、字节流
InputStream/OutputStream
读取:需要注意的是,InputStream read(byte[] ) 方法,并不能保证一定能读取完全,特别是网络情况下,需要循环读保证读到
索引:seek/ mark/reset
过滤器流 – 装饰者模式
缓存流 BufferedInputStream/BufferedOutPutStream
压缩流 提供了 zip/gzip 等压缩
摘要流 – MD5/SHA 在流处理的过程中,计算摘要信息,比单独计算要节省空间
加密流
特定用途的流
PushBackInputStream 可以把字节压回到流中
数据流 DataInputStream/DataOutputStream – 可以直接读入数据 ByteArrayInputStream/ByteArrayOutputStream
PrintStream
FileInputStream/FileOutputStream
II、字符流
Reader/Writer
方法和字节流类似,可以指定字符集
过滤器
缓存 BufferedReader/BufferedWriter
特定用途的流
PushBackReader 可以把字符压回到流中
PrintWriter
FileReader/FileWriter
III 随机读写
因为Java IO 流的体系,流都是顺序的;所以对于使用流的读写
FileWriter/FileOutputStream
只有两种方式写入,要么是 覆盖写,要么是追加写,并不能实现随机读写。
new FileOutputStream(fileName, true);
如果要实现 随机读写
RandomAccessFile – 不在流继承体系中的类
IV、其他
对于流是否准备好的差异InputStream的 available 知道有多少字节,返回的就是可读的字节数;而字符因为字符集的问题 ready 方法只能返回 boolean
字节流和字符流转换
InputStreamReader/OutputStreamWriter
2、NIO 同步非阻塞 -> select模型 (多路复用) Channel + Buffer
单纯的同步非阻塞存在用户 CPU 挨个空轮询的问题,所有的就绪都放到Selector中
当没有通道 就绪时,第一次 调用 select 会阻塞。后续会不断调用select 方法,只要有通道就绪,就可以执行处理。(如果把 Channel配置成阻塞, 则和IO方式一样使用)
I、通道
通道表示了到 IO(文件、网络等) 的链接
通道与流的区别:通道是双向的,而流是单向的;通道需要和 Buffer 结合操作
操作方法
读取:channel.read(Buffer)
写入:channel.write(Buffer)
需要注意的是
读取方法返回的是读入字节数
写入时,不能保证Buffer一次全部写完,所以需要调用 buffer.hasRemaining 检查是否还有数据,循环写入
a、文件通道 FileChannel
通道的具体实现类FileChannelImpl 不在JDK 中
获取FileChannel的方式:流 FileInputStrem/FileOutputStream ; 随机读写文件RandomAccessFile
FileChannel 结合 缓存Buffer
实现如下操作 Read | Write | Size/position/close/force/truncate
b、TCP通道
SocketChannel 和Socket 使用类似,Client 创建 Socket,Server通过 链接获取一个 Socket;所以SocketChannel的来源也是两个
打开一个Socket通道:
打开通道SocketChannel socketChannel = SocketChannel.open();
链接 socketChannel.connect(new InetSocketAddress(host, port));
读写方式都是标准的方式。
阻塞模式与非阻塞模式:阻塞方式和正常的流方式类似;而非阻塞方式不等待任何结果,适用于轮询。
ServerSocketChannel 和ServerSocket使用类似
打开一个ServerSocketChannel
打开通道 ServerSocketChannel serverChannel = ServerSocketChannel.open();
绑定 serverChannel.socket().bind(new InetSocketAddress(port));
监听 SocketChannel channel = serverChannel.accept();
读写方式都是标准的方式。
阻塞模式与非阻塞模式:阻塞方式和正常的流方式类似;而非阻塞方式不等待任何结果,适用于轮询。
c、UDP通道
DatagramChannel和DatagramSocket类似
打开一个DatagramChannel
打开通道 DatagramChannel channel = DatagramChannel.open();
绑定端口(对于发送可以不指定端口)channel.socket().bind(new InetSocketAddress(port));
监听/发送
channel.receive(buf);
channel.send(buf, new InetSocketAddress(host, ip));
这里区别的是取消了 DatagramPacket 的使用
d、通道间传输数据
主要是针对 File文件通道和其他通道直接传输数据的;最常见的就是文件通道和网络通道交换数据。 大名鼎鼎的 ZeroCopy,直接从 文件通道到 网卡通道,不需要进过系统内核态,拷贝几次数据
transferTo
从文件通道 写入 到另一个通道中
直接写入,不必经过 系统上下文/用户上下文
DMA技术???
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
transferFrom 从另一个通道 读取数据到 文件通道
II、缓存/缓冲
a、Buffer的基本操作
使用Buffer 进行读写的关键步骤
一、写入数据到Buffer
二、调用flip()方法
三、从Buffer中读取数据
四、调用clear()方法或者compact()方法
Buffer的关键属性
Capacity
静态属性,记录缓存区的容量大小,创建时指定
Position | Limit
动态属性,position表示当前位置(写和读都一样,从0开始到最大capacity-1);
Limit 读时表明有多少可读所以 = position;写时表明有多少可写 = capacity
Mark
标记状态,可以任意标记,不能超过 position,类似于checkpoint,可以回退到这个位置
0<= mark <= position <= limit <= capactiy
Buffer使用这些属性来标记缓冲数据是否可读,哪些可读。
创建缓冲区
一、正常创建:ByteBuffer buffer = ByteBuffer.allocate(8092);
二、直接缓存: allocateDirect -- VM 直接对系统缓存/网卡缓存操作。
写入数据
一、从通道获取chanel.read(buffer)
二、内存数据直接写入buffer.put(byte[])
读入数据
一、数据读到通道中去 channel.write(buffer)
二、数据输入内存 buffer.get()
重置索引
一、flip / rewind,回到起始位置,区别是 flip 时,设置limit=position
二、clear/compact 清空数据(并不真的删除数据) position=0,limit=capacity; 对于compact,limit=capacity - position
三、mark / reset 只是标记使用,后续通过把mark赋给 position使用,实现重新读取
Buffer使用,需要关注的一个问题
ByteBuffer bu = ByteBuffer.allocate(10);
byte[] data = "0123456789".getBytes();
bu.put(data);
bu.rewind();
bu.get();
bu.flip(); -- 此处 flip 后 position=0 limit=1,导致容量只能有1个可以使用
bu.put("12".getBytes());
printLocation(bu);
当缓冲不是完整写入/或读取不完全时,使用了 flip 后,因为不断用 position设置limit,导致读写模式切换后,缓冲容量不断缩小。
解决方式:
一、读模式使用 flip 可以完整读取 buffer中已有的内容
写模式使用 rewind 这样 limit 不受限制
二、每次操作完,都 clear 清空缓冲
b、常见缓冲区
最常用的就是 ByteBuffer,按字节处理
和流一样,还有其他具体数据类型的缓冲
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
c、分散(scatter)聚集(gather)
把通道的数据读取到多个缓冲区中
Scatter read
从多个缓存读取
Gather write
把数据写入多个缓存
典型的应用场景就是 消息头固定 + 消息体
这样可以分开处理。
d、特殊缓冲区
MappedByteBuffer –大文件读写利器
使用内存映射的方式,直接把文件内存映射到 虚拟内存上。
Java在 32位机器上,一个进程只能分配 2G内存(受地址空间影响),所以JVM只能分配2G,如果读写大文件怎么办?
input.map(FileChannel.MapMode.READ_ONLY, position, length);
使用 通道 channel.map 方法打开
可以指定任意位置,任意长度的数据读写;可以分块读取
使用了 Map 缓冲区的 通道,不必使用 channel.read/write 来读写缓冲区了;而是直接读写缓存去 buffer.get / put
缺点是内存,不会立即回收,而是要等到垃圾回收才会回收;如果文件太大,需要及时回收内存。
III、选择器
如果不使用选择器, NIO 和 IO其实并没有太多的优势。需要使用阻塞方式,读取数据。选择器 使用了一个 多路复用的技术,通过注册到选择器的多路轮询进行处理。
使用选择器的过程和通道类似
一、打开选择器Selector selector = Selector.open();
二、注册通道到 选择器 serverChannel.register(selector, SelectionKey.OP_ACCEPT);
三、轮询通道,查看是否有事件就绪 selector.select()
四、一旦有事件就绪,返回 SectionKey的集合,给应用处理
通过SectionKey对象可以做具体处理
处理过的事件要从集合删除
a、SelectionKey
四个常量,表名 监听的事件类型 accept/connect/read/write
每个SelectionKey
就绪和感兴趣的事件结合
返回channel和selector,还有注册时的附加对象
IV、管道Pipe
线程间通讯的利器
一、打开管道 Pipe pipe = Pipe.open()
二、获取 发送通道 和 接收通道
Pipe.SinkChannel send = pipe.sink();
Pipe.SourceChannel recieve = pipe.source();
三、发送和接收,和普通的通道 + Buffer类似
3、Reactor模式 (NIO 模式增强后的 伪异步模式)
注意 Java NIO 本身的操作是 同步非阻塞的;通过 Reactor 模式封装后,从实现上看,变成了异步非阻塞的,轮询的工作应用交给了EventLoops框架,应用变成了被动调用的;但所有的调用还是在一个线程里,并没有实现完全的异步效果。
Reactor模式 关键角色
Dispatcher/Reaction - Demultiplexer
|
EventHandler (Handler)
Reacotr 模式的 角色
Handler – 操作系统的句柄;对网络来说就是 socket 描述符
Demultiplexer – 事件分离器,即NIO的 Selector.select 方法,对应了操作系统的 select 函数。
EventHandler – 事件处理器 ,即NIO的 SelectionKey 后的事件比如 OP_ACCEPT
Dispatcher/Reaction – 管理器,对应事件的注册、删除事件、派发事件等等,对应NIO的Selector对象
可以看到 Reactor 模式就是 Observer模式在IO通讯的一个应用。裸观察者模式关注的是数据的变化,比较单一;Reactor需要关注很多事件列表,关注的内容比较复杂一点。
Reactor模式的使用场景非常多,很多经典的框架,比如 NodeJS、Netty 都使用了Reactor 模式的架构。
4、AIO - 异步非阻塞 Channel + Buffer
AIO 也称为 nio2是对asynchronize IO API的包装,Linux上没有底层实现,可能还是epoll模拟的; 所以Linux aio的效率不高 java aio在windows上是利用iocp实现的,这是真正的异步IO。而在linux上,是通过epoll模拟异步的
I、通道
所有的通道提供了两种方式的读写
一、Future – 使用了java的并发包
二、CompletionHandler 异步通知接口
和NIO一样也是配合 Buffer进行读写
a、文件通道AsynchronousFileChannel
和NIO的FileChannel不同。异步的通道不是通过 流/随机文件获取的通道,而是直接打开的通道
Path filePath = Paths.get(fileName);
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(filePath);
读取时,指定异步事件调用时,指定位置读取到缓存
fileChannel.read(buffer, position, null, new CompletionHandler(){
@Override
public void completed(Integer result, Object attachment) { }
}
b、TCP通道
AsynchronousSocketChannel
流程和NIO的一致
一、打开通道 AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
二、链接 socketChannel.connect(socketAddress);
读写方式都是标准的方式。即通过buffer读写。
AsynchronousServerSocketChannel
流程和NIO的流程一致
一、打开通道AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
二、绑定serverChannel.bind(socketAddress);
三、监听
serverChannel.accept(null, new CompletionHandler(){
@Override
public void completed(AsynchronousSocketChannel result, Object attachment) { }
}
5、Proactor 模式 (真正的异步IO)
同Reactor模式一样,也是一种异步操作的IO,依赖于操作系统层面的支持
Proactor - Asynchronous Operation Processor
|
CompletionHandler (Handler)
从模式看,两者极为相似,所不同的是,事件管理和派发,都是由操作系统实现
Proactor角色
Handler – 系统句柄和 Raactor 一样
Asynchronous Operation Processor – 异步消息处理器,由操作系统实现。
CompletionHandler – 完成事件接口,一般是回调函数。对应 NIO的 对应接口。
Proactor – 管理器,从操作系统完成事件队列中取出异步操作的结果,分发 并调用相应的后续回调函数进行处理 。
IO设计模式之:Reactor 和 Proactor的差异
同步 or 异步
Reactor 是基于同步的;而Proactor 是基于异步的
主动 or 被动
Reactor 是用户态下 主动去轮询,而Proactor 是完全是被动被系统 通过回调函数调用
单线程 or 多线程
Reactor 是单线程的 事件分离和分发模型;Proactor是多线程的 事件分离和分发模型。
总体来说,Reactor 是基于epoll操作系统发生事件后通知 进程,在用户态完成数据的拷贝;由框架在从系统态读取完数据后,回调 应用二次开发的程序;Proactor 则是基于IOCP 操作系统再系统态(内核)读完数据,填到用户态的缓冲中,回调二次开发程序。
因是同步的 所以 Reactor 适合于处理时间短的高效任务,节省了线程等资源;适合于IO密集型,不适合CPU密集型。Proactor 目前支持的底层操作系统少,依赖于底层。适用于任何使用场景。