一文看懂java io系統 (轉)


出處:  一文看懂java io系統

 

學習java IO系統,重點是學會IO模型,了解了各種IO模型之后就可以更好的理解java IO

Java IO 是一套Java用來讀寫數據(輸入和輸出)的API。大部分程序都要處理一些輸入,並由輸入產生一些輸出。Java為此提供了java.io包

java中io系統可以分為Bio,Nio,Aio三種io模型

  1. 關於Bio,我們需要知道什么是同步阻塞IO模型,Bio操作的對象:流,以及如何使用Bio進行網絡編程,使用Bio進行網絡編程的問題
  2. 關於Nio,我們需要知道什么是同步非阻塞IO模型,什么是多路復用Io模型,以及Nio中的Buffer,Channel,Selector的概念,以及如何使用Nio進行網絡編程
  3. 關於Aio,我們需要知道什么是異步非阻塞IO模型,Aio可以使用幾種方式實現異步操作,以及如何使用Aio進行網絡編程

BIO

BIO是同步阻塞IO,JDK1.4之前只有這一個IO模型,BIO操作的對象是流,一個線程只能處理一個流的IO請求,如果想要同時處理多個流就需要使用多線程

流包括字符流和字節流,流從概念上來說是一個連續的數據流。當程序需要讀數據的時候就需要使用輸入流讀取數據,當需要往外寫數據的時候就需要輸出流

阻塞IO模型

在Linux中,當應用進程調用recvfrom方法調用數據的時候,如果內核沒有把數據准備好不會立刻返回,而是會經歷等待數據准備就緒,數據從內核復制到用戶空間之后再返回,這期間應用進程一直阻塞直到返回,所以被稱為阻塞IO模型

BIO中操作的流主要有兩大類,字節流和字符流,兩類根據流的方向都可以分為輸入流和輸出流

按照類型和輸入輸出方向可分為:

  1. 輸入字節流:InputStream
  2. 輸出字節流:OutputStream
  3. 輸入字符流:Reader
  4. 輸出字符流:Writer

字節流主要用來處理字節或二進制對象,字符流用來處理字符文本或字符串

使用InputStreamReader可以將輸入字節流轉化為輸入字符流

Reader reader  = new InputStreamReader(inputStream); 

使用OutputStreamWriter可以將輸出字節流轉化為輸出字符流

Writer writer = new OutputStreamWriter(outputStream)

我們可以在程序中通過InputStream和Reader從數據源中讀取數據,然后也可以在程序中將數據通過OutputStream和Writer輸出到目標媒介中

在使用字節流的時候,InputStream和OutputStream都是抽象類,我們實例化的都是他們的子類,每一個子類都有自己的作用范圍

圖是網上的,侵刪

在使用字符流的時候也是,Reader和Writer都是抽象類,我們實例化的都是他們的子類,每一個子類都有自己的作用范圍

圖是網上的,侵刪

以讀寫文件為例

從數據源中讀取數據

輸入字節流:InputStream

public static void main(String[] args) throws Exception{ File file = new File("D:/a.txt"); InputStream inputStream = new FileInputStream(file); byte[] bytes = new byte[(int) file.length()]; inputStream.read(bytes); System.out.println(new String(bytes)); inputStream.close(); } 

輸入字符流:Reader

public static void main(String[] args) throws Exception{ File file = new File("D:/a.txt"); Reader reader = new FileReader(file); char[] bytes = new char[(int) file.length()]; reader.read(bytes); System.out.println(new String(bytes)); reader.close(); } 

輸出到目標媒介

輸出字節流:OutputStream

public static void main(String[] args) throws Exception{ String var = "hai this is a test"; File file = new File("D:/b.txt"); OutputStream outputStream = new FileOutputStream(file); outputStream.write(var.getBytes()); outputStream.close(); } 

輸出字符流:Writer

public static void main(String[] args) throws Exception{ String var = "hai this is a test"; File file = new File("D:/b.txt"); Writer writer = new FileWriter(file); writer.write(var); writer.close(); } 

BufferedInputStream

