1. 程式人生 > >從BIO到NIO

從BIO到NIO

我們知道,Java服務端程式設計,很重要的一塊就是IO,而我們的Java IO,經歷了由BIO到NIO再到AIO的過程。
首先,我們來看一下什麼是BIO:

1.普通BIO
我們剛開始學Java Socket的時候,可能都寫過這麼一段程式碼:

    ServerSocket serverSocket = new ServerSocket(8000);
    while (true){
      Socket socket = serverSocket.accept();
      InputStream is = socket.getInputStream();
      byte [] buff = new byte[1024];
      int len = is.read(buff);
      while (len > 0){
        String msg = new String(buff,0,len);
        System.out.println("收到" + msg);
        len = is.read(buff);
      }
    }

顯然,這段程式碼把對客戶端接入後的處理都放在了while迴圈中,也就是說,這段程式碼一段時間只能處理一個客戶端連線,那麼問題來了,我們想一想,能作為服務端的伺服器效能應該是不錯的,所以這種方式浪費了我們的效能,而且,由於一次處理一個客戶端連線,併發量自然而然地上不去,所以,這個方法不會在生產環境中使用。

2.多執行緒式BIO

為了提高併發量,我們可以將程式碼改成這個樣子:

    ServerSocket serverSocket = new ServerSocket(8000);
    while (true){
      Socket socket = serverSocket.accept();
      new Thread(new Handler(socket)).start();
    }
public class Handler implements Runnable {

  private Socket socket;

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

  @Override
  public void run() {
    InputStream is = null;
    try {
      is = socket.getInputStream();
      byte[] buff = new byte[1024];
      int len = is.read(buff);
      while (len > 0) {
        String msg = new String(buff, 0, len);
        System.out.println("收到" + msg);
        len = is.read(buff);
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (is != null) {
        try {
          is.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }
}

但是這樣會有一個問題,我們來一個客戶端連線不管三七二十一就會new一個執行緒,這樣執行緒數和客戶端連線數是1:1的關係,我們知道,執行緒的建立和銷燬是很耗資源的一件事情,同樣地,這樣也會增加執行緒上下文切換的開銷。
為了降低執行緒建立和銷燬的開銷,我們可以引入執行緒池(程式碼先不貼了),但是這樣並沒有從根本上解決問題。
為了從根本上解決問題,Java引入了NIO:

3.NIO

我們可以這麼來理解,我們把web伺服器比作銀行,客戶端發起的連線比作要辦理業務的客戶,銀行處理業務的工作人員比作處理客戶端連線的執行緒,由於銀行的資源(假如說只有一臺計算機能夠處理業務)有限,那麼同一時刻只有一個人可以辦理業務。
那麼我們的BIO就相當於:進來一個客戶,就給它分配一個業務員,但是業務員只有得到資源(計算機)才能處理業務,所以客戶一進入銀行就給他分配業務員是沒什麼卵用的,還浪費資源(業務員是要領工資的)
而我們的NIO呢,就相當於只有一個業務員,但是有一個排號系統,客戶進入銀行先在排號系統登記,領一個號碼,等到輪到某個客戶處理業務了,才給他分配一個業務員,處理他的業務。

public class NIOServer {
  private int port = 8000;
  private Selector selector = null;
  private Charset charset = Charset.forName("UTF-8");
  public NIOServer(int port) throws IOException {
    this.port = port;
    ServerSocketChannel server = ServerSocketChannel.open();
    server.bind(new InetSocketAddress(this.port));
    server.configureBlocking(false);
    selector = Selector.open();
    server.register(selector, SelectionKey.OP_ACCEPT);
  }

  public void listener() throws IOException {
    while (true) {
      int wait = selector.select();
      if (wait == 0) continue;
      Set<SelectionKey> keys = selector.selectedKeys();
      Iterator<SelectionKey> iterator = keys.iterator();
      while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        iterator.remove();
        process(key);
      }
    }
  }

  public void process(SelectionKey key) throws IOException {
    if (key.isAcceptable()) {
      ServerSocketChannel server = (ServerSocketChannel) key.channel();
      SocketChannel client = server.accept();
      client.configureBlocking(false);
      client.register(selector, SelectionKey.OP_READ);
    }
    if (key.isReadable()) {
      SocketChannel client = (SocketChannel) key.channel();
      ByteBuffer buff = ByteBuffer.allocate(1024);
      StringBuilder content = new StringBuilder();
      try {
        while (client.read(buff) > 0) {
          buff.flip();
          content.append(charset.decode(buff));
        }
        System.out.println(content);
        key.interestOps(SelectionKey.OP_READ);
      } catch (IOException io) {
        key.cancel();
        if (key.channel() != null) {
          key.channel().close();
        }
      }
    }
  }

  public static void main(String[] args) throws IOException {
    new NIOServer(8000).listener();
  }
}

NIO呢,就是我們來一個客戶端連線,先不處理,先在Selector上註冊,然後不斷地輪詢註冊在Selector上面的channel,當準備好讀或者寫的時候,再對客戶端連線進行操作,這樣呢,就大大節省了服務端的資源,因此十分適合於大量長連線的場景。