NIO(生活篇)
今晚是個下雨天,寫完今天最後一行程式碼,小魯班起身合上電腦,用滾燙的開水為自己泡製了一桶老壇酸菜牛肉麵。這大概是苦逼程式猿給接下來繼續奮戰的自己最好的饋贈。年輕的程式猿更偏愛坐在窗前,在夜晚中靜靜的享受獨特的泡麵香味。。。
科班出身的小魯班雖然寫了N多複雜(CRUD)程式碼,但仍口味清淡,他們往往不加或少加料包,由泡麵熱騰騰的蒸汽燻蒸自己的臉頻,潤溼又幹又澀的雙眼,撫慰受傷的心靈。然後,看著外邊依然還是熙熙攘攘的車流和不屬於自己的任何一個亮燈的視窗,卻思考著如何才能成為ー個名垂青史的程式猿。小魯班不迷茫。。。
"我們一起學貓叫,一起喵喵喵"~~~小魯班放在書桌上的大哥大手機突然響了,打破了小魯班腦子裡美好的yy
小魯班心想:都這麼晚了,誰TM還打電話過來,拿起電話一看,哦原來是他表哥魯班大師
魯班大師:小老弟。晚上好嘛!
小魯班:嚶嚶嚶,原來是大表哥呀,能和你通話真讓我難以置信呀。
魯班大師:聽說你今天早上請兩老去館子喝早茶去了呀,有錢人,看來混的很不錯嘛。
小魯班:哎,別提了,等了半天才通知有位置( 接待阻塞 ),坐下之後又沒人來負責寫選單( 點餐阻塞 ),寫完選單又沒有人負責上菜,我去~氣死老子
魯班大師:哈哈哈哈哈,這館子的老闆也太奧特曼(out)了,現在規模大點的飯館都採用IO/">NIO( 同步非阻塞IO )模式啦。
小魯班:額?,NIO是什麼鬼,這和飯館有什麼關係呢?
魯班大師: emmmmm,故事得從一段很長很長的網路程式設計模式歷史開始說起呢~
S1.傳統的網路程式設計模式(單執行緒下的通訊)
在單執行緒模式下,IO操作沒完成的時候,無法返回,造成伺服器執行緒阻塞,其他客戶端不能連上服務端。
在只有一個餐廳服務員的情況下,服務員接待了一位客人,客人到餐桌上坐下後,服務員等待客人點餐,此時又有一個客人來吃飯,但是已經沒有服務員去接待了,因為這個服務員在等待第一個客人點餐,直到第一個客人點完餐後,服務員把選單交給廚房,然後才能去接待第二個進來的客人。。。(這樣的服務客人早就走了)
那麼我們來看看如何改進
S2改良後網路程式設計模式(多執行緒)
在S1中我們發現了一些問題,當IO阻塞的時候,服務端無法接受請求,因此S2改用了多執行緒模式
在多執行緒模式下,只要有客戶端連進來,我們都會為之建立一個執行緒專門去處理客戶端的IO操作。當完成之後,執行緒就會自動銷燬。但是這樣會帶來一個問題,就是執行緒的頻繁的建立和銷燬非常消耗伺服器的資源。
飯館裡的老闆面對這種情況,只好繼續請服務員去寫選單了,來一個客人,就請一個服務員去負責客人的單子,問題是請服務員非常消耗老闆的money呀,而且當寫完單子後又要計算工資,這個過程非常耗時間。
S3繼續改良後的網路程式設計模式(執行緒池)
S2我們發現了這樣的問題就是執行緒的建立和銷燬非常損耗系統的效能,因此我們想到JDBC中連線池的解決方案,同樣的,這裡我們可以建立執行緒池
啟動服務後,事先建立100個執行緒,當有客戶端連進來的時候,不需要建立,就給他分配一個執行緒用於IO讀寫操作,當客戶完成IO操作完成之後就歸還執行緒到執行緒池中而不是銷燬,這樣做的好處就是解決了在執行的時候執行緒建立和銷燬對系統資源的損耗。同時也暴露了一些問題,其一在高併發的情況下,執行緒池中的執行緒不夠用了,此時會造成客戶端等待阻塞(當然也可以繼續建立執行緒來解決),其二多執行緒環境下由於執行緒搶奪CPU資源的隨機性,使得執行緒一起頻繁的進行上下文的切換,消耗的大量的資源,而這些資源本來是用來處理業務用的,現在則用來切換執行緒,這就大大的降低了系統有效的資源真正利用率。
老闆覺得一直請人不划算,乾脆就請30個人是在餐廳一個角落待命,當有客人坐下來的時候,就分配一個服務員去點餐。但是當有31客人同時來的時候,假設30個服務員都在等待寫單,那麼第31個客人假如要點餐的時候就沒人為他服務了,同時點完餐時候的,突然客人想加餐,此時每個服務員都想著去搶到這個客戶,競爭過程消耗了時間,同時得知道剛剛的賬單都點了些什麼還要交接任務,就更浪費人力物力。
S4再次改良後的網路程式設計模式(NIO)
S3我們發現執行緒池不夠用,以及高併發情況下頻繁執行緒搶奪CPU資源的損耗效能的問題。主要原因都是執行緒太多引起的,因此我們可以通過改變執行緒的建立時機,不是Socket剛剛連上來的時候建立執行緒,而是等待需要進行IO操作的時候再去建立執行緒。
這張圖對比上面的題我們發現多個三個陌生的面孔,下面介紹一下他們
Channel表示為一個已經建立好的支援I/O操作的實體(如檔案和網路)的連線,在此連線上進行資料的讀寫操作,使用的是 Buffer 緩衝區來實現讀寫。
ServerSocketChannel------->open() 獲得例項 ----------->register(selector,accept) 將通道管理器和該通道繫結,併為該通道註冊SelectionKey.OP_ACCEPT事件
Selector一個專門的選擇器來同時對多個Socket通道進行監聽(輪詢,不算阻塞),當其中的某些Socket通道上有它感興趣的事件發生時,這些通道就會變為可用狀態,當狀態是IO狀態的時候,就會為他分配一個執行緒處理業務,當不是IO狀態的時候只會為他註冊一個連線,不會分配執行緒,這樣的話就保證了,系統中存在的執行緒都是用來處理業務的而不是用來等待的,這樣就能夠減少執行緒,也就減少了執行緒上下文的切換損耗資源。
Selector----->open()獲得例項------>select()監聽動作(讀還是寫還是連線,相當於之前的accept()方法),通過原始碼發現SelectorProvider.provider().poll()依賴於作業系統建立
Buffer緩衝區,裡邊有些方法例如clear、flip、rewind都是操作limit和position的值來實現重複讀寫,這樣的話IO就不會阻塞,不會出現客戶端在寫入的時候,服務端不能寫出造成執行緒的阻塞。
position(初始的位置,讀的時候,位置會移動)
limit(當你讀取完成了,資料需要進行固定flip(),limit=position)
capacity(陣列大小的一個容量)
clear()把position迴歸到原位
其實這裡的Selector相當於一個接待主管,當有一個客人從大門(Channel)進來來吃飯的時候,先帶它到位置上,給他安排一個臺號,然後一直監聽客人的需求,當客人需要點餐的時候,此時接待主管監聽到了,就立馬給他分配一個服務員去幫你它完成點餐,當客人需要加餐的時候,接待主管分配服務員到指定臺號,然後只需要在賬單(Buffer)上新增即可!
小魯班:哇塞,有點暈,但是我還是能看懂的,這些都是概念,表哥有程式碼麼?
魯班大師:程式碼嘛,我今天打排位的時候用魯班被噴沒面板,咳咳!
小魯班:面板好說好說!
1.OIO服務端程式碼
public class OioServer { @SuppressWarnings("resource") public static void main(String[] args) throws Exception { ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); //建立socket服務,監聽10101埠 ServerSocket server=new ServerSocket(10101); System.out.println("伺服器啟動!"); while(true){ //獲取一個套接字(阻塞) final Socket socket = server.accept(); System.out.println("來個一個新客戶端!"); newCachedThreadPool.execute(new Runnable() { @Override public void run() { //業務處理 handler(socket); } }); } } /** * 讀取資料 * @param socket * @throws Exception */ public static void handler(Socket socket){ try { byte[] bytes = new byte[1024]; InputStream inputStream = socket.getInputStream(); while(true){ //讀取資料(阻塞) int read = inputStream.read(bytes); if(read != -1){ System.out.println(new String(bytes, 0, read)); }else{ break; } } } catch (Exception e) { e.printStackTrace(); }finally{ try { System.out.println("socket關閉"); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
2.NIO服務端程式碼
public class NIOServer { // 通道管理器 private Selector selector; /** * 獲得一個ServerSocket通道,並對該通道做一些初始化的工作 * * @param port *繫結的埠號 * @throws IOException */ public void initServer(int port) throws IOException { // 獲得一個ServerSocket通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 設定通道為非阻塞 serverChannel.configureBlocking(false); // 將該通道對應的ServerSocket繫結到port埠 serverChannel.socket().bind(new InetSocketAddress(port)); // 獲得一個通道管理器 this.selector = Selector.open(); // 將通道管理器和該通道繫結,併為該通道註冊SelectionKey.OP_ACCEPT事件,註冊該事件後, // 當該事件到達時,selector.select()會返回,如果該事件沒到達selector.select()會一直阻塞。 serverChannel.register(selector, SelectionKey.OP_ACCEPT); } /** * 採用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理 * * @throws IOException */ public void listen() throws IOException { System.out.println("服務端啟動成功!"); // 輪詢訪問selector while (true) { // 當註冊的事件到達時,方法返回;否則,該方法會一直阻塞 selector.select(); // 獲得selector中選中的項的迭代器,選中的項為註冊的事件 Iterator<?> ite = this.selector.selectedKeys().iterator(); while (ite.hasNext()) { SelectionKey key = (SelectionKey) ite.next(); // 刪除已選的key,以防重複處理 ite.remove(); handler(key); } } } /** * 處理請求 * * @param key * @throws IOException */ public void handler(SelectionKey key) throws IOException { // 客戶端請求連線事件 if (key.isAcceptable()) { handlerAccept(key); // 獲得了可讀的事件 } else if (key.isReadable()) { handelerRead(key); } } /** * 處理連線請求 * * @param key * @throws IOException */ public void handlerAccept(SelectionKey key) throws IOException { ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 獲得和客戶端連線的通道 SocketChannel channel = server.accept(); // 設定成非阻塞 channel.configureBlocking(false); // 在這裡可以給客戶端傳送資訊哦 System.out.println("新的客戶端連線"); // 在和客戶端連線成功之後,為了可以接收到客戶端的資訊,需要給通道設定讀的許可權。 channel.register(this.selector, SelectionKey.OP_READ); } /** * 處理讀的事件 * * @param key * @throws IOException */ public void handelerRead(SelectionKey key) throws IOException { // 伺服器可讀取訊息:得到事件發生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 建立讀取的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); int read = channel.read(buffer); if(read > 0){ byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("服務端收到資訊:" + msg); //回寫資料 ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes()); channel.write(outBuffer);// 將訊息回送給客戶端 }else{ System.out.println("客戶端關閉"); key.cancel(); } } /** * 啟動服務端測試 * * @throws IOException */ public static void main(String[] args) throws IOException { NIOServer server = new NIOServer(); server.initServer(8000); server.listen(); } }
魯班大師:好好理解,有什麼不懂的就留言,你表哥先撤了,面板可別忘了送,不早了晚安~
小魯班:好滴!