BIO:
Blocking I/O,傳統的同步阻塞式網絡編程;
網絡編程的基本模型是C/S模型,即兩個進程間的通信;服務端提供IP和監聽端口,客戶端通過連接向服務端發起連接請求,通過三次握手連接,若成功則雙方通過套接字進行通信;
採用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後爲每個客戶端創建一個新的線程,
進行鏈路處理,處理完成後,通過輸出流返回應答給客戶端,線程銷燬。即典型的一請求一應答通宵模型。
問題是:
缺乏彈性伸縮能力,當客戶端併發訪問量增加後,服務端的線程個數和客戶端併發訪問數呈1:1的正比關係;
線程數量快速膨脹後,系統的性能將急劇下降,隨着訪問量的繼續增大,系統最終就死-掉-了
僞異步I/O編程
爲了改進這種一連接一線程的模型,使用線程池來管理這些線程,實現1個或多個線程處理N個客戶端的模型(但是底層還是使用的同步阻塞I/O),
通常被稱爲“僞異步I/O模型“。
不能使用使用CachedThreadPool線程池,建議FixedThreadPool,
如果發生大量併發請求,超過最大數量的線程就只能等待,直到線程池中的有空閒的線程可以被複用。
在讀取數據較慢時(比如數據量大、網絡傳輸慢等),大量併發的情況下,其他接入的消息,只能一直等待,這是問題。
什麼是NIO?
New I/O簡稱,或Non-block I/O,與舊式的基於流的I/O方法相對,表示一套新的I/O標準;jdk1.4引入的;
NIO提供了與傳統BIO模型中的Socket和ServerSocket相對應的SocketChannel和ServerSocketChannel,兩種不同的套接字通道實現。
新增的着兩種通道都支持阻塞和非阻塞兩種模式,阻塞模式使用就像傳統中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反
對於低負載、低併發的應用程序,可以使用同步阻塞I/O來提升開發速率和更好的維護性;
對於高負載、高併發的(網絡)應用,應使用NIO的非阻塞模式來開發。
NIO是基於塊(Block)的,以塊爲基本單位處理數據,傳統I/O基於字節(Byte)的;性能比流好些;
爲所有原始類型提供(Buffer)緩存支持;
增加通道(Channel)對象,作爲新的I/O抽象,類似舊式的Stream;
支持文件鎖,和內存映射文件的文件訪問接口;使用文件來實現鎖;
基於網絡操作,提供了Selector的異步網絡I/O;
Buffer(緩存區)&Channel(通道)
Buffer:NIO的核心部分,所有IO操作部分都要Buffer做,與Channel(通道)交互;
緩衝區實際上是一個數組,並提供了對數據結構化訪問以及維護讀寫位置等信息。
Channel,通道是IO的抽象,另一端就是要操作的對象或socket;
通過對buffer的讀取,來實現對IO的操作;
ByteBuffer、CharBuffer,DoubleBuffer,IntBuffer,LongBuffer等,實現了Buffer;
Channel:我們對數據的讀取和寫入要通過Channel,它就像水管一樣,是一個通道。
通道不同於流的地方就是通道是雙向的,可以用於讀、寫和同時讀寫操作。
底層的操作系統的通道一般都是全雙工的,所以全雙工的Channel比流能更好的映射底層操作系統的API。
Channel主要分兩大類:
SelectableChannel:用戶網絡讀寫(子類有:ServerSocketChannel和SocketChannel);
FileChannel:用於文件操作;
基本使用一:
FileInputStream fin = new FileInputStream(new File("d:\\temp_buffer.tmp"));
FileChannel fc=fin.getChannel();
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
fc.read(byteBuffer);
fc.close();
byteBuffer.flip();
基本使用二:NIO複製文件
public static void nioCopyFile(String resource, String destination)
throws IOException {
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
FileChannel readChannel = fis.getChannel(); //讀文件通道
FileChannel writeChannel = fos.getChannel(); //寫文件通道
ByteBuffer buffer = ByteBuffer.allocate(1024); //讀入數據緩存
while (true) {
buffer.clear();
int len = readChannel.read(buffer); //讀入數據
if (len == -1) {
break;
//讀取完畢
}
buffer.flip();
writeChannel.write(buffer);//寫入文件
}
readChannel.close();
writeChannel.close();
}
Buffer中3個重要參數:位置(position)、容量(capacity)、上限(limit);
位置:記錄當前緩衝區的位置,將從此位置開始讀寫數據;
容量:緩存區的總容量上限;
上限:緩存區實際數據的大小,可讀取的容量,總是小於等於容量;通常情況和容量相等;
Buffer的3個函數:
flip():讀寫轉換時使用;先將limit設置爲當前position的位置(實際大小),再將position重置爲0,清除標誌位Mark;
rewind():將position置零,並清除標誌位;
clear():將position置零,同時將limit設置爲capacity大小,清除標誌位Mark;
實例展示:
ByteBuffer b=ByteBuffer.allocate(15); //15個字節大小的緩衝區 | |
System.out.println("limit="+b.limit()+"capacity="+b.capacity()+"position="+b.position(); |
b.put((byte)i);
}
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
b.flip(); //重置position | |
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position()); | |
for(int i=0;i<5;i++){ |
}
System.out.println("limit="+b.limit()+" capacity="+b.capacity()+" position="+b.position());
文件映射到內存實例:
RandomAccessFile raf = new RandomAccessFile("C:\\mapfile.txt", "rw");
FileChannel fc = raf.getChannel(); //將文件映射到內存中
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, raf.length());
while(mbb.hasRemaining()){
System.out.print((char)mbb.get());
}
mbb.put(0,(byte)98); //修改文件
raf.close();
多路複用器 Selector
NIO的基礎;Selector提供選擇已經就緒的任務的能力:Selector會不斷輪詢註冊在其上的Channel;
如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,
然後通過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作。
一個Selector可以同時輪詢多個Channel,因爲JDK使用了epoll()代替傳統的select實現,所以沒有最大連接句柄1024/2048的限制。
所以,只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。
網絡編程
多線程網絡服務器的一般結構:
(圖,N<--->N)
問題:
爲每一個客戶端開啓一個線程,如果客戶端延時異常,線程可能被長時間佔用,因爲數據的準備和讀取都在這個線程中,佔用時間;
若客戶端數量衆多,可能消耗大量的系統資源;
解決:
非阻塞的NIO;
數據準備好了再工作;
NIO做法:把數據準備好了再通知我;
Channel類似於流,一個Channel可以和文件或者網絡Socket對應;
一個Selector對應一個線程,可以輪詢多個Channel,一個Channel背後對應一個Socket,即一個客戶端;
所以一個線程可以輪詢了多個channel,選擇準備好數據的Channel;
Selector的兩個方法,
select():若沒有準備好數據的Channel,則阻塞;準備好數據後返回SelectionKey,表示一對關係selector和channel的對應;
selectNow():也是選擇已經準備好的channel,若沒有準備好的,則直接返回,不阻塞;
代碼實例:
一定要寫個例子看看哦;
總結:
NIO會將數據準備好後再交給應用處理,數據讀取過程依然在線程中完成;
節省數據準備時間(因爲selector可以複用);
AIO
NIO是剝離了數據準備時,避免大量線程的等待操作,數據讀寫處理還是在線程應用中完成;
AIO更進一步,在數據準備及讀寫時都不處理,等數據操作完成後,加入回調函數即可;異步操作IO;
IO本身的速度是沒有改變的,這幾種方式只是改變裏處理IO的方式,改善線程調度,看起來系統性能變高;
server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));
使用server上的accept()方法:
public abstract <A> void accept(A attachment, CompletionHandler<AsynchronousSocketChannel,?super A> handler);
還有read()、 write() 方法;AsynchronousSocketChannel.read(ByteBuffer b, ...)
爲什麼需要了解NIO和AIO?
AIO是真正的異步非阻塞的,所以,在面對超級大量的客戶端,更能得心應手。
具體選擇什麼樣的模型或者NIO框架,完全基於業務的實際應用場景和性能需求,
如果客戶端很少,服務器負荷不重,就沒有必要選擇開發起來相對不那麼簡單的NIO做服務端;
相反,就應考慮使用NIO或者相關的框架了