通常的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 目前支持的底層操作系統少,依賴于底層。適用于任何使用場景。