Java IO編程全解(二)——傳統的BIO編程
前面講到:Java IO編程全解(一)——Java的I/O演進之路
網絡編程的基本模型是Client/Server模型,也就是兩個進程之間進行相互通信,其中服務端提供位置信息(綁定的IP地址和監聽端口),客戶端通過連接操作向服務端監聽的地址發起連接請求,通過三次握手建立連接,如果連接建立成功,雙方就可以通過網絡套接字(Socket)進行通信。
在基於傳統同步阻塞模型開發中,ServerSocket負責綁定IP地址,啟動監聽端口;Socket負責發起連接操作。連接成功之後,雙方通過輸入和輸出流進行同步阻塞式通信。
以經典的時間服務器(TimeServer)為例,通過代碼分析來回顧和熟悉下BIO編程。
1. BIO通信模型圖
如下圖BIO的服務端通信模型:采用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後為每個客戶端創建一個新的線程進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端,線程銷毀。這就是典型的一請求一應答通信模型。
圖1 同步阻塞I/O服務端通信模型(一客戶端一線程)
該模型最大的問題就是缺乏彈性伸縮能力,當客戶端並發訪問量增加後,服務端的線程個數和客戶端並發訪問數呈1:1 的正比關系,猶豫線程是Java虛擬機非常寶貴的系統資源,當線程數膨脹之後,系統的性能將急劇下降,隨著並發訪問量的繼續增大,系統會發生線程堆棧溢出、創建新線程失敗等問題,並最終導致進程宕機或者僵死,不能對外提供服務。
2. 同步阻塞式I/O創建的TimeServer
package joanna.yan.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class TimeServer { public static void main(String[] args) { int port=9090; if(args!=null&&args.length>0){ try { port=Integer.valueOf(args[0]); } catch (Exception e) { // 采用默認值 } } ServerSocket server=null; try { server=new ServerSocket(port); System.out.println("The time server is start in port:"+port); Socket socket=null; while(true){//通過一個無限循環來監聽客戶端的連接 socket=server.accept();//如果沒有客戶端接入,則主線程阻塞在ServerSocket的accept操作上。 new Thread(new TimeServerHandler(socket)).start(); } } catch (IOException e) { e.printStackTrace(); }finally{ if(server!=null){ System.out.println("The time server close"); try { server.close(); server=null; } catch (IOException e) { e.printStackTrace(); } } } } }
package joanna.yan.bio; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Date; 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); //如果請求消息為查詢時間的指令"QUERY TIME ORDER"則獲取當前最新的系統時間。 currentTime="QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER"; out.println(currentTime); } } catch (IOException e) { e.printStackTrace(); }finally{ if(in!=null){ try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if(out!=null){ out.close(); out=null; } if(this.socket!=null){ try { this.socket.close(); this.socket=null; } catch (IOException e) { e.printStackTrace(); } } } } }
3. 同步阻塞式I/O創建的TimeClient
客戶端通過Socket創建,發送查詢時間服務器的“QUERY TIME ORDER”指令,然後讀取服務端的響應並將結果打印出來,隨後關閉連接,釋放資源,程序退出執行。
package joanna.yan.bio; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; public class TimeClient { public static void main(String[] args) { int port=9090; if(args!=null&&args.length>0){ try { port=Integer.valueOf(args[0]); } catch (Exception e) { // 采用默認值 } } 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 (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }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個客戶端的模型,由於它的底層通信機制依然使用同步阻塞I/O,所以被稱為“偽異步”。後面我們將通過對偽異步代碼的分析,看看偽異步能否滿足我們對高性能、高並發接入的訴求。
如果此文對您有幫助,微信打賞我一下吧~
Java IO編程全解(二)——傳統的BIO編程