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閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容