IO 編程模型(java篇) 精華一頁(yè)紙

通常的IO操作,只要不是操作系統(tǒng)內(nèi)存的數(shù)據(jù),基本都是IO操作,常見(jiàn)的IO操作,一般都是 操作磁盤(pán)、網(wǎng)卡這些(串口這些用的少不考慮),對(duì)于應(yīng)用而言讀取網(wǎng)絡(luò)上的數(shù)據(jù)和讀取文件里的數(shù)據(jù)沒(méi)有什么不同。對(duì)于IO操作,分為幾個(gè)層面來(lái)看這個(gè)問(wèn)題:一是怎么表征IO的數(shù)據(jù);二是IO操作的模型

首先澄清幾個(gè)概念

同步or異步

指的是消息交互的方式。在這里一般是指 用戶態(tài)和系統(tǒng)態(tài):

同步:向系統(tǒng)發(fā)送了消息后,需要等待系統(tǒng)返回,進(jìn)行交互處理。比如FileInputStream.read 需要等待 返回,一次交互才結(jié)束。需要 client自己處理/判斷消息。

異步:向系統(tǒng)發(fā)送了消息后,不需要等待系統(tǒng),繼續(xù)其他操作,等系統(tǒng)操作完成,以消息通知處理結(jié)果,比如 AsynchronousFileChannel.read 后,不需要等系統(tǒng)結(jié)果返回。

阻塞 or 非阻塞

指的是線程執(zhí)行時(shí)的狀態(tài)

阻塞:執(zhí)行時(shí),方法沒(méi)有交出當(dāng)前的控制權(quán).

非阻塞:在執(zhí)行時(shí),方法立即交出控制權(quán)。比如 Future.get

白話來(lái)說(shuō)就是

同步:可以理解為主動(dòng),我去要東西,一直把東西拿回家

異步:可以理解為被動(dòng),我打個(gè)電話,別人把東西送到我家

阻塞:說(shuō)完,我腦子一直在想這個(gè)東西,等他,其他啥也沒(méi)干

非阻塞:說(shuō)完,我就去干其他事情了

1、BIO - 同步阻塞 流Stream

可以把流理解為 一個(gè)存取數(shù)據(jù)的通道。 根據(jù)流向,可以分為輸出和輸入流;根據(jù)數(shù)據(jù)類(lèi)型,分為字符流和字節(jié)流

I、字節(jié)流

InputStream/OutputStream

讀取:需要注意的是,InputStream read(byte[] ) 方法,并不能保證一定能讀取完全,特別是網(wǎng)絡(luò)情況下,需要循環(huán)讀保證讀到

索引:seek/ mark/reset

過(guò)濾器流 – 裝飾者模式

緩存流 BufferedInputStream/BufferedOutPutStream

壓縮流 提供了 zip/gzip 等壓縮

摘要流 – MD5/SHA 在流處理的過(guò)程中,計(jì)算摘要信息,比單獨(dú)計(jì)算要節(jié)省空間

加密流

特定用途的流

PushBackInputStream 可以把字節(jié)壓回到流中

數(shù)據(jù)流 DataInputStream/DataOutputStream – 可以直接讀入數(shù)據(jù) ByteArrayInputStream/ByteArrayOutputStream

PrintStream

FileInputStream/FileOutputStream

II、字符流

Reader/Writer

方法和字節(jié)流類(lèi)似,可以指定字符集

過(guò)濾器

緩存 BufferedReader/BufferedWriter

特定用途的流

PushBackReader 可以把字符壓回到流中

PrintWriter

FileReader/FileWriter

III 隨機(jī)讀寫(xiě)

因?yàn)镴ava IO 流的體系,流都是順序的;所以對(duì)于使用流的讀寫(xiě)

FileWriter/FileOutputStream

只有兩種方式寫(xiě)入,要么是 覆蓋寫(xiě),要么是追加寫(xiě),并不能實(shí)現(xiàn)隨機(jī)讀寫(xiě)。

new FileOutputStream(fileName, true);

如果要實(shí)現(xiàn) 隨機(jī)讀寫(xiě)

RandomAccessFile – 不在流繼承體系中的類(lèi)

IV、其他

對(duì)于流是否準(zhǔn)備好的差異InputStream的 available 知道有多少字節(jié),返回的就是可讀的字節(jié)數(shù);而字符因?yàn)樽址膯?wèn)題 ready 方法只能返回 boolean

