NIO入門
-
傳統的同步阻塞式I/O編程
-
傳統BIO
-
服務端提供位置信息(綁定的IP地址和監聽端口),客戶端通過連接操作向服務端監聽的地址發起連接請求,通過三次握手建立連接,如果連接成功,雙方就可以通過網絡套接字(Socket)進行通信
-
ServerSocket 負責綁定IP地址,啓動監聽端口
-
Socket負責發起連接操作。連接成功之後,雙方通過輸入流和輸出流進行同步阻塞式通信
-
-
BIO通信模型
-
採用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後,爲每個客戶端創建一個新的線程進行鏈路處理,處理完成之後,通過輸出流返回應答客戶端,線程消毀。一請求一應答通信模型
-
缺乏彈性伸縮能力,線程數膨脹,系統的性能將急劇下降,系統會發生線程堆棧溢出,創建新線程失敗等問題
-
-
僞異步
-
爲了改進一線程一連接模型,後來又演進出了一種通過線程池或者消息隊列實現1個或者多個線程處理N個客戶端的請求,由於它的底層通信機制依然使用同步阻塞I/O,所以被稱爲“僞異步”
-
-
僞異步I/O編程
-
爲了解決一個鏈接需要一個線程處理的問題,後來有人對它的線程模型進行了優化,後端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數M:線程池最大線程N的比例關係,其中M可以遠遠大於N,設置線程的最大值,防止由於海量併發接入導致線程耗盡
-
將客戶端的Scoket封裝成一個Task,投遞到後端的線程池中去進行處理,線程池可以設置消息隊列的大小和最大線程數,資源佔用是可控的。
-
由於線程池和消息隊列都有有界的,因此,無論客戶端併發連接數多大,它都不會導致線程個數膨脹或者內存溢出,想比較一連一線程模型,是一種改良。
-
優點:採用線程池,避免了爲每個請求都創建一個獨立線程造成的線程資源耗盡問題,
-
缺點:底層的通信依然採用同步阻塞模型,因此無法從根本解決問題
-
-
僞異步I/O弊端分享
-
TCP知識
-
當消息的接收方處理緩慢的時候,將不能及時地從TCP緩存區讀取數據,這將會導致發送方的TCPwindow size不斷減少,直到爲0,雙方處於Keep-Alive狀態,消息發送方將不能再向TCP緩衝區寫入消息,這時如果採用的是同步阻塞I/O,write操作將會被無限期阻塞,直到TCP window size大於0或者發生I/O異常
-
阻塞的時間取決於對方的I/O線程的處理速度和網絡I/O的傳輸速度。本質上講,我們無法保證生產環境的網絡狀態和對端的應用程序能足夠快
-
級聯故障
-
服務端處理緩慢,返回應答消息耗費60s
-
採用僞異步I/O的線程正在讀取故障服務節點的響應,由於讀取輸入流是阻塞的,因此,它也會被同步阻塞
-
假如所有可用線程都被故障服務器阻塞,那後續所有I/O消息都將在隊列中排隊
-
由於線程池採用阻塞隊列實現,當隊列積滿之後,後續入列的操作將被阻塞
-
前端只有一個Acceptor線程接受客戶端接入,它的阻塞在線程池的同步阻塞隊列之後,新的客端請求將被拒絕,客戶端會發送大量的超時
-
-
-
-
-
基於NIO的非阻塞編程
-
概念
-
與socket類,和ServerSocket類相對應,NIO也提供了SocketChannel和ServerSocketChannel兩種不同的套接字通道實現
-
兩種新增的通道都支持阻塞和非阻塞兩種模式
-
提供了高速的,面向快的I/O
-
-
緩存區Buffer
-
任何時候訪問NIO中的數據,都是通過緩衝區進行操作的,讀直接讀到緩衝區中的,寫入數據,是寫入到緩衝區中的
-
緩衝區實質上是個數組。緩衝區提供了對數據的結構化訪問以及維護讀寫位置等信息
-
子節緩衝區,ByteBuffer,CharBuffer 字符緩衝區 。。。
-
方便網絡讀寫
-
-
通道Channel
-
Channel是一個通道,可以通過它讀取和寫入數據,它就是自來水管
-
通道與流不同之處就在於通道是雙向的,流只是一個方向上移動,一個流必須是inputStream或者OutputStream的子類,而且通道可以用於讀寫,或同時讀寫
-
因爲channel是全雙工的,所以它可以比較好的映射底層操作系統的API,
-
實際上channel可以分爲兩大類,分別是用於網絡讀取的SelectableChannel和用於文件操作的FileChannel,ServerSocketChannel和SocketChannel都是SelectableChannel的子類
-
-
多路複用器Selector
-
多路複用器提供選擇已經就緒的任務的能力,Selector會不斷地輪詢註冊在其上的Channel,如果某個Channel上面有新的TCP連接接入,讀和寫實際,這個Channel就處於就緒狀態,會被Seclettor輪詢出來,然後通過SelectionKey,獲取就緒的Channel的集合,然後進行後續的I/O操作
-
一個多路複用器Selector可以同時輪詢多個Channel,由於jdk使用了epoll代替了傳統的select實現,所以它並沒有最大連接句柄1024/2048的限制·,這也意味着只需要一個線程負責Seclector的輪詢,就可以接入成千上萬的客戶端,這是個很大的進行。
-
實現步驟
-
打開iServerSocketChannel 用於監聽客戶端的連接,它是所有客戶端連接的父管道
ServerSocketChannel server = ServerSocketChannel.open();
-
綁定監聽端口,設置連接爲非阻塞模式
server.bind(new InetSocketAddress(this.port));
server.configureBlocking(false);
-
創建Reactor線程,創建多路複用器並啓動線程
Selector selector = Selector.open();
-
將ServerSocketChannel註冊到Reactor線程的多路複用器Seclector上,監聽ACCEPT事件
server.register(selector, SelectionKey.OP_ACCEPT);
-
多路複用器在線程run方法的無限循環體內輪詢準備就緒的Key
while (true) { //大堂經理再叫號 selector.select(); //每次都拿到所有的號子 Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iter = keys.iterator(); //不斷地迭代,就叫輪詢 //同步體現在這裏,因爲每次只能拿一個key,每次只能處理一種狀態 while (iter.hasNext()) { SelectionKey key = iter.next(); iter.remove(); //每一個key代表一種狀態 //沒一個號對應一個業務 //數據就緒、數據可讀、數據可寫 等等等等 process(key); } }
-
-
多路複用器監聽到有新的客戶端接入,處理新的接入請求,完成TCP三次握手,建立物理鏈路;設置客戶端鏈路爲非阻塞模式;將新接入的客戶端連接註冊到Reactor線程的多路複用器上,監聽讀操作,用來讀取客戶端發送的網絡信息
-
if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); //這個方法體現非阻塞,不管你數據有沒有準備好 //你給我一個狀態和反饋 SocketChannel channel = server.accept(); //一定一定要記得設置爲非阻塞 channel.configureBlocking(false); //當數據準備就緒的時候,將狀態改爲可讀 key = channel.register(selector, SelectionKey.OP_READ); }
-
異步讀取客戶端請求信息到緩衝區
//具體辦業務的方法,坐班櫃員 //每一次輪詢就是調用一次process方法,而每一次調用,只能幹一件事 //在同一時間點,只能幹一件事 // 每個都會跑完,每給都走不同的邏輯,就是不阻塞,然後線程可以去跑其他的,這就是不阻塞 private void process(SelectionKey key) throws IOException { //針對於每一種狀態給一個反應 // 是否就緒, if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); //這個方法體現非阻塞,不管你數據有沒有準備好 //你給我一個狀態和反饋 SocketChannel channel = server.accept(); //一定一定要記得設置爲非阻塞 channel.configureBlocking(false); //當數據準備就緒的時候,將狀態改爲可讀 key = channel.register(selector, SelectionKey.OP_READ); } // 是否可讀 else if (key.isReadable()) { //key.channel 從多路複用器中拿到客戶端的引用 SocketChannel channel = (SocketChannel) key.channel(); int len = channel.read(buffer); if (len > 0) { // buffer.flip(); String content = new String(buffer.array(), 0, len); key = channel.register(selector, SelectionKey.OP_WRITE); //在key上攜帶一個附件,一會再寫出去 key.attach(content); System.out.println("讀取內容:" + content); } // 是否可寫 } else if (key.isWritable()) { SocketChannel channel = (SocketChannel) key.channel(); String content = (String) key.attachment(); channel.write(ByteBuffer.wrap(("輸出:" + content).getBytes())); channel.close(); } }
-
-
優點:
-
客戶端發起的連接操作是異步的,可以通過在多路複用器註冊OP_CINNECT等待後續結果,不需要像之前的客戶端那樣被同步阻塞
-
SocketChannel的讀寫操作是異步的,如果沒有可讀寫的數據它不會同步等待,直接返回,這樣I/O通信線程就可以處理其他鏈路,
-
-
-
基於NIO2.0的異步非阻塞(AIO)編程
-
以後加
-
-
爲什麼要使用NIO編程
-
異步非阻塞
-
JDK 1.7 2002年 提供的NIO2 新增了異步的套接字通道,纔是真正的異步I/O,在異步I/O操作的時候,可以傳遞信號變量,當操作完之後,會回調相關的方法
-
JDK1.4 提供的NIO 實際上只能被稱爲非阻塞I/O,不能叫異步非阻塞
-
-
多路複用器
-
多路複用的核心,是通過Selector不斷地輪詢註冊在其上的Channel,如果某個Channel上面有新的TCP連接接入,讀和寫實際,這個Channel就處於就緒狀態,會被Seclettor輪詢出來,然後通過SelectionKEy,獲取就緒的Channel的集合,然後進行後續的I/O操作
-
-
僞異步I/O
-
線程池處理,爲了處理Tomcat通信線程同步I/O導致業務線程被掛住的問題
-
-
-
爲什麼選擇Netty
-
因爲Netty是NIO的升級版本,這叫好事者,自己寫太難了,
-
API使用簡單,門檻低
-
功能強大,預置了多種編解碼功能,支持多種主流協議
-
性能高
-