1. 程式人生 > >傳統IO與NIO比較

傳統IO與NIO比較

我們先來看一段傳統IO的程式碼

public class OioServer {
    public static void main(String[] args) throws IOException {
        //這裡可以直接寫成ServerSocket server = new ServerSocket(10101);
        ServerSocket server = new ServerSocket();
        server.bind(new InetSocketAddress(10101));
        System.out.println("伺服器啟動"
); while (true) { //此處會阻塞 Socket socket = server.accept(); System.out.println("來了一個新客戶端"); handler(socket); } } public static void handler(Socket socket) { try { byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream(); while (true) { int read = inputStream.read(bytes); if (read != -1) { System.out.println(new String(bytes,0,read)); }else { break;
} } } catch (IOException e) { e.printStackTrace(); }finally { try { System.out.println("socket關閉"); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }

使用telnet連線

admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

我們會看到OioServer的執行情況

伺服器啟動
來了一個新客戶端

但是當我們又使用一個telnet連線進來的時候,OioServer的執行情況沒變,說明一個服務端只能接收一個客戶端點連線,原因在於Socket socket = server.accept();發生了堵塞,現在我們將其改寫成多執行緒

public class OioServerThread {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(10101);
        ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
        System.out.println("伺服器啟動");
        while (true) {
            Socket socket = server.accept();
            System.out.println("來了一個新客戶端");
            service.execute(() -> handler(socket));
        }
    }
    public static void handler(Socket socket) {
        try {
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream();
            while (true) {
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(new String(bytes,0,read));
                }else {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                System.out.println("socket關閉");
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

執行可知,當我們啟動了多個telnet進行連線的時候,它是可以一起連線進來的

伺服器啟動
來了一個新客戶端
來了一個新客戶端

但是這裡有一個問題,我們執行緒池的可用執行緒是有限的,不可能無限提供執行緒來接收大量客戶端的連線,遲早它會無響應被堵塞的。

我們現在來看一下NIO,NIO其實是使用傳統IO的特性建立一個channel(通道),通過該通道來註冊事件SelectionKey

SelectionKey有四種事件

  • SelectionKey.OP_ACCEPT —— 接收連線繼續事件,表示伺服器監聽到了客戶連線,伺服器可以接收這個連線了
  • SelectionKey.OP_CONNECT —— 連線就緒事件,表示客戶與伺服器的連線已經建立成功
  • SelectionKey.OP_READ —— 讀就緒事件,表示通道中已經有了可讀的資料,可以執行讀操作了(通道目前有資料,可以進行讀操作了)
  • SelectionKey.OP_WRITE —— 寫就緒件,表示已經可以向通道寫資料了(通道目前可以用於寫操作)

 這裡 注意,下面兩種,SelectionKey.OP_READ ,SelectionKey.OP_WRITE ,

1.當向通道中註冊SelectionKey.OP_READ事件後,如果客戶端有向快取中write資料,下次輪詢時,則會 isReadable()=true;

2.當向通道中註冊SelectionKey.OP_WRITE事件後,這時你會發現當前輪詢執行緒中isWritable()一直為ture,如果不設定為其他事件

public class NIOServer {
   // 通道管理器
   private Selector selector;

   /**
    * 獲得一個ServerSocket通道,並對該通道做一些初始化的工作
    * 
    * @param port
    *            繫結的埠號
    * @throws IOException
    */
   public void initServer(int port) throws IOException {
      // 獲得一個ServerSocket通道
      ServerSocketChannel serverChannel = ServerSocketChannel.open();
      // 設定通道為非阻塞
      serverChannel.configureBlocking(false);
      // 將該通道對應的ServerSocket繫結到port埠
      serverChannel.socket().bind(new InetSocketAddress(port));
      // 獲得一個通道管理器
      this.selector = Selector.open();
      // 將通道管理器和該通道繫結,併為該通道註冊SelectionKey.OP_ACCEPT事件,註冊該事件後,
      // 當該事件到達時,selector.select()會返回,如果該事件沒到達selector.select()會一直阻塞。
      serverChannel.register(selector, SelectionKey.OP_ACCEPT);
   }

   /**
    * 採用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理
    * 
    * @throws IOException
    */
   public void listen() throws IOException {
      System.out.println("服務端啟動成功!");
      // 輪詢訪問selector
      while (true) {
         // 當註冊的事件到達時,方法返回;否則,該方法會一直阻塞
         selector.select();
         // 獲得selector中選中的項的迭代器,選中的項為註冊的事件
         Iterator<?> ite = this.selector.selectedKeys().iterator();
         while (ite.hasNext()) {
            SelectionKey key = (SelectionKey) ite.next();
            // 刪除已選的key,以防重複處理
            ite.remove();
            handler(key);
         }
      }
   }

   /**
    * 處理請求
    * 
    * @param key
    * @throws IOException
    */
   public void handler(SelectionKey key) throws IOException {
      
      // 客戶端請求連線事件
      if (key.isAcceptable()) {
         handlerAccept(key);
         // 獲得了可讀的事件
      } else if (key.isReadable()) {
         handelerRead(key);
      }
   }

   /**
    * 處理連線請求
    * 
    * @param key
    * @throws IOException
    */
   public void handlerAccept(SelectionKey key) throws IOException {
      ServerSocketChannel server = (ServerSocketChannel) key.channel();
      // 獲得和客戶端連線的通道
      SocketChannel channel = server.accept();
      // 設定成非阻塞
      channel.configureBlocking(false);

      // 在這裡可以給客戶端傳送資訊哦
      System.out.println("新的客戶端連線");
      // 在和客戶端連線成功之後,為了可以接收到客戶端的資訊,需要給通道設定讀的許可權。
      channel.register(this.selector, SelectionKey.OP_READ);
   }

   /**
    * 處理讀的事件
    * 
    * @param key
    * @throws IOException
    */
   public void handelerRead(SelectionKey key) throws IOException {
      // 伺服器可讀取訊息:得到事件發生的Socket通道
      SocketChannel channel = (SocketChannel) key.channel();
      // 建立讀取的緩衝區
      ByteBuffer buffer = ByteBuffer.allocate(1024);
      int read = channel.read(buffer);
      if(read > 0){
         byte[] data = buffer.array();
         String msg = new String(data).trim();
         System.out.println("服務端收到資訊:" + msg);
         
         //回寫資料
         ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
         channel.write(outBuffer);// 將訊息回送給客戶端
      }else{
         System.out.println("客戶端關閉");
         key.cancel();
      }
   }

   /**
    * 啟動服務端測試
    * 
    * @throws IOException
    */
   public static void main(String[] args) throws IOException {
      NIOServer server = new NIOServer();
      server.initServer(10101);
      server.listen();
   }

}

NIO與傳統IO最大的不同

  1. NIO有通道的概念,傳統IO沒有這個概念,但通道的概念是基於傳統IO的
  2. 傳統IO的字元接受處理是也是實用的Java原生的序列化流的方式,而NIO是使用ByteBuffer的緩衝區機制。

使用telnet測試,NIO是肯定支援多個客戶端同時操作的,但很重要的一點是NIO是單執行緒的,傳統IO和NIO的邏輯如下

傳統IO

NIO

至於NIO如何多執行緒,可以參考NIO如何多執行緒操作 ,這其實也是Netty的原理。

分別用兩個telnet連線

admindeMacBook-Pro:IOServer admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
dsfds
好的

admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
22222
好的

服務端顯示如下

服務端啟動成功!
新的客戶端連線
服務端收到資訊:dsfds
新的客戶端連線
服務端收到資訊:22222

當我們退出其中一個的時候

admindeMacBook-Pro:~ admin$ telnet 127.0.0.1 10101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
22222
好的^]
telnet> quit
Connection closed.

服務端顯示如下

服務端啟動成功!
新的客戶端連線
服務端收到資訊:dsfds
新的客戶端連線
服務端收到資訊:2222