字節(jié)流和字符流轉(zhuǎn)換

InputStreamReader/OutputStreamWriter

2、NIO 同步非阻塞 -> select模型 (多路復(fù)用) Channel + Buffer

單純的同步非阻塞存在用戶 CPU 挨個(gè)空輪詢的問(wèn)題,所有的就緒都放到Selector中

當(dāng)沒(méi)有通道 就緒時(shí),第一次 調(diào)用 select 會(huì)阻塞。后續(xù)會(huì)不斷調(diào)用select 方法,只要有通道就緒,就可以執(zhí)行處理。(如果把 Channel配置成阻塞, 則和IO方式一樣使用)

I、通道

通道表示了到 IO(文件、網(wǎng)絡(luò)等) 的鏈接

通道與流的區(qū)別:通道是雙向的,而流是單向的;通道需要和 Buffer 結(jié)合操作

操作方法

讀取:channel.read(Buffer)

寫(xiě)入:channel.write(Buffer)

需要注意的是

讀取方法返回的是讀入字節(jié)數(shù)

寫(xiě)入時(shí),不能保證Buffer一次全部寫(xiě)完,所以需要調(diào)用 buffer.hasRemaining 檢查是否還有數(shù)據(jù),循環(huán)寫(xiě)入

a、文件通道 FileChannel

通道的具體實(shí)現(xiàn)類(lèi)FileChannelImpl 不在JDK 中

獲取FileChannel的方式:流 FileInputStrem/FileOutputStream ; 隨機(jī)讀寫(xiě)文件RandomAccessFile

FileChannel 結(jié)合 緩存Buffer

實(shí)現(xiàn)如下操作 Read | Write | Size/position/close/force/truncate

b、TCP通道

SocketChannel 和Socket 使用類(lèi)似,Client 創(chuàng)建 Socket,Server通過(guò) 鏈接獲取一個(gè) Socket;所以SocketChannel的來(lái)源也是兩個(gè)

打開(kāi)一個(gè)Socket通道:

打開(kāi)通道SocketChannel socketChannel = SocketChannel.open();

鏈接 socketChannel.connect(new InetSocketAddress(host, port));

讀寫(xiě)方式都是標(biāo)準(zhǔn)的方式。

阻塞模式與非阻塞模式:阻塞方式和正常的流方式類(lèi)似;而非阻塞方式不等待任何結(jié)果,適用于輪詢。

ServerSocketChannel 和ServerSocket使用類(lèi)似

打開(kāi)一個(gè)ServerSocketChannel

打開(kāi)通道 ServerSocketChannel serverChannel = ServerSocketChannel.open();

綁定 serverChannel.socket().bind(new InetSocketAddress(port));

監(jiān)聽(tīng) SocketChannel channel = serverChannel.accept();

讀寫(xiě)方式都是標(biāo)準(zhǔn)的方式。

阻塞模式與非阻塞模式:阻塞方式和正常的流方式類(lèi)似;而非阻塞方式不等待任何結(jié)果,適用于輪詢。

c、UDP通道

DatagramChannel和DatagramSocket類(lèi)似

打開(kāi)一個(gè)DatagramChannel

打開(kāi)通道 DatagramChannel channel = DatagramChannel.open();

綁定端口(對(duì)于發(fā)送可以不指定端口)channel.socket().bind(new InetSocketAddress(port));

監(jiān)聽(tīng)/發(fā)送

channel.receive(buf);

channel.send(buf, new InetSocketAddress(host, ip));

這里區(qū)別的是取消了 DatagramPacket 的使用

d、通道間傳輸數(shù)據(jù)

主要是針對(duì) File文件通道和其他通道直接傳輸數(shù)據(jù)的;最常見(jiàn)的就是文件通道和網(wǎng)絡(luò)通道交換數(shù)據(jù)。 ?大名鼎鼎的 ZeroCopy,直接從 文件通道到 網(wǎng)卡通道,不需要進(jìn)過(guò)系統(tǒng)內(nèi)核態(tài),拷貝幾次數(shù)據(jù)

transferTo

從文件通道 寫(xiě)入 到另一個(gè)通道中

直接寫(xiě)入,不必經(jīng)過(guò) 系統(tǒng)上下文/用戶上下文

DMA技術(shù)???

fileChannel.transferTo(0, fileChannel.size(), socketChannel);

transferFrom 從另一個(gè)通道 讀取數(shù)據(jù)到 文件通道

II、緩存/緩沖

a、Buffer的基本操作

使用Buffer 進(jìn)行讀寫(xiě)的關(guān)鍵步驟

一、寫(xiě)入數(shù)據(jù)到Buffer

二、調(diào)用flip()方法

三、從Buffer中讀取數(shù)據(jù)

四、調(diào)用clear()方法或者compact()方法

Buffer的關(guān)鍵屬性

Capacity

靜態(tài)屬性,記錄緩存區(qū)的容量大小,創(chuàng)建時(shí)指定

Position | Limit

動(dòng)態(tài)屬性,position表示當(dāng)前位置(寫(xiě)和讀都一樣,從0開(kāi)始到最大capacity-1);

Limit 讀時(shí)表明有多少可讀所以 = position;寫(xiě)時(shí)表明有多少可寫(xiě) = capacity

Mark

標(biāo)記狀態(tài),可以任意標(biāo)記,不能超過(guò) position,類(lèi)似于checkpoint,可以回退到這個(gè)位置

0<= mark <= position <= limit <= capactiy

Buffer使用這些屬性來(lái)標(biāo)記緩沖數(shù)據(jù)是否可讀,哪些可讀。

創(chuàng)建緩沖區(qū)

一、正常創(chuàng)建:ByteBuffer buffer = ByteBuffer.allocate(8092);

二、直接緩存: allocateDirect -- VM 直接對(duì)系統(tǒng)緩存/網(wǎng)卡緩存操作。

寫(xiě)入數(shù)據(jù)

一、從通道獲取chanel.read(buffer)

二、內(nèi)存數(shù)據(jù)直接寫(xiě)入buffer.put(byte[])

讀入數(shù)據(jù)

一、數(shù)據(jù)讀到通道中去 channel.write(buffer)

二、數(shù)據(jù)輸入內(nèi)存 buffer.get()

重置索引

一、flip / rewind,回到起始位置,區(qū)別是 flip 時(shí),設(shè)置limit=position

二、clear/compact 清空數(shù)據(jù)(并不真的刪除數(shù)據(jù)) position=0,limit=capacity; 對(duì)于compact,limit=capacity - position

三、mark / reset 只是標(biāo)記使用,后續(xù)通過(guò)把mark賦給 position使用,實(shí)現(xiàn)重新讀取

Buffer使用,需要關(guān)注的一個(gè)問(wèn)題

ByteBuffer bu = ByteBuffer.allocate(10);

byte[] data = "0123456789".getBytes();

bu.put(data);

bu.rewind();

bu.get();

bu.flip(); -- 此處 flip 后 position=0 limit=1,導(dǎo)致容量只能有1個(gè)可以使用

bu.put("12".getBytes());

printLocation(bu);

當(dāng)緩沖不是完整寫(xiě)入/或讀取不完全時(shí),使用了 flip 后,因?yàn)椴粩嘤?position設(shè)置limit,導(dǎo)致讀寫(xiě)模式切換后,緩沖容量不斷縮小。

解決方式:

一、讀模式使用 flip 可以完整讀取 buffer中已有的內(nèi)容

寫(xiě)模式使用 rewind 這樣 limit 不受限制

二、每次操作完,都 clear 清空緩沖

b、常見(jiàn)緩沖區(qū)

最常用的就是 ByteBuffer,按字節(jié)處理

和流一樣,還有其他具體數(shù)據(jù)類(lèi)型的緩沖

CharBuffer

DoubleBuffer

FloatBuffer

IntBuffer

LongBuffer

ShortBuffer

c、分散(scatter)聚集(gather)

把通道的數(shù)據(jù)讀取到多個(gè)緩沖區(qū)中

Scatter read

從多個(gè)緩存讀取

Gather write

把數(shù)據(jù)寫(xiě)入多個(gè)緩存

典型的應(yīng)用場(chǎng)景就是 消息頭固定 + 消息體

這樣可以分開(kāi)處理。

d、特殊緩沖區(qū)

MappedByteBuffer –大文件讀寫(xiě)利器

