出處: 一文看懂java io系統
學習java IO系統,重點是學會IO模型,了解了各種IO模型之后就可以更好的理解java IO
Java IO 是一套Java用來讀寫數據(輸入和輸出)的API。大部分程序都要處理一些輸入,並由輸入產生一些輸出。Java為此提供了java.io包
java中io系統可以分為Bio,Nio,Aio三種io模型
- 關於Bio,我們需要知道什么是同步阻塞IO模型,Bio操作的對象:流,以及如何使用Bio進行網絡編程,使用Bio進行網絡編程的問題
- 關於Nio,我們需要知道什么是同步非阻塞IO模型,什么是多路復用Io模型,以及Nio中的Buffer,Channel,Selector的概念,以及如何使用Nio進行網絡編程
- 關於Aio,我們需要知道什么是異步非阻塞IO模型,Aio可以使用幾種方式實現異步操作,以及如何使用Aio進行網絡編程
BIO
BIO是同步阻塞IO,JDK1.4之前只有這一個IO模型,BIO操作的對象是流,一個線程只能處理一個流的IO請求,如果想要同時處理多個流就需要使用多線程
流包括字符流和字節流,流從概念上來說是一個連續的數據流。當程序需要讀數據的時候就需要使用輸入流讀取數據,當需要往外寫數據的時候就需要輸出流
阻塞IO模型
在Linux中,當應用進程調用recvfrom方法調用數據的時候,如果內核沒有把數據准備好不會立刻返回,而是會經歷等待數據准備就緒,數據從內核復制到用戶空間之后再返回,這期間應用進程一直阻塞直到返回,所以被稱為阻塞IO模型
流
BIO中操作的流主要有兩大類,字節流和字符流,兩類根據流的方向都可以分為輸入流和輸出流
按照類型和輸入輸出方向可分為:
- 輸入字節流:InputStream
- 輸出字節流:OutputStream
- 輸入字符流:Reader
- 輸出字符流: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:
- 寫入數據到Buffer
- 調用
flip()
方法將Buffer從寫模式切換到讀模式 - 從Buffer中讀取數據
- 調用
clear()
方法或者compact()
方法清空緩沖區,讓它可以再次被寫入
更多詳細信息看這個:http://ifeve.com/buffers/
Channel(通道)
Channel(通道)數據總是從通道讀取到緩沖區,或者從緩沖區寫入到通道中,Channel只負責運輸數據,而操作數據是Buffer
通道與流類似,不同地方:
- 在於條通道是雙向的,可以同時進行讀,寫操作,而流是單向流動的,只能寫入或者讀取
- 流的讀寫是阻塞的,通道可以異步讀寫
數據從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的優點在於:
- epoll支持打開的文件描述符數量不在受限制,select/poll可以打開的文件描述符數量有限
- 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