傳統IO與NIO比較

咱們先來看一段傳統IO的代碼java

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鏈接web

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

咱們會看到OioServer的運行狀況緩存

服務器啓動
來了一個新客戶端服務器

可是當咱們又使用一個telnet鏈接進來的時候,OioServer的運行狀況沒變,說明一個服務端只能接收一個客戶端點鏈接,緣由在於Socket socket = server.accept();發生了堵塞,如今咱們將其改寫成多線程websocket

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進行鏈接的時候,它是能夠一塊兒鏈接進來的多線程

服務器啓動
來了一個新客戶端
來了一個新客戶端併發

可是這裏有一個問題,咱們線程池的可用線程是有限的,不可能無限提供線程來接收大量客戶端的鏈接,早晚它會無響應被堵塞的。socket

咱們如今來看一下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
新的客戶端鏈接
服務端收到信息:22222
客戶端關閉

若是咱們使用telnet鏈接進去之後,直接關閉shell,則服務端會拋出異常

服務端啓動成功!
新的客戶端鏈接
服務端收到信息:
Exception in thread "main" java.io.IOException: Connection reset by peer
    at sun.nio.ch.FileDispatcherImpl.read0(Native Method)
    at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)
    at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
    at sun.nio.ch.IOUtil.read(IOUtil.java:197)
    at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380)
    at com.guanjian.websocket.io.NIOServer.handelerRead(NIOServer.java:111)
    at com.guanjian.websocket.io.NIOServer.handler(NIOServer.java:77)
    at com.guanjian.websocket.io.NIOServer.listen(NIOServer.java:59)
    at com.guanjian.websocket.io.NIOServer.main(NIOServer.java:134)

說明在讀取Buffer緩衝區的時候,拋出了異常,因此咱們應該在讀取的時候捕獲異常,而不是拋出異常

/**
 * 處理讀的事件
 * 
 * @param key
 * @throws IOException
 */
public void handelerRead(SelectionKey key) {
   // 服務器可讀取消息:獲得事件發生的Socket通道
   SocketChannel channel = (SocketChannel) key.channel();
   // 建立讀取的緩衝區
   ByteBuffer buffer = ByteBuffer.allocate(1024);
   try {
      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();
           }
   } catch (IOException e) {
      e.printStackTrace();
   }
}

咱們如今來證實NIO是單線程的,將以上代碼修改一下

/**
 * 處理讀的事件
 * 
 * @param key
 * @throws IOException
 */
public void handelerRead(SelectionKey key) {
   // 服務器可讀取消息:獲得事件發生的Socket通道
   SocketChannel channel = (SocketChannel) key.channel();
   // 建立讀取的緩衝區
   ByteBuffer buffer = ByteBuffer.allocate(1024);
   try {
      int read = channel.read(buffer);
      Thread.sleep(60000);
      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();
           }
   } catch (Exception e) {
      e.printStackTrace();
   }
}

咱們讓他發送消息的時候睡一分鐘。啓動服務端,鏈接第一個telnet進來,併發幾個字符

此時咱們連進第二個telnet,會發現服務端沒反應,須要等到一分鐘以後,第一個telnet纔會收到"好的",而服務端纔會顯示"新的客戶端鏈接"。

說明服務端在處理髮送字符的時候被阻塞,NIO爲單線程。

相關文章
相關標籤/搜索