【網絡編程】僞異步I/O編程

上次說了BIO編程。爲了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題,後來人們對它的線程模型進行了優化,服務器端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數M對應線程池N的比例關係。通過線程池可以靈活調配線程資源,設置線程的最大值,防止由於海量併發客戶端接入導致線程耗盡。這就是僞異步I/O編程。

如下圖所示,採用線程池和任務隊列可以實現僞異步I/O通信框架。

僞異步I/O服務器端通信模型服務器端代碼:

package com.test.aio1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
 * 僞異步I/O服務器端
 * @author 程就人生
 * @Date
 */
public class HelloServer {

  public static void main( String[] args ){
    int port = 8080;
        ServerSocket serverSocket = null;
        try {
      serverSocket = new ServerSocket(port);
      Socket socket = null;
      HelloServerHandleExecutePool execute = new HelloServerHandleExecutePool(50, 1000);
      // 通過無限循環監聽客戶端連接
      while(true){
        // 如果沒有客戶端連接,則阻塞在Accept操作上
        socket = serverSocket.accept();
        // 如果有客戶端連接,則加入線程池執行
        execute.execute(new HelloHandler(socket));
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    }
}

/**
 * 線程池類
 * @author 程就人生
 * @Date
 */
class HelloServerHandleExecutePool{  

  private ExecutorService executorService;  
  
  public HelloServerHandleExecutePool(int maxPoolSize, int queueSize) {    
    executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<java.lang.Runnable>(queueSize));
  }
  
  public void execute(java.lang.Runnable task){
    executorService.execute(task);
  }  
}

/**
 * hello處理類
 * @author 程就人生
 * @Date
 */
class HelloHandler implements Runnable{
  
  private Socket socket;

  public HelloHandler(Socket socket) {
    this.socket = socket;
  }

  public void run() {
    BufferedReader reader = null;
    PrintWriter writer = null;
    try {
      reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
      // 返回給客戶端,是否刷新設置爲true
      writer = new PrintWriter(this.socket.getOutputStream(), true);
      String body = null;
      while(true){
        body = reader.readLine();
        if(body == null){
          break;
        }
        System.out.println("服務器端接收到的:" + body);
        writer.println("來自服務器端的響應!");        
      }
    } catch (IOException e) {
      e.printStackTrace();
      // 出現異常時,對資源的釋放
      if(reader != null){
        try {
          reader.close();
        } catch (IOException e1) {
          e1.printStackTrace();
        }
      }
      if(writer != null){
        writer.close();
        writer = null;
      }
      if(socket != null){
        try {
          socket.close();
        } catch (IOException e1) {
          e1.printStackTrace();
        }
      }
    }
  }  
}

僞異步I/O服務器端的主函數有了變化,先創建一個Hello服務處理類的線程池。當接收到新的客戶端連接時,將請求soket封裝成一個Task,然後調用線程池的execute方法執行,從而避免每個客戶端連接時都創建一個新線程。由於線程池和消息隊列都是有界的。因此,無論客戶端併發連接數多大,都不會導致服務器端線程個數過於膨脹而導致內存溢出,相比於傳統的BIO線程模型是一種改良。 運行服務器端結果。

雖然僞異步I/O通信框架採用了線程池實現,因此避免了每個請求都創建一個獨立線程造成的線程資源耗盡問題,但它的底層的通信依舊採用同步阻塞模型,因此無法從根本上解決問題。一起看下socket的InputStream方法。

再看下InputStream類。


當對Socket的輸入流進行讀取操作時,它會一直堵塞下去,直到發生三種事件:

  • 有數據可讀。
  • 可用數據已經讀完。
  • 發生異常。

如果對方發生請求或者應答比較緩慢,或者網絡傳輸比較慢,讀取輸入流一方的通信線程將會被長時間阻塞。如果對方要60s才能將數據發送完成,讀取一方的I/O也會被阻塞60s。在這60s期間,其他接入的客戶端只能在消息隊列中排隊。僞異步I/O只是對BIO線程模型的一個簡單優化,無法從根本上解決同步IO導致的通信線程阻塞問題。
僞異步I/O的缺點顯而易見。

  • 如果客戶端請求量增加,會導致服務器端處理應答消息緩慢。
  • 如果有故障節點,由於讀取輸入流的阻塞的,它會被阻塞,直到釋放。
  • 如果所有的線程都被故障服務器阻塞,那麼後續的IO消息都將在隊列中排隊等等。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章