在使用InputStream的時候,都是一個字節一個字節的讀或寫,而BufferedInputStream為輸入字節流提供了緩沖區,讀數據的時候會一次讀取一塊數據放到緩沖區里,當緩沖區里的數據被讀完之后,輸入流會再次填充數據緩沖區,直到輸入流被讀完,有了緩沖區就能夠提高很多io速度

使用方式將輸入流包裝到BufferedInputStream中

/** * inputStream 輸入流 * 1024 內部緩沖區大小為1024byte */ BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream,1024); 

BufferedOutputStream

BufferedOutputStream可以為輸出字節流提供緩沖區,作用與BufferedInputStream類似

使用方式將輸出流包裝到BufferedOutputStream中

/** * outputStream 輸出流 * 1024 內部緩沖區大小為1024byte */ BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream,1024); 

字節流提供了帶緩沖區的,那字符流肯定也提供了BufferedReader和BufferedWriter

BufferedReader

為輸入字符流提供緩沖區,使用方式如下

BufferedReader bufferedReader = new BufferedReader(reader,1024); 

BufferedWriter

為輸出字符流提供緩沖區,使用方式如下

BufferedWriter bufferedWriter = new BufferedWriter(writer,1024); 

BIO模型 網絡編程

當使用BIO模型進行Socket編程的時候,服務端通常使用while循環中調用accept方法,在沒有客戶端請求時,accept方法會一直阻塞,直到接收到請求並返回處理的相應,這個過程都是線性的,只有處理完當前的請求之后才會接受處理后面的請求,這樣通常會導致通信線程被長時間阻塞

BIO模型處理多個連接:

在這種模式中我們通常用一個線程去接受請求,然后用一個線程池去處理請求,用這種方式並發管理多個Socket客戶端連接,像這樣:

使用BIO模型進行網絡編程的問題在於缺乏彈性伸縮能力,客戶端並發訪問數量和服務器線程數量是1:1的關系,而且平時由於阻塞會有大量的線程處於等待狀態,等待輸入或者輸出數據就緒,造成資源浪費,在面對大量並發的情況下,如果不使用線程池直接new線程的話,就會大致線程膨脹,系統性能下降,有可能導致堆棧的內存溢出,而且頻繁的創建銷毀線程,更浪費資源

使用線程池可能是更優一點的方案,但是無法解決阻塞IO的阻塞問題,而且還需要考慮如果線程池的數量設置較小就會拒絕大量的Socket客戶端的連接,如果線程池數量設置較大的時候,會導致大量的上下文切換,而且程序要為每個線程的調用棧都分配內存,其默認值大小區間為 64 KB 到 1 MB,浪費虛擬機內存

BIO模型適用於鏈接數目固定而且比較少的架構,但是使用這種模型寫的代碼更直觀簡單易於理解

NIO

JDK 1.4版本以來,JDK發布了全新的I/O類庫,簡稱NIO,是一種同步非阻塞IO模型

非阻塞IO模型

同步非阻塞IO模型實現:

非阻塞IO模型

應用進程調用recvfrom系統調用,如果內核數據沒有准備好,會直接返回一個EWOULDBLOCK錯誤,應用進程不會阻塞,但是需要應用進程不斷的輪詢調用recvfrom,直到內核數據准備就緒,之后等待數據從內核復制到用戶空間(這段時間會阻塞,但是耗時極小),復制完成后返回

IO復用模型

IO復用模型,利用Linux系統提供的select,poll系統調用,將一個或者多個文件句柄(網絡編程中的客戶端鏈接)傳遞給select或者poll系統調用,應用進程阻塞在select上,這樣就形成了一個進程對應多個Socket鏈接,然后select/poll會線性掃描這個Socket鏈接的集合,當只有少數socket有數據的時候,會導致效率下降,而且select/poll受限於所持有的文件句柄數量,默認值是1024個

信號驅動 IO模型

系統調用sigaction執行一個信號處理函數,這個系統調用不會阻塞應用進程,當數據准備就緒的時候,就為該進程生成一個SIGIO信號,通過信號回調通知應用程序調用recvfrom來讀取數據

