IO程式設計和NIO程式設計簡介

海晨憶發表於2018-02-12

個人部落格:haichenyi.com。感謝關注

  傳統的同步阻塞I/O通訊模型,導致的結果就是隻要有一方處理資料緩慢,都會影響另外一方的處理效能。按照故障設計原則,一方的處理出現問題,不應該影響到另外一方才對。但是,在同步阻塞的模式下面,這樣的情況是無法避免的,很難通過業務層去解決。既然同步無法避免,為了避免就產生了非同步。Netty框架就一個完全非同步非阻塞的I/O通訊方式

同步阻塞式I/O程式設計

  簡單的來說,傳統同步阻塞的I/O通訊模式,伺服器端處理的方式是,每當有一個新使用者接入的時候,就new一個新的執行緒,一個執行緒只能處理一個客戶端的連線,在高效能方面,併發高的情景下無法滿足。虛擬碼如下:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author 海晨憶
 * @date 2018/2/9
 * @desc
 */
public class SocketServer {
  private int port = 8080;
  private Socket socket = null;

  public SocketServer(int port) {
    this.port = port;
  }

  public void connect() {
    ServerSocket server = null;
    try {
      server = new ServerSocket(port);
      while (true) {
        socket = server.accept();
        new Thread(new Runnable() {
          @Override
          public void run() {
            new TimerServerHandler(socket).run();
          }
        }).start();
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      //釋放資源
      if (server != null) {
        try {
          server.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
        server = null;
      }
    }
  }
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * @author 海晨憶
 * @date 2018/2/9
 * @desc
 */
public class TimerServerHandler implements Runnable {
  private Socket socket;

  public TimerServerHandler(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;
      }
    } catch (IOException e) {
      e.printStackTrace();
      //釋放in,out,socket資源
    }
  }
}

  上面這個就是最原始的服務端IO的程式碼,這裡我就給出的是最簡化的,當有新的客戶端接入的時候,服務端是怎麼處理執行緒的,可以看出,每當有新的客戶端接入的時候,總是回新建立一個執行緒去服務這個新的客戶端

偽非同步式程式設計

  後來慢慢演化出一個版本“偽非同步”模型,新增加一個執行緒池或者訊息佇列,滿足一個執行緒或者多個執行緒滿足N個客戶端,通過執行緒池可以靈活的呼叫執行緒資源。通過設定執行緒池的最大值,防止海量併發接入造成的執行緒耗盡,它的底層實現依然是同步阻塞模型,虛擬碼如下:

import com.example.zwang.mysocket.server.TimerServerHandler;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author 海晨憶
 * @date 2018/2/9
 * @desc
 */
public class SocketServer {
  private int port = 8080;
  private Socket socket = null;

  public SocketServer(int port) {
    this.port = port;
  }

  private void connect() {
    ServerSocket server = null;
    try {
      server = new ServerSocket(port);
      TimeServerHandlerExecutePool executePool = new TimeServerHandlerExecutePool(50, 1000);
      while (true) {
        socket = server.accept();
        executePool.execute(new TimerServerHandler(socket));
      }
    } catch (IOException e) {
      e.printStackTrace();
    }finally {
      //釋放資源
    }
  }
}
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author 海晨憶
 * @date 2018/2/9
 * @desc
 */
public class TimeServerHandlerExecutePool {
  private ExecutorService executor;

  public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) {
    executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize,
        120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
  }

  public void execute(Runnable task) {
    executor.execute(task);
  }
}

  “偽非同步”的程式碼和傳統同步的唯一區別就是在於,首先先建立了一個時間服務處理類的執行緒池,當有新的客戶端接入的時候,先將socket請求封裝成task,然後呼叫執行緒池的execute方法執行,從而避免了每一個新請求建立一個新執行緒。由於執行緒池和訊息佇列都是有限的,因此,無論客戶端的併發量多大,它都不會導致執行緒個數過於大,而造成的記憶體溢位。相對於傳統的同步阻塞,是一種改良。

  但是他沒有從更本上解決同步的問題,偽非同步的問題在於,他還是有一方處理出現問題還是會影響到另一方。因為:
* 當對socket的輸入流進行讀取操作的時候,它會一直阻塞直到一下三種方式發生:
 1. 有資料可讀
 2. 可讀資料已經讀取完
 3. 發生空指標或者I/O異常。
這意味者,當讀取inputstream方處理速度緩慢(不管是什麼原因造成的速度緩慢),另一方會一直同步阻塞,直到這一方把資料處理完.
* 當呼叫outputstream的write方法寫輸出流的時候,它將會被阻塞,直到所有要傳送的位元組全部寫入完畢,或者發生異常。學過TCP/IP相關知識的人都直到,當訊息的接收方處理訊息緩慢,不能及時的從TCP緩衝區讀取資料,這將會導致傳送方的TCP緩衝區的size一直減少,直到0.緩衝區為0,那麼發訊息的一方將無法將訊息寫入緩衝區,直到緩衝區的size大於0

  通過以上。我們瞭解到讀和寫的操作都是同步阻塞的,阻塞的時間取決於對方的I/O執行緒的處理速度和網路I/O的傳送速度。從本質上面看,我們無法保證對方的處理速度和網路傳送速度。如果,我們的程式依靠與對方的處理速度,那麼,他的可靠性將會非常差。

