BIO即Block IO,阻塞式IO。
網絡編程的基本模型是Client/Server模型,也就是兩個進程之間進行相互通信,其中服務端提供位置信息(綁定的IP地址和監聽端口),客戶端通過連接操作向服務端監聽的地址發起連接請求,通過三次握手建立連接,如果連接建立成功,雙方就可以通過網絡套接字(Socket)進行通信。
在基于傳統同步阻塞模型開發中,ServerSocket負責綁定IP地址,啟動監聽端口:Socket負責發起連接操作。連接成功之后,雙方通過輸入和輸出流進行同步阻塞式通信。
傳統阻塞式IO
BIO服務端通信模型:采用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之后為每個客戶端創建一個新的線程進行鏈路處理,處理完之后,通過輸出流返回應答給客戶端,線程銷毀。
該模型最大的問題就是缺乏彈性伸縮能力,當客戶端并發訪問量則增加后,服務端的線程個數和客戶端并發訪問數呈1:1的正比關系,由于線程是java虛擬機非常寶貴的系統資源,當線程數膨脹之后,系統的性能 將急劇下降,隨著并發訪問量的繼續增大,系統會發生線程堆棧溢出、創建新線程失敗等問題,并最終導致進程宕機或者僵死,不能對外提供服務。
傳統的BIO
代碼演示:
服務端
public class TimeServer {
public static void main(String[] args) {
int port = 8081;
if(args!=null&&args.length>0){
try{
port = Integer.valueOf(args[0]);
}catch (Exception e){
e.printStackTrace();
}
}
ServerSocket serverSocket = null;
try{
serverSocket = new ServerSocket(port);
System.out.println("服務器已經啟動--端口號:"+port);
Socket socket = null;
while (true){
socket = serverSocket.accept();
new Thread(new TimeServerHandler(socket)).start();
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(serverSocket!=null){
System.out.println("服務器關閉");
try{
serverSocket.close();
}catch (Exception e1){
e1.printStackTrace();
}
serverSocket = null;
}
}
}
}
服務端處理器
public class TimeServerHandler implements Runnable {
public TimeServerHandler(Socket socket) {
this.socket = socket;
}
private 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("服務器收到消息:"+body);
currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new Date(System.currentTimeMillis()).toString():"BAD ORDER";
out.println(currentTime);
}
}catch (Exception e){
e.printStackTrace();
if(in!=null){
try{
in.close();
}catch (Exception e1){
e1.printStackTrace();
}
}
if(out!=null){
out.close();
out = null;
}
if(this.socket!=null){
try{
this.socket.close();
}catch (Exception e1){
e.printStackTrace();
}
this.socket = null;
}
}
}
}
客戶端
public class TimeClient {
public static void main(String[] args) {
int port = 8081;
if(args!=null&&args.length>0){
try{
port = Integer.valueOf(args[0]);
}catch (Exception e){
e.printStackTrace();
}
}
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("發送命令成功");
String resp = in.readLine();
System.out.println("收到的消息:"+resp);
}catch (Exception e){
e.printStackTrace();
}finally {
if(out!=null){
out.close();
out = null;
}
if(in!= null){
try{
in.close();
}catch (Exception e2){
e2.printStackTrace();
}
in = null;
}
if(socket!= null){
try{
socket.close();
}catch (Exception e2){
e2.printStackTrace();
}
socket = null;
}
}
}
}
傳統的BIO每當一個新的客戶端請求接入時,服務端必須創建一個新的線程處理接入的客戶端鏈路,一個線程只能處理一個客戶端連接。在高性能服務器應用領域,往往需要面向成千上萬個客戶端的并發連接,所以這種模型肯定無法滿足高性能高并發的 場景。
偽異步IO編程
偽異步的原理就是后端通過一個線程池來處理過個客戶端的請求接入,形成客戶端個數M:線程池最大線程數N的比例關系,其中M可以遠遠大于N。通過線程池可以靈活的調配線程資源,設置線程的最大值,防止由于海量并發接入導致線程耗盡。
具體實現:當有新的客戶端接入時,將客戶端的Socket封裝成一個Task(該任務實現Runnable接口)投遞到后端的線程池中進行處理,JDK的線程池維護一個消息隊列和N個活躍線程,對消息隊列中的任務進行處理。由于線程池可以設置消息隊列的大小和最大線程數,因此,它的資源占用是可控的,無論多少個客戶端并發訪問,都不會導致資源的耗盡和宕機。
代碼演示:
線程池
public class TimeServerHandlerExecutePool {
private ExecutorService executorService;
public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize){
this.executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
maxPoolSize,120L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize));
}
public void execute(Runnable task){
executorService.execute(task);
}
}
server代碼和原來差不多,只是將原來的創建線程改為使用線程池來執行這個任務。
serverSocket = new ServerSocket(port);
System.out.println("服務器已經啟動--端口號:"+port);
Socket socket = null;
TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(50,10000);//創建IO任務線程池
while (true){
socket = serverSocket.accept();
singleExecutor.execute(new TimeServerHandler(socket));
}
偽異步IO弊端分析:
read
當對Socket的輸入流進行讀取操作的時候,它會一直阻塞下去,知道發生如下三種事件。
- 有數據可讀
- 可用數據已經讀取完畢
- 發生空指針異常或者IO異常
這意味著當對方發送請求或者應答消息比較緩慢,或者網絡傳輸較慢時,讀取輸入流一方的通信線程將被長時間阻塞,如果對方要60s才能夠將數據發送完畢,讀取一方的IO線程也將被同步阻塞60s,在此期間,其他接入消息只能在消息隊列中排隊。
write
當調用OutputStream的write方法寫輸出流的時候,它將會被阻塞,知道所有要發送的字節全部寫入完畢,或者發生異常。學習過TCP/IP相關知識的人都知道,當消息的接收方處理緩慢的時候,將不能及時的從TCP緩沖區讀取數據,這將會導致發送方的TCP window size不斷減少,知道為0,雙方處于Keep-Alive狀態,消息發送方將不能再向TCP緩沖區寫入消息,這時如果采用的是同步阻塞IO,write操作將會被無線阻塞,知道TCP window size大于0或者發生IO異常。
通過對輸入和輸出流的API進行分析,讀和寫操作都是同步阻塞的,阻塞的時間取決于對方對方IO線程的處理速度和網絡IO的傳輸速度。本質上來講,我們無法保證生產環境的網絡狀況和對端的應用程序能足夠快,如果我們的應用程序依賴對方的處理速度,它的可靠性就非常差。