使用內(nèi)存映射的方式,直接把文件內(nèi)存映射到 虛擬內(nèi)存上。

Java在 32位機(jī)器上,一個(gè)進(jìn)程只能分配 2G內(nèi)存(受地址空間影響),所以JVM只能分配2G,如果讀寫(xiě)大文件怎么辦?

input.map(FileChannel.MapMode.READ_ONLY, position, length);

使用 通道 channel.map 方法打開(kāi)

可以指定任意位置,任意長(zhǎng)度的數(shù)據(jù)讀寫(xiě);可以分塊讀取

使用了 Map 緩沖區(qū)的 通道,不必使用 channel.read/write 來(lái)讀寫(xiě)緩沖區(qū)了;而是直接讀寫(xiě)緩存去 buffer.get / put

缺點(diǎn)是內(nèi)存,不會(huì)立即回收,而是要等到垃圾回收才會(huì)回收;如果文件太大,需要及時(shí)回收內(nèi)存。

III、選擇器

如果不使用選擇器, NIO 和 IO其實(shí)并沒(méi)有太多的優(yōu)勢(shì)。需要使用阻塞方式,讀取數(shù)據(jù)。選擇器 使用了一個(gè) 多路復(fù)用的技術(shù),通過(guò)注冊(cè)到選擇器的多路輪詢進(jìn)行處理。

使用選擇器的過(guò)程和通道類(lèi)似

一、打開(kāi)選擇器Selector selector = Selector.open();

二、注冊(cè)通道到 選擇器 serverChannel.register(selector, SelectionKey.OP_ACCEPT);

三、輪詢通道,查看是否有事件就緒 selector.select()

四、一旦有事件就緒,返回 SectionKey的集合,給應(yīng)用處理

通過(guò)SectionKey對(duì)象可以做具體處理

處理過(guò)的事件要從集合刪除

a、SelectionKey

四個(gè)常量,表名 監(jiān)聽(tīng)的事件類(lèi)型 accept/connect/read/write

每個(gè)SelectionKey

就緒和感興趣的事件結(jié)合

返回channel和selector,還有注冊(cè)時(shí)的附加對(duì)象

IV、管道Pipe

線程間通訊的利器

一、打開(kāi)管道 Pipe pipe = Pipe.open()

二、獲取 發(fā)送通道 和 接收通道

Pipe.SinkChannel send = pipe.sink();

Pipe.SourceChannel recieve = pipe.source();

三、發(fā)送和接收,和普通的通道 + Buffer類(lèi)似

3、Reactor模式 (NIO 模式增強(qiáng)后的 偽異步模式)

注意 Java NIO 本身的操作是 同步非阻塞的;通過(guò) Reactor 模式封裝后,從實(shí)現(xiàn)上看,變成了異步非阻塞的,輪詢的工作應(yīng)用交給了EventLoops框架,應(yīng)用變成了被動(dòng)調(diào)用的;但所有的調(diào)用還是在一個(gè)線程里,并沒(méi)有實(shí)現(xiàn)完全的異步效果。

Reactor模式 關(guān)鍵角色

Dispatcher/Reaction - Demultiplexer

|

EventHandler (Handler)

Reacotr 模式的 角色

Handler – 操作系統(tǒng)的句柄;對(duì)網(wǎng)絡(luò)來(lái)說(shuō)就是 socket 描述符

Demultiplexer – 事件分離器,即NIO的 Selector.select 方法,對(duì)應(yīng)了操作系統(tǒng)的 select 函數(shù)。

EventHandler – 事件處理器 ,即NIO的 SelectionKey 后的事件比如 OP_ACCEPT

Dispatcher/Reaction – 管理器,對(duì)應(yīng)事件的注冊(cè)、刪除事件、派發(fā)事件等等,對(duì)應(yīng)NIO的Selector對(duì)象

可以看到 Reactor 模式就是 Observer模式在IO通訊的一個(gè)應(yīng)用。裸觀察者模式關(guān)注的是數(shù)據(jù)的變化,比較單一;Reactor需要關(guān)注很多事件列表,關(guān)注的內(nèi)容比較復(fù)雜一點(diǎn)。

Reactor模式的使用場(chǎng)景非常多,很多經(jīng)典的框架,比如 NodeJS、Netty 都使用了Reactor 模式的架構(gòu)。