NIO程式設計

  官方叫法new I/O,也就是新的IO程式設計,更多的人喜歡稱它為:Non-block IO即非阻塞IO。
  與Socket和serverSocket類對應,NIO提供了SocketChannel和ServerSocketChannel兩種不同的套接字通道實現,這兩種都支援阻塞式程式設計和非阻塞式程式設計。開發人員可以根據自己的需求選擇合適的程式設計模式。一般低負載,低併發的應用程式選擇同步阻塞的方式以降低程式設計的複雜度。高負載,高併發的不用想了,非阻塞就是為了解決這個問題的
1. 緩衝區Buffer
  Buffer是一個物件,它包含一些寫入或者讀出的資料。再NIO中加入buffer物件,體現了新庫和舊庫的一個重要區別。在面向流的io中,可以直接把資料讀取或者寫入到stream物件中。在NIO庫中,所有資料操作都是通過緩衝區處理的。
緩衝區實質上是一個陣列,通常是一個位元組陣列(ByteBuffer),基本資料型別除了boolean沒有,其他都有,如ShortBuffer,CharBuffer等等
2. 通道Channel
  Channel是一個通道,雙向通道,網路資料都是通過Channel讀取,寫入的。是的,沒錯,Channel它既可以進行讀操作,也可以進行寫操作。而流只能是一個方向。只能讀操作或者只能寫操作,而channel是全雙工,讀寫可以同時進行。channel可以分為兩大類:網路讀寫的SelectableChannel和檔案操作的FileChannel。我們前面提到的SocketChannel和ServerSocketChannel都是SelectableChannel的子類。
3. 多路複用器Selector
  selector多路複用器,他是java NIO程式設計的基礎,熟練的掌握selector對於NIO程式設計至關重要。多路複用器提供選擇已經就緒的任務的能力。簡單的講就是他會不斷的輪詢註冊的channel,如果一個Channel發生了讀寫操作,這個Chnnel就會處於就緒狀態,會被selector輪詢出來,通過SelectorKey獲取就緒Channel集合,進行後續的IO操作。一個selector對應多個Channel

  由於原生NIO編碼比較麻煩和複雜,我這裡就給出了思路的虛擬碼。下一篇我們將用NIO中的Netty框架實現Socket通訊,編碼簡單,一行程式碼解決煩人粘包、拆包問題。

/**
   * 服務端nio過程的虛擬碼
   *
   * @param port 埠號
   * @throws IOException IOException
   */
  private void init(int port) throws IOException {
    //第一步:開啟ServerSocketChannel,用於監聽客戶端連線,它是所有客戶端連線的父管道
    ServerSocketChannel socketChannel = ServerSocketChannel.open();
    //第二步:監聽繫結埠,設定連線模式為非阻塞模式,
    socketChannel.socket().bind(new InetSocketAddress(InetAddress.getByName("IP"), port));
    socketChannel.configureBlocking(false);
    //第三步:建立Reactor執行緒,建立多路複用器,並啟動執行緒。
    Selector selector = Selector.open();
    new Thread().start();
    //第四步:將ServerSocketChannel註冊到Reactor執行緒的多路複用器上,監聽accept事件
    SelectionKey key = socketChannel.register(selector, SelectionKey.OP_ACCEPT/*,ioHandler*/);
    //第五步:多路複用器線上程run方法的無線迴圈體內輪詢準備就緒的key
    int num = selector.select();
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> it = selectionKeys.iterator();
    while (it.hasNext()) {
      SelectionKey next = it.next();
      //deal with io event...
    }
    //第六步:多路複用器檢測到有新客戶端接入,處理新的接入請求,完成TCP三次握手,建立物理鏈路
    SocketChannel channel = socketChannel.accept();
    //第七步:設定客戶端為非阻塞模式
    channel.configureBlocking(false);
    channel.socket().setReuseAddress(true);
    //第八步:將新接入的客戶端註冊到reactor執行緒的多路複用器上,監聽讀操作,讀取客戶端傳送的訊息
    SelectionKey key1 = socketChannel.register(selector, SelectionKey.OP_ACCEPT/*,ioHandler*/);
    //第九步:非同步讀取客戶端訊息到緩衝區,
    /*int readNumber = channel.read("receivebuff");*/
    //第十步:對byteBuffer進行編解碼,如果有半包資訊指標reset,繼續讀取到後續的報文,將解碼成功訊息封裝成task,投遞到業務執行緒池,進行業務邏輯編排
    Object massage = null;
    while (buff.hasRemain()) {
      buff.mark();
      Object massage1 = decode(btyeBuffer);
      if (massage1 == null) {
        byteBuffer.reset();
        break;
      }
      massageList.add(massage1);
    }
    if (!byteBuffer.hasRemain()) {
      byteBuffer.clean();
    } else {
      byteBuffer.compact();
    }
    if (massageList != null && !massageList.isEmpty()) {
      for (Object massage3 : massageList){
        handlerTask(massage3);
      }
    }
    //第十一步:將POJO物件encode成ByteBuff,呼叫SocketChannel的非同步write介面,將非同步訊息傳送到客戶端
    socketChannel.write(buffer);
  }

結束!!!

相關文章