NIO的核心概念

Buffer(緩沖區)

Buffer是一個對象,它包含一些要寫入或者讀出的數據,在NIO中所有數據都是用緩存區處理的,在讀數據的時候要從緩沖區中讀,寫數據的時候會先寫到緩沖區中,緩沖區本質上是一塊可以寫入數據,然后可以從中讀取數據的一個數組,提供了對數據的結構化訪問以及在內部維護了讀寫位置等信息

實例化一個ByteBuffer

//創建一個容量為1024個byte的緩沖區 ByteBuffer buffer=ByteBuffer.allocate(1024); 

如何使用Buffer:

  1. 寫入數據到Buffer
  2. 調用flip()方法將Buffer從寫模式切換到讀模式
  3. 從Buffer中讀取數據
  4. 調用clear()方法或者compact()方法清空緩沖區,讓它可以再次被寫入

更多詳細信息看這個:http://ifeve.com/buffers/

Channel(通道)

Channel(通道)數據總是從通道讀取到緩沖區,或者從緩沖區寫入到通道中,Channel只負責運輸數據,而操作數據是Buffer

通道與流類似,不同地方:

  1. 在於條通道是雙向的,可以同時進行讀,寫操作,而流是單向流動的,只能寫入或者讀取
  2. 流的讀寫是阻塞的,通道可以異步讀寫

數據從Channel讀到Buffer

inChannel.read(buffer); 

數據從Buffer寫到Channel

outChannel.write(buffer); 

更多詳細信息看這個:<http://ifeve.com/channels/>

以復制文件為例

FileInputStream fileInputStream=new FileInputStream(new File(src)); FileOutputStream fileOutputStream=new FileOutputStream(new File(dst)); //獲取輸入輸出channel通道 FileChannel inChannel=fileInputStream.getChannel(); FileChannel outChannel=fileOutputStream.getChannel(); //創建容量為1024個byte的buffer ByteBuffer buffer=ByteBuffer.allocate(1024); while(true){ //從inChannel里讀數據,如果讀不到字節了就返回-1,文件就讀完了 int eof =inChannel.read(buffer); if(eof==-1){ break; } //將Buffer從寫模式切換到讀模式 buffer.flip(); //開始往outChannel寫數據 outChannel.write(buffer); //清空buffer buffer.clear(); } inChannel.close(); outChannel.close(); fileInputStream.close(); fileOutputStream.close(); 

Selector(多路復用選擇器)

Selector是NIO編程的基礎,主要作用就是將多個Channel注冊到Selector上,如果Channel上發生讀或寫事件,Channel就處於就緒狀態,就會被Selector輪詢出來,然后通過SelectionKey就可以獲取到已經就緒的Channel集合,進行IO操作了

Selector與Channel,Buffer之間的關系

更多詳細信息看這個:<http://ifeve.com/selectors/

NIO模型 網絡編程

JDK中NIO使用多路復用的IO模型,通過把多個IO阻塞復用到一個select的阻塞上,實現系統在單線程中可以同時處理多個客戶端請求,節省系統開銷,在JDK1.4和1.5 update10版本之前,JDK的Selector基於select/poll模型實現,在JDK 1.5 update10以上的版本,底層使用epoll代替了select/poll

epoll較select/poll的優點在於:

  1. epoll支持打開的文件描述符數量不在受限制,select/poll可以打開的文件描述符數量有限
  2. select/poll使用輪詢方式遍歷整個文件描述符的集合,epoll基於每個文件描述符的callback函數回調

select,poll,epoll都是IO多路復用的機制。I/O多路復用就是通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫

NIO提供了兩套不同的套接字通道實現網絡編程,服務端:ServerSocketChannel和客戶端SocketChannel,兩種通道都支持阻塞和非阻塞模式

服務端代碼

