1.1 I/O基礎入門
io的缺陷:只有輸入輸出流,同步阻塞(bio),導致通信線程被長時間阻塞,字符集有限,硬件可移植行不好。
1.1.1、linux的網絡IO模型簡介
1)阻塞io模型,默認情況下,所有文件操作都是阻塞的。2)非阻塞模型,從應用數據到內核,3)IO複用模型,進程通過一個或多個fd傳遞給select或poll系統調用,阻塞在select操作上,select判斷是否就緒,是,立即回調rollback,4)信號驅動io模型,通過系統調用sigaction執行一個信號處理函數,5)異步io,
2.1、傳統的bio編程
網絡編程的基本模型是client/server模型,也就是兩個進程之間進行相互通信,其中服務器提供位置信息,(地址和端口),客戶端通過連接操作向服務端監聽的地址發起連接,通過三次握手建立連接,雙方通過socket進行通信。
我們先看看bio的通信模型圖
解釋:bio通信 由一個獨立的acceptor的線程負責監聽客戶端的鏈接,它接受到客戶端連接請求後爲每個客戶端創建一個新的線程進行鏈路處理,處理完成之後,通過輸出流應該給客戶端,線程銷燬,爲1對1的 請求應答模型。
服務端線程和客戶端線程比例呈1:1的正比關係,當線程或併發訪問量增大時候,系統可能會發生線程堆棧溢出,創建失敗,宕機僵死等問題。
下面是對以上預測的實戰推導
2.1.2同步阻塞io創建源碼分析
public class TimeServer { public static void main(String[] args) throws IOException { int port=8080; if (args != null && args.length > 0) { port = Integer.valueOf(args[0]); } ServerSocket serverSocket=null; try { serverSocket = new ServerSocket(port); System.out.println("the time server is start in port:"+port); Socket socket=null; while (true){ socket=serverSocket.accept(); new Thread(new TimeServerHandler(socket)).start(); } } catch (IOException e) { e.printStackTrace(); }finally { if(serverSocket!=null){ System.out.println("the time server close"); serverSocket.close(); serverSocket=null; } } } }
TimeServer根據傳入的參數設置監聽端口,如果沒有入參,使用默認值8080,通過構造函數創建一個serversocket,端口合法,則監聽成功,通過一個死循環來監聽客戶端的鏈接。若無客戶端,則一直阻塞
public class TimeServerHandler implements Runnable{ private Socket socket; public TimeServerHandler(Socket socket) { this.socket = socket; } @Override public void run() { BufferedReader in = null; PrintWriter out = null; try { in = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); out = new PrintWriter(this.socket.getOutputStream(), true); String currentTime = null; String body = null; while (true) { body = in.readLine(); if (body == null) break; System.out.println("The time server receive order : " + body); currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date( System.currentTimeMillis()).toString() : "BAD ORDER"; out.println(currentTime); } } catch (Exception e) { if (in != null) { try { in.close(); } catch (IOException e1) { e1.printStackTrace(); } } if (out != null) { out.close(); out = null; } if (this.socket != null) { try { this.socket.close(); } catch (IOException e1) { e1.printStackTrace(); } this.socket = null; } } } }
body = in.readLine();若讀到輸入流的尾部,則返回null,讀到非空,則對內容判斷,有標誌關鍵字,則通過printwriter發送給客戶端,退出循環
啓動後服務端線程一直在運行
2.1.3 同步阻塞io創建TimeClient
客戶端通過socket創建,發送查詢時間服務器的"QUERY TIME ORDER"指令,然後讀取服務端的響應將結果打印出來,隨後關閉鏈接,釋放資源。
public class TimeClient { public static void main(String[] args) { int port = 8080; if (args != null && args.length > 0) { port = Integer.valueOf(args[0]); } Socket socket = null; BufferedReader in = null; PrintWriter out = null; try { socket = new Socket("127.0.0.1", port); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(), true); out.println("QUERY TIME ORDER"); System.out.println("Send order 2 server succeed."); String resp = in.readLine(); System.out.println("Now is : " + resp); } catch (Exception e) { //不需要處理 } finally { if (out != null) { out.close(); out = null; } if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } in = null; } if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } socket = null; } } } }
客戶端
服務端
以上數據,就可發現BIO主要的問題在於每當有一個新的客戶端請求接入時,服務端必須創建一個新的線程處理新接入的客戶端鏈路,一個線程只能處理一個客戶端連接。在高性能服務器應用領域,往往需要面向成千上萬個客戶端的併發連接,這種模型顯然無法滿足高性能、高併發接入的場景。
爲了改進一線程一連接模型,後來又演進出了一種通過線程池或者消息隊列實現1個或者多個線程處理N個客戶端的模型,由於它的底層通信機制依然使用同步阻塞IO,所以被稱爲 “僞異步”,
2.2.1僞異步io測試和分析
再以上的TimeServer新增了兩行代碼,其他不變,同時新增了一個線程池類,timeServerHandlerExecutepool
public class TimeServerHandlerExecutePool { private ExecutorService executor; public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) { executor = new ThreadPoolExecutor(Runtime.getRuntime() .availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue(queueSize)); } public void execute(java.lang.Runnable task) { executor.execute(task); } }
參數解釋:
corePoolSize
核心線程數,默認情況下核心線程會一直存活,即使處於閒置狀態也不會受存keepAliveTime限制。除非將allowCoreThreadTimeOut設置爲true。
maximumPoolSize
線程池所能容納的最大線程數。超過這個數的線程將被阻塞。當任務隊列爲沒有設置大小的LinkedBlockingDeque時,這個值無效。
keepAliveTime
非核心線程的閒置超時時間,超過這個時間就會被回收。
unit
指定keepAliveTime的單位,如TimeUnit.SECONDS。當將allowCoreThreadTimeOut設置爲true時對corePoolSize生效。
workQueue
線程池中的任務隊列.
常用的有三種隊列,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue。
threadFactory
線程工廠,提供創建新線程的功能。ThreadFactory是一個接口,只有一個方法
以上方法,由於線程池和消息隊列都是有界的,因此客戶端併發連接數多大,都不會導致服務端內存溢出,但是底層通信依然是同步阻塞模型,
總結:同步阻塞模型的io,在對socket的輸入流進行讀取操作的時候,它會一直阻塞直到發生,有數據可讀或,可用數據已經讀取完畢,或發生空指針或io異常,纔會結束,但是在網絡傳送上,我們無法保證網絡狀況和對端應用程序足夠快,所以,同步阻塞的消息傳送無法滿足。