4、AIO - 異步非阻塞 Channel + Buffer

AIO 也稱(chēng)為 nio2是對(duì)asynchronize IO API的包裝,Linux上沒(méi)有底層實(shí)現(xiàn),可能還是epoll模擬的; 所以Linux aio的效率不高 java aio在windows上是利用iocp實(shí)現(xiàn)的,這是真正的異步IO。而在linux上,是通過(guò)epoll模擬異步的

I、通道

所有的通道提供了兩種方式的讀寫(xiě)

一、Future – 使用了java的并發(fā)包

二、CompletionHandler 異步通知接口

和NIO一樣也是配合 Buffer進(jìn)行讀寫(xiě)

a、文件通道AsynchronousFileChannel

和NIO的FileChannel不同。異步的通道不是通過(guò) 流/隨機(jī)文件獲取的通道,而是直接打開(kāi)的通道

Path filePath = Paths.get(fileName);

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(filePath);

讀取時(shí),指定異步事件調(diào)用時(shí),指定位置讀取到緩存

fileChannel.read(buffer, position, null, new CompletionHandler(){

@Override

public void completed(Integer result, Object attachment) { }

}

b、TCP通道

AsynchronousSocketChannel

流程和NIO的一致

一、打開(kāi)通道 AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();

二、鏈接 socketChannel.connect(socketAddress);

讀寫(xiě)方式都是標(biāo)準(zhǔn)的方式。即通過(guò)buffer讀寫(xiě)。

AsynchronousServerSocketChannel

流程和NIO的流程一致

一、打開(kāi)通道AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();

二、綁定serverChannel.bind(socketAddress);

三、監(jiān)聽(tīng)

serverChannel.accept(null, new CompletionHandler(){

@Override

public void completed(AsynchronousSocketChannel result, Object attachment) { }

}

5、Proactor 模式 (真正的異步IO)

同Reactor模式一樣,也是一種異步操作的IO,依賴(lài)于操作系統(tǒng)層面的支持

Proactor - Asynchronous Operation Processor

|

CompletionHandler (Handler)

從模式看,兩者極為相似,所不同的是,事件管理和派發(fā),都是由操作系統(tǒng)實(shí)現(xiàn)

Proactor角色

Handler – 系統(tǒng)句柄和 Raactor 一樣

Asynchronous Operation Processor – 異步消息處理器,由操作系統(tǒng)實(shí)現(xiàn)。

CompletionHandler – 完成事件接口,一般是回調(diào)函數(shù)。對(duì)應(yīng) NIO的 對(duì)應(yīng)接口。

Proactor – 管理器,從操作系統(tǒng)完成事件隊(duì)列中取出異步操作的結(jié)果,分發(fā) 并調(diào)用相應(yīng)的后續(xù)回調(diào)函數(shù)進(jìn)行處理 。

IO設(shè)計(jì)模式之:Reactor 和 Proactor的差異

同步 or 異步

Reactor 是基于同步的;而Proactor 是基于異步的

主動(dòng) or 被動(dòng)

Reactor 是用戶態(tài)下 主動(dòng)去輪詢,而Proactor 是完全是被動(dòng)被系統(tǒng) 通過(guò)回調(diào)函數(shù)調(diào)用

單線程 or 多線程

Reactor 是單線程的 事件分離和分發(fā)模型;Proactor是多線程的 事件分離和分發(fā)模型。

總體來(lái)說(shuō),Reactor 是基于epoll操作系統(tǒng)發(fā)生事件后通知 進(jìn)程,在用戶態(tài)完成數(shù)據(jù)的拷貝;由框架在從系統(tǒng)態(tài)讀取完數(shù)據(jù)后,回調(diào) 應(yīng)用二次開(kāi)發(fā)的程序;Proactor 則是基于IOCP 操作系統(tǒng)再系統(tǒng)態(tài)(內(nèi)核)讀完數(shù)據(jù),填到用戶態(tài)的緩沖中,回調(diào)二次開(kāi)發(fā)程序。

因是同步的 所以 Reactor 適合于處理時(shí)間短的高效任務(wù),節(jié)省了線程等資源;適合于IO密集型,不適合CPU密集型。Proactor 目前支持的底層操作系統(tǒng)少,依賴(lài)于底層。適用于任何使用場(chǎng)景。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容