服務端接受客戶端發送的消息輸出,並給客戶端發送一個消息

        //創建多路復用選擇器Selector Selector selector=Selector.open(); //創建一個通道對象Channel,監聽9001端口 ServerSocketChannel channel = ServerSocketChannel.open().bind(new InetSocketAddress(9001)); //設置channel為非阻塞 channel.configureBlocking(false); // /** * 1.SelectionKey.OP_CONNECT:連接事件 * 2.SelectionKey.OP_ACCEPT:接收事件 * 3.SelectionKey.OP_READ:讀事件 * 4.SelectionKey.OP_WRITE:寫事件 * * 將channel綁定到selector上並注冊OP_ACCEPT事件 */ channel.register(selector,SelectionKey.OP_ACCEPT); while (true){ //只有當OP_ACCEPT事件到達時,selector.select()會返回(一個key),如果該事件沒到達會一直阻塞 selector.select(); //當有事件到達了,select()不在阻塞,然后selector.selectedKeys()會取到已經到達事件的SelectionKey集合 Set keys = selector.selectedKeys(); Iterator iterator = keys.iterator(); while (iterator.hasNext()){ SelectionKey key = (SelectionKey) iterator.next(); //刪除這個SelectionKey,防止下次select方法返回已處理過的通道 iterator.remove(); //根據SelectionKey狀態判斷 if (key.isConnectable()){ //連接成功 } else if (key.isAcceptable()){ /** * 接受客戶端請求 * * 因為我們只注冊了OP_ACCEPT事件,所以有客戶端鏈接上,只會走到這 * 我們要做的就是去讀取客戶端的數據,所以我們需要根據SelectionKey獲取到serverChannel * 根據serverChannel獲取到客戶端Channel,然后為其再注冊一個OP_READ事件 */ // 1,獲取到ServerSocketChannel ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); // 2,因為已經確定有事件到達,所以accept()方法不會阻塞 SocketChannel clientChannel = serverChannel.accept(); // 3,設置channel為非阻塞 clientChannel.configureBlocking(false); // 4,注冊OP_READ事件 clientChannel.register(key.selector(),SelectionKey.OP_READ); } else if (key.isReadable()){ // 通道可以讀數據 /** * 因為客戶端連上服務器之后,注冊了一個OP_READ事件發送了一些數據 * 所以首先還是需要先獲取到clientChannel * 然后通過Buffer讀取clientChannel的數據 */ SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE); long bytesRead = clientChannel.read(byteBuffer); while (bytesRead>0){ byteBuffer.flip(); System.out.println("client data :"+new String(byteBuffer.array())); byteBuffer.clear(); bytesRead = clientChannel.read(byteBuffer); } /** * 我們服務端收到信息之后,我們再給客戶端發送一個數據 */ byteBuffer.clear(); byteBuffer.put("客戶端你好,我是服務端,你看這NIO多難".getBytes("UTF-8")); byteBuffer.flip(); clientChannel.write(byteBuffer); } else if (key.isWritable() && key.isValid()){ //通道可以寫數據 } } } 

客戶端代碼

客戶端連接上服務端后,先給服務端發送一個消息,並接受服務端發送的消息

Selector selector = Selector.open(); SocketChannel clientChannel = SocketChannel.open(); //將channel設置為非阻塞 clientChannel.configureBlocking(false); //連接服務器 clientChannel.connect(new InetSocketAddress(9001)); //注冊OP_CONNECT事件 clientChannel.register(selector, SelectionKey.OP_CONNECT); while (true){ //如果事件沒到達就一直阻塞着 selector.select(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); iterator.remove(); if (key.isConnectable()){ /** * 連接服務器端成功 * * 首先獲取到clientChannel,然后通過Buffer寫入數據,然后為clientChannel注冊OP_READ時間 */ clientChannel = (SocketChannel) key.channel(); if (clientChannel.isConnectionPending()){ clientChannel.finishConnect(); } clientChannel.configureBlocking(false); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.clear(); byteBuffer.put("服務端你好,我是客戶端,你看這NIO難嗎".getBytes("UTF-8")); byteBuffer.flip(); clientChannel.write(byteBuffer); clientChannel.register(key.selector(),SelectionKey.OP_READ); } else if (key.isReadable()){ //通道可以讀數據 clientChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE); long bytesRead = clientChannel.read(byteBuffer); while (bytesRead>0){ byteBuffer.flip(); System.out.println("server data :"+new String(byteBuffer.array())); byteBuffer.clear(); bytesRead = clientChannel.read(byteBuffer); } } else if (key.isWritable() && key.isValid()){ //通道可以寫數據 } } } 

