揭開網絡編程的神祕面紗1(BIO、偽異步IO、NIO、AIO)

什麼是socket?

網絡上的兩個程序通過一個雙向的通信連接實現數據的交換,這個連接的一端稱為socket。socket編程進行的是端到端的通信,基於網絡層和傳輸層的實現。在網絡層,socket函數需要指定底層是使用IPV4還是IPV6協議,傳輸層需要指定是tcp還是udp。

基於TCP協議的socket調用過程

揭開網絡編程的神秘面紗1(BIO、偽異步IO、NIO、AIO)

基於UDP協議的socket調用過程

揭開網絡編程的神秘面紗1(BIO、偽異步IO、NIO、AIO)

java中的socket

java中的socket可以分為普通Socket和NioSocket兩種。

普通Socket分為ServerSocket和Socket兩大類,ServerSocket用於服務端,通過accept方法監聽請求,監聽到請求後返回Socket,Socket用於完成數據傳輸,客戶端直接使用Socket發起請求並傳輸數據。

NioSocket是JDK1.4新增加的IO模式,底層採用新的處理方式,大大提高java IO的效率。Socket也屬於IO的一種,NioSocket提供相應的類:ServerSocketChannel和SocketChannel,分別對應Socket中的ServerSocket和Socket。

BIO編程(同步阻塞IO)

同步和異步

同步和異步是針對應用程序和內核的交互而言的,同步指的是用戶進程出發IO操作並等待或者輪詢地去查看IO操作是否就緒,而異步是指用戶進程觸發IO操作以後便開始做自己的事情,當IO操作完成時,會得到IO完成的通知。

阻塞和非阻塞

阻塞和非阻塞是針對於進程在訪問數據的時候,根據IO操作的就緒狀態來採取的不同方式,說白了是一種讀取或者寫入操作方法的實現方式,阻塞方式下讀取或者寫入函數將一直等待,而非阻塞方式下,讀取或者寫入方法會立即返回一個狀態值。

採用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後為每個客戶端創建一個新的線程進行鏈路處理,處理完成後,通過輸出流返回應答給客戶端,線程銷燬。

揭開網絡編程的神秘面紗1(BIO、偽異步IO、NIO、AIO)

BIO模型,服務器實現模式為一個連接一個線程,也就是客戶端有連接請求時服務端就需要啟動一個線程來進行處理,如果這個連接不做任何事情會造成不必要的線程開銷。

ServerSocket server = new ServerSocket(portNumber); //創建一個新的ServerSocket,用以監聽指定端口上的連接請求
......
Socket clientSocket;
while(true){
\t\t\t\tclientSocket = serverSocket.accept(); //對accept()方法的調用將被阻塞,直到一個連接建立
\t\t\t
\t\t\tnew Thread(new ServerHandler(clientSocket)).start(); //創建線程,處理clientSocket鏈路上的數據
}

ServerSocket上的accept()方法將會一直阻塞,直到一個連接建立,隨後返回一個新的Socket用於客戶端和服務端之間的通信。ServerSocket將繼續監聽傳入的連接。

當客戶端併發訪問量增加後,服務端的線程個數和客戶端併發數將以1:1的關係增長,由於線程是比較寶貴的系統資源,線程數量快速膨脹後,系統的性能將急劇下降,最終系統將掛掉。所以BIO模型只適用於連接數目比較小且固定的架構。

偽異步I/O編程

為了避免為每個客戶端請求都創建一個獨立的線程造成的線程資源耗盡問題,我們服務端把客戶端的Socket請求封裝成一個Task(實現java.lang.Runnable接口),放到線程池進行管理。線程池維護一個消息隊列和N個活躍線程對消息隊列中的任務進行處理。這種方式叫做偽異步I/O編程。

揭開網絡編程的神秘面紗1(BIO、偽異步IO、NIO、AIO)

由於線程池可以設置消息對壘的大小和最大線程數,因此,它的資源佔用是可控制的。

ServerSocket server = new ServerSocket(portNumber); //創建一個新的ServerSocket,用以監聽指定端口上的連接請求

ExecutorService executorService = Executors.newFixedThreadPool(60); //創建線程池
......
Socket clientSocket;
while(true){
\t\t\t\tclientSocket = serverSocket.accept(); //對accept()方法的調用將被阻塞,直到一個連接建立
\t\t\t

\t\t\texecutorService.execute(new ServerHandler(clientSocket));//把clientSocket請求提交給線程池,在線程池中處理clientSocket鏈路上的數據
}

我們可以根據需求,選擇合適的線程池。如果使用CachedThreadPool線程池,除了能自動幫我們管理線程(線程複用),客戶端與線程數也是1:1的模型。而如果使用FixedThreadPool線程池,我們可以有效控制線程的最大數量,保證系統線程資源的控制,實現N:M的偽異步IO模型,由此引入的問題也一目瞭然,由於限制了線程數量,如果發生大量客戶端請求時並且超過最大線程數,只能把請求放到隊列中等待,直到線程池中有空閒的線程可以複用。這也就會導致Socket一直阻塞。

如果對線程池不夠了解的同學,可以查閱我的文章《 》。

下文講解NIO、AIO編程。


分享到:


相關文章: