《Netty 權威指南》—— 傳統的BIO程式設計
宣告:本文是《Netty 權威指南》的樣章,感謝博文視點授權併發程式設計網站釋出樣章,
網路程式設計的基本模型是Client/Server模型,也就是兩個程序之間進行相互通訊,其中服務端提供位置資訊(繫結的IP地址和監聽埠),客戶端通過連線操作向服務端監聽的地址發起連線請求,通過三次握手建立連線,如果連線建立成功,雙方就可以通過網路套接字(Socket)進行通訊。
在基於傳統同步阻塞模型開發中,ServerSocket負責繫結IP地址,啟動監聽埠,Socket負責發起連線操作,連線成功之後,雙方通過輸入和輸出流進行同步阻塞式通訊。
下面,我們就以經典的時間伺服器(TimeServer)為例,通過程式碼分析來回顧和熟悉下BIO程式設計。
2.1.1.BIO通訊模型圖
首先,我們通過下面的通訊模型圖來熟悉下BIO的服務端通訊模型:採用BIO通訊模型的服務端,通常由一個獨立的Acceptor執行緒負責監聽客戶端的連線,它接收到客戶端連線請求之後為每個客戶端建立一個新的執行緒進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端,執行緒銷燬。這就是典型的一請求一應答通訊模型。
該模型最大的問題就是缺乏彈性伸縮能力,當客戶端併發訪問量增加後,服務端的執行緒個數和客戶端併發訪問數呈1:1的正比關係,由於執行緒是JAVA虛擬機器非常寶貴的系統資源,當執行緒數膨脹之後,系統的效能將急劇下降,隨著併發訪問量的繼續增大,系統會發生執行緒堆疊溢位、建立新執行緒失敗等問題,並最終導致程序宕機或者僵死,不能對外提供服務。
下面的兩個小節,我們會分別對服務端和客戶端進行原始碼分析,尋找同步阻塞IO的弊端。
2.1.1.同步阻塞式IO建立的TimeServer原始碼分析
同步阻塞IO的TimeServer:
public class TimeServer { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException 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(); new Thread(new TimeServerHandler(socket)).start(); } } finally { if (server != null) { System.out.println("The time server close"); server.close(); server = null; } } } }
imeServer根據傳入的引數設定監聽埠,如果沒有入參,使用預設值8080,20行通過建構函式建立ServerSocket,如果埠合法且沒有被佔用,服務端監聽成功。23-26行通過一個無限迴圈來監聽客戶端的連線,如果沒有客戶端接入,則主執行緒阻塞在ServerSocket的accept操作上。啟動TimeServer,通過JvisualVM列印執行緒堆疊,我們可以發現主程式確實阻塞在accept操作上,如下圖所示:
當有新的客戶端接入的時候,執行程式碼25行,以Socket為引數構造TimeServerHandler物件,TimeServerHandler是一個Runnable,使用它為建構函式的引數建立一個新的客戶端執行緒處理這條Socket鏈路。下面我們繼續分析TimeServerHandler的程式碼。
同步阻塞IO的TimeServerHandler:
public class TimeServerHandler implements Runnable { private Socket socket; public TimeServerHandler(Socket socket) { this.socket = socket; } /* * (non-Javadoc) * * @see java.lang.Runnable#run() */ @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; } } } }
25行通過BufferedReader讀取一行,如果已經讀到了輸入流的尾部,則返回值為null,退出迴圈。如果讀到了非空值,則對內容進行判斷,如果請求訊息為查詢時間的指令”QUERY TIME ORDER”則獲取當前最新的系統時間,通過PrintWriter的println函式傳送給客戶端,最後退出迴圈。程式碼35-52行釋放輸入流、輸出流、和Socket套接字控制代碼資源,最後執行緒自動銷燬並被虛擬機器回收。
在下一個小結,我們將介紹同步阻塞IO的客戶端程式碼,然後分別執行服務端和客戶端,檢視下程式的執行結果。
2.1.1.同步阻塞式IO建立的TimeClient原始碼分析
客戶端通過Socket建立,傳送查詢時間伺服器的”QUERY TIME ORDER”指令,然後讀取服務端的響應並將結果打印出來,隨後關閉連線,釋放資源,程式退出執行。
同步阻塞IO的TimeClient:
public class TimeClient { /** * @param args */ public static void main(String[] args) { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException 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 (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; } } } }
第23行客戶端通過PrintWriter向服務端傳送”QUERY TIME ORDER”指令,然後通過BufferedReader的readLine讀取響應並列印。
分別執行服務端和客戶端,執行結果如下:
服務端執行結果如下:
客戶端執行結果如下:
到此為止,同步阻塞式IO開發的時間伺服器程式已經講解完畢,我們發現,BIO主要的問題在於每當有一個新的客戶端請求接入時,服務端必須建立一個新的執行緒處理新接入的客戶端鏈路,一個執行緒只能處理一個客戶端連線。在高效能伺服器應用領域,往往需要面向成千上萬個客戶端的併發連線,這種模型顯然無法滿足高效能、高併發接入的場景。
為了改進一執行緒一連線模型,後來又演進出了一種通過執行緒池或者訊息佇列實現1個或者多個執行緒處理N個客戶端的模型,由於它的底層通訊機制依然使用同步阻塞IO,所以被稱為 “偽非同步”,下面章節我們就對偽非同步程式碼進行分析,看看偽非同步是否能夠滿足我們對高效能、高併發接入的訴求。