使用原生NIO類庫十分復雜,NIO的類庫和Api繁雜,使用麻煩,需要對網絡編程十分熟悉,才能編寫出高質量的NIO程序,所以並不建議直接使用原生NIO進行網絡編程,而是使用一些成熟的框架,比如Netty

AIO

JDK1.7升級了Nio類庫,成為Nio2.0,最主要的是提供了異步文件的IO操作,以及事件驅動IO,AIO的異步套接字通道是真正的異步非阻塞IO

異步IO模型

在Linux系統中,應用進程發起read操作,立刻可以去做其他的事,內核會將數據准備好並且復制到用空間后告訴應用進程,數據已經復制完成read操作

aio模型 網絡編程

異步操作

aio不需要通過多路復用器對注冊的通道進行輪詢操作就可以實現異步讀寫,從而簡化了NIO的編程模型

aio通過異步通道實現異步操作,異步通道提供了兩種方式獲取操作結果:

  1. 通過Future類來獲取異步操作的結果,不過要注意的是future.get()是阻塞方法,會阻塞線程
  2. 通過回調的方式進行異步,通過傳入一個CompletionHandler的實現類進行回調,CompletionHandler定義了兩個方法,completed和failed兩方法分別對應成功和失敗

Aio中的Channel都支持以上兩種方式

AIO提供了對應的異步套接字通道實現網絡編程,服務端:AsynchronousServerSocketChannel和客戶端AsynchronousSocketChannel

服務端

服務端向客戶端發送消息,並接受客戶端發送的消息

AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1", 9001)); //異步接受請求 server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { //成功時 @Override public void completed(AsynchronousSocketChannel result, Void attachment) { try { ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("我是服務端,客戶端你好".getBytes()); buffer.flip(); result.write(buffer, null, new CompletionHandler<Integer, Void>(){ @Override public void completed(Integer result, Void attachment) { System.out.println("服務端發送消息成功"); } @Override public void failed(Throwable exc, Void attachment) { System.out.println("發送失敗"); } }); ByteBuffer readBuffer = ByteBuffer.allocate(1024); result.read(readBuffer, null, new CompletionHandler<Integer, Void>() { //成功時調用 @Override public void completed(Integer result, Void attachment) { System.out.println(new String(readBuffer.array())); } //失敗時調用 @Override public void failed(Throwable exc, Void attachment) { System.out.println("讀取失敗"); } }); } catch (Exception e) { e.printStackTrace(); } } //失敗時 @Override public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); //防止線程執行完 TimeUnit.SECONDS.sleep(1000L); 

客戶端

客戶端向服務端發送消息,並接受服務端發送的消息

AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); Future<Void> future = client.connect(new InetSocketAddress("127.0.0.1", 9001)); //阻塞,獲取連接 future.get(); ByteBuffer buffer = ByteBuffer.allocate(1024); //讀數據 client.read(buffer, null, new CompletionHandler<Integer, Void>() { //成功時調用 @Override public void completed(Integer result, Void attachment) { System.out.println(new String(buffer.array())); } //失敗時調用 @Override public void failed(Throwable exc, Void attachment) { System.out.println("客戶端接收消息失敗"); } }); ByteBuffer writeBuffer = ByteBuffer.allocate(1024); writeBuffer.put("我是客戶端,服務端你好".getBytes()); writeBuffer.flip(); //阻塞方法 Future<Integer> write = client.write(writeBuffer); Integer r = write.get(); if(r>0){ System.out.println("客戶端消息發送成功"); } //休眠線程 TimeUnit.SECONDS.sleep(1000L); 

總結

各IO模型對比:

偽異步IO是指使用線程池處理請求的Bio模型

參考:

netty權威指南 第二版

http://ifeve.com/java-nio-all/ 並發編程網

https://tech.meituan.com/2016/11/04/nio.html 美團技術團隊

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM