NIO/BIO
BIO網絡通訊
概述
網絡編程的基本模型是Client/Server模型,也就是兩個進程之間進行相互通訊,其中服務端提供位置信息(綁定的IP地址和監聽端口),客戶端經過鏈接操做向服務端監聽的地址發起鏈接請求,經過三次握手創建鏈接,若是鏈接創建成功,雙方就能夠經過網絡套接字(Socket)進行通訊。
在基於傳統同步阻塞模型開發中,ServerSocket負責綁定IP地址,啓動監聽端口;Socket負責發起鏈接操做。鏈接成功以後,雙方經過輸入和輸出流進行同步阻塞式通訊。
BIO通訊模型的服務端,一般由一個獨立的Acceptor線程負責監聽客戶端的鏈接,它接收到客戶端鏈接請求以後爲每一個客戶端建立一個新的線程進行鏈路處理,處理完成以後,經過輸出流返回應答給客戶端,線程銷燬。這就是典型的一請求一應答通訊模型。
該模型最大的問題就是缺少彈性伸縮能力,
當客戶端併發訪問量增長後,服務端的線程個數和客戶端併發訪問數呈1:1的正比關係,因爲線程是Java虛擬機很是寶貴的系統資源,當線程數膨脹以後,系統的性能將急劇降低,隨着併發訪問量的繼續增大,系統會發生線程堆棧溢出、建立新線程失敗等問題,並最終致使進程宕機或者僵死,不能對外提供服務。
BIO主要的問題在於每當有一個新的客戶端請求接入時,服務端必須建立一個新的線程處理新接入的客戶端鏈路,一個線程只能處理一個客戶端鏈接。在高性能服務器應用領域,每每須要面向成千上萬個客戶端的併發鏈接,這種模型顯然沒法知足高性能、高併發接入的場景。
引入線程池
爲了解決同步阻塞I/O面臨的一個鏈路須要一個線程處理的問題,後來有人對它的線程模型進行了優化——後端經過一個線程池來處理多個客戶端的請求接入,造成客戶端個數M:線程池最大線程數N的比例關係,其中M能夠遠遠大於N。
經過線程池能夠靈活地調配線程資源,設置線程的最大值,防止因爲海量併發接入致使線程耗盡。
因爲線程池和消息隊列都是有界的,並且避免了爲每一個請求都建立一個獨立線程形成的線程資源耗盡問題
所以,不管客戶端併發鏈接數多大,它都不會致使線程個數過於膨脹或者內存溢出。相比於傳統的一鏈接一線程模型,是一種改良。
BIO通訊框架的弊病
引入線程池避免了建立大量線程,可是因爲底層的通訊依然採用同步阻塞模型,其實並未從根本上解決問題
當對Socket的輸入流進行讀取操做的時候,它會一直阻塞下去,直到發生以下三種事件。
1)有數據可讀;
2)可用數據已經讀取完畢;
3)發生空指針或者I/O異常。
這意味着當對方發送請求或者應答消息比較緩慢,或者網絡傳輸較慢時,讀取輸入流一方的通訊線程將被長時間阻塞,若是對方要60s纔可以將數據發送完成,讀取一方的I/O線程也將會被同步阻塞60s,在此期間,其餘接入消息只能在消息隊列中排隊。
BIO的網絡通訊,讀和寫操做都是同步阻塞的,阻塞的時間取決於對方I/O線程的處理速度和網絡I/O的傳輸速度。
本質上來說,咱們沒法保證生產環境的網絡情況和對端的應用程序能足夠快,若是咱們的應用程序依賴對方的處理速度,它的可靠性就很是差
不管是否引入線程池技術,若是底層用的是BIO通訊框架,都是沒法從根本上解決通訊線程阻塞問題。
BIO 4種產生阻塞的方法:
1-4
//accept方法會產生阻塞,直到有客戶端鏈接
//傳統的BIO會產生阻塞:
//1.服務端accept()方法會產生阻塞
//當有客戶端接入時,accpet()方法不阻塞
//可是客戶端沒有任何的流輸入,因此產生了阻塞
//2.即read()方法也會產生阻塞
//3.若是服務端未啓動,客戶端就鏈接的話會報錯,Connection refused
//可是,須要留意的是,這個異常在程序啓動後,並非立刻拋出的
//而是卡頓了一秒鐘纔出現的
//這個現象的緣由:
//客戶端程序啓動=》嘗試鏈接服務端=》等待服務端的連接響應=》服務端沒有啓動=》
//客戶端收到服務端的響應,報出錯誤提示
//實際上,對應客戶端,socket.connect()這個方法也會產生阻塞
4.//結果證實,當不斷向outputStream裏寫數據時,寫到必定大小後,會產生阻塞
//即Write方法也會產生阻塞
NIO
NIO概述
Non-Blocking I/O,是一種非阻塞通訊模型。不一樣的語言或操做系統都有其不一樣的實現。
java.nio是jdk1.4版本引入的一套API,咱們能夠利用這套API實現非阻塞的網絡編程模型。
目前不管是何種應用,都是分佈式架構,由於分佈式架構可以抗高併發,實現高可用,負載均衡以及存儲和處理海量數據,而分佈式架構的基礎是網絡通訊。
隨着當前大數據和實時計算技術的興起,高性能 RPC 框架與網絡編程技術再次成爲焦點。
Fackebook的Thrift框架
scala的 Akka框架
實時流領域的 Storm、Spark框架
開源分佈式數據庫中的 Mycat、VoltDB
Java 領域裏大名鼎鼎的 NIO 框架——Netty,則被衆多的開源項目或商業軟件所採用。
BIO和NIO的對比
BIO
1.阻塞通訊模型,典型表明是ServerSocket 和Socketaccept connect read write 會產生阻塞。因此BIO通訊模型的弊端在於:若是有大量請求,會建立大量線程,一是可能形成內存溢出,此外,線程多了以後,會形成cpu的負載太高,由於要作線程管理和上下文切換。雖然引入線程池,也未能解決根本問題,由於底層仍是同步阻塞模型。同步阻塞模型一是性能低,二是不可靠,取決於對端環境。
2.面向流處理,即阻塞最根本的原子在於流的read和wirte方法是阻塞方法
Java NIO 管道是2個線程之間的單向數據鏈接。
Pipe有一個source通道和一個sink通道。數據會被寫到sink通道,從source通道讀取
BIO的使用場景:訪問量少,長請求(好比下載一個大文件等場景)
NIO的適用場景:高併發,高訪問量,短請求
NIO
1.非阻塞通訊模型
2.面向緩衝區(Buffer)
3.NIO傳輸數據的方式:把數據放到緩衝區,而後經過Channel進行傳輸。
Buffer
Buffer的子類,對應了8種基本數據類型裏的七種(沒有boolean類型),分別是:
1.ByteBuffer
2.CharBuffer
3.DoubleBuffer
4.FolatBuffer
5.IntBuffer
6.LongBuffer
7.ShortBuffer
1.建立緩衝區
static ByteBuffer allocate(int capacity)參數:capacity 緩衝區的容量,以字節爲單位
2.向緩衝區裏寫數據
put(byte b)
put(byte[] src)
putXxx()
3.從緩衝區裏讀數據
get()
當向緩衝區裏寫入字節時,好比1,當調用get()方法時,獲得的倒是0
4.Buffter緩衝區的4個關鍵元素
①capacity,緩存區總容量
②limit的大小<=capacity的大小,建立緩衝區時,默認=capacity
③position,初始位置在緩存區的0位,當寫入數據時,數據的寫入位置就是position的位置
寫完後,position的位置+1。
④mark,標記
5.filp()方法:
flip()反轉緩衝區,做用至關於buffer.limit(buffer.position());+ buffer.position(0);
HasRemaining方法:
告知當前位置(position)和限制位(limit)之間是否還有元素
6.rewind()重繞緩衝區
將position置爲0
7.clear()清空緩衝區
//clear的做用是清空緩衝區,但並不真正的清除數據,而是把postion置爲0,limit置爲容量上限
//因此,當get(0)時,是能夠獲得原緩衝區數據的。可是咱們通常都是在clear()方法以後,寫數據,而後filp()
//因此,並不影響緩衝區的重用。
Channel
Channel:通道,面向緩衝區,進行雙向傳輸
總接口:Channel
其中重點關注他的4個子類
操做TCP的SocketChannel
SocketChannel默認也是非阻塞的,須要個更改下configureBlocking(false);
ServerSocketChannel是一個抽象類,不能直接new,因此調用其靜態方法open()
建立出來以後,默認是阻塞的,若是要設置成非阻塞模式,須要設置:
ssc.configureBlocking(false); 屬性爲false表示非阻塞
和ServerSocketChannel
SocketChannel默認也是非阻塞的,須要個更改下configureBlocking(false);
操做UDP的DatagramChannel
操做文件的FileChannel
* 這個方法用來測試FileChannel,FileChannel只能經過FileInputStream,FileOutputStream和
* RandomAccessFile的getChannel()方法獲得。
* FileChannel在文件操做上,性能上沒什麼差異。讀或寫都是經過緩衝區來操做。此外還提供了一些額外方法,好比能夠指定從文件的某個位置開始讀或寫
* 若是FileChannel是經過FileInputStream獲得,那他只能讀文件,不能寫文件。
* 經過RandomAccessFile獲得的FileChannel,既能夠對指定文件讀也能夠寫。而且均可以指定開始讀或寫的位置
Selector
Selector 多路複用器:
能夠理解爲路由器和交換機
在一個Selector上能夠同時註冊多個非阻塞的通道,從而只須要不多的線程數既能夠管理許多通道。特別適用於開啓許多通道可是每一個通道中數據流量都很低的狀況
java
//select()是選擇器selector查詢是否有事件觸發的方法,好比accpet事件是否觸發,若是accpet事件觸發:
//就意味着有客戶端接入了。注意,select()是一個阻塞方法。當有事件被觸發時,阻塞放開。
//引入selector的好處是:線程沒必要每時每刻都去工做、去查詢客戶端是否有新事件,沒有事件的時候,線程就睡覺,休息
//有事件發生,selector會知道,線程再醒來工做。這樣一來,能夠避免了線程無心義的空轉,節省cpu資源,同時也不影響工做
Zero-Copy
原理概述
zero copy(零複製)是一種特殊形式的內存映射,它容許你將Kernel內存直接映射到設備內存空間上。其實就是設備能夠經過直接內存訪問(direct memory access,DMA)方式來訪問Kernal Space。
咱們拿Kafka舉例,Kafka會對流數據作持久化存儲以實現容錯
這意味着當Consumer從Kafka消費數據時,會有不少data從硬盤讀出以後,會原封不動的經過socket傳輸給用戶。
數據都被拷貝了四次!
Zero Copy的具體實現
實際上第二次和第三次copy是毫無心義的。應用程序僅僅緩存了一下data就原封不動的把它發回給socket buffer。實際上,data應該直接在read buffer和socket buffer之間傳輸,
爲何要使用kernel buffer作中介
使用kernel buffer作中介(而不是直接把data傳到user buffer中)看起來比較低效(多了一次copy)。
然而實際上kernel buffer是用來提升性能的。
在進行讀操做的時候,kernel buffer起到了預讀cache的做用。當寫請求的data size比kernel buffer的size小的時候,這可以顯著的提高性能
在進行寫操做時,kernel buffer的存在可使得寫請求徹底異步。
但悲劇的是,當讀請求的data size遠大於kernel buffer size的時候,這個方法自己變成了性能的瓶頸。
數據庫