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

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

首先澄清幾個概念

同步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í)行時的狀態(tài)

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

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

白話來說就是

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

異步:可以理解為被動,我打個電話,別人把東西送到我家

阻塞:說完,我腦子一直在想這個東西,等他,其他啥也沒干

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

1、BIO - 同步阻塞 流Stream

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

I、字節(jié)流

InputStream/OutputStream

讀?。盒枰⒁獾氖牵琁nputStream read(byte[] ) 方法,并不能保證一定能讀取完全,特別是網(wǎng)絡(luò)情況下,需要循環(huán)讀保證讀到

索引:seek/ mark/reset

過濾器流 – 裝飾者模式

緩存流 BufferedInputStream/BufferedOutPutStream

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

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

加密流

特定用途的流

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

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

PrintStream

FileInputStream/FileOutputStream

II、字符流

Reader/Writer

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

過濾器

緩存 BufferedReader/BufferedWriter

特定用途的流

PushBackReader 可以把字符壓回到流中

PrintWriter

FileReader/FileWriter

III 隨機(jī)讀寫

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

FileWriter/FileOutputStream

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

new FileOutputStream(fileName, true);

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

RandomAccessFile – 不在流繼承體系中的類

IV、其他

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

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

InputStreamReader/OutputStreamWriter

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

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

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

I、通道

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

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

操作方法

讀取:channel.read(Buffer)

寫入:channel.write(Buffer)

需要注意的是

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

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

a、文件通道 FileChannel

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

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

FileChannel 結(jié)合 緩存Buffer

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

b、TCP通道

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

打開一個Socket通道:

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

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

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

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

ServerSocketChannel 和ServerSocket使用類似

打開一個ServerSocketChannel

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

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

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

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

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

c、UDP通道

DatagramChannel和DatagramSocket類似

打開一個DatagramChannel

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

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

監(jiān)聽/發(fā)送

channel.receive(buf);

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

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

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

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

transferTo

從文件通道 寫入 到另一個通道中

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

DMA技術(shù)???

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

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

II、緩存/緩沖

a、Buffer的基本操作

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

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

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

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

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

Buffer的關(guān)鍵屬性

Capacity

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

Position | Limit

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

Limit 讀時表明有多少可讀所以 = position;寫時表明有多少可寫 = capacity

Mark

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

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

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

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

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

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

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

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

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

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

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

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

重置索引

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

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

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

Buffer使用,需要關(guā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個可以使用

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

printLocation(bu);

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

解決方式:

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

寫模式使用 rewind 這樣 limit 不受限制

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

b、常見緩沖區(qū)

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

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

CharBuffer

DoubleBuffer

FloatBuffer

IntBuffer

LongBuffer

ShortBuffer

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

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

Scatter read

從多個緩存讀取

Gather write

把數(shù)據(jù)寫入多個緩存

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

這樣可以分開處理。

d、特殊緩沖區(qū)

MappedByteBuffer –大文件讀寫利器

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

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

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

使用 通道 channel.map 方法打開

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

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

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

III、選擇器

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

使用選擇器的過程和通道類似

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

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

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

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

通過SectionKey對象可以做具體處理

處理過的事件要從集合刪除

a、SelectionKey

四個常量,表名 監(jiān)聽的事件類型 accept/connect/read/write

每個SelectionKey

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

返回channel和selector,還有注冊時的附加對象

IV、管道Pipe

線程間通訊的利器

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

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

Pipe.SinkChannel send = pipe.sink();

Pipe.SourceChannel recieve = pipe.source();

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

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

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

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

Dispatcher/Reaction - Demultiplexer

|

EventHandler (Handler)

Reacotr 模式的 角色

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

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

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

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

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

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

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

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

I、通道

所有的通道提供了兩種方式的讀寫

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

二、CompletionHandler 異步通知接口

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

a、文件通道AsynchronousFileChannel

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

Path filePath = Paths.get(fileName);

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(filePath);

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

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);

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

AsynchronousServerSocketChannel

流程和NIO的流程一致

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

二、綁定serverChannel.bind(socketAddress);

三、監(jiān)聽

serverChannel.accept(null, new CompletionHandler(){

@Override

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

}

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

同Reactor模式一樣,也是一種異步操作的IO,依賴于操作系統(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ù)。對應(yīng) NIO的 對應(yīng)接口。

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

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

同步 or 異步

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

主動 or 被動

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

單線程 or 多線程

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容