IO 编程模型(java篇) 精华一页纸

通常的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 目前支持的底层操作系统少,依赖于底层。适用于任何使用场景。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容