IO多路複用

學習Redis和Netty等可以高效處理Socket請求的框架或工具都提到了IO多路複用模型,那麼IO多路複用模型究竟是什麼,本文將會對其進行一個簡單的介紹。主要涉及到如下幾方面知識:
  • 阻塞IO
  • 非阻塞IO
  • IO多路複用

阻塞IO

所謂阻塞IO是指調用方從發起IO請到收到被調用方返回的數據之間的這段時間,調用方線程一直處於阻塞狀態,若是是UI線程則意味着界面不響應,假死。借用網上的一張經典阻塞IO模型以下:

在這裏插入圖片描述
用Socket編程代碼演示以下:java

  • Server端:
public class Server {

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("服務器啓動完成...監聽啓動!");
            //開啓監聽,等待客戶端的訪問
           while(true) {
               Socket socket = serverSocket.accept();
               // 獲取輸入流,由於是客戶端向服務器端發送了數據
               InputStream inputStream = socket.getInputStream();
               // 建立一個緩衝流
               BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
               String info = null;
               while ((info = br.readLine()) != null) {
                   System.out.println("這裏是服務端 客戶端是:" + info);
               }
               //向客戶端作出響應
               OutputStream outputStream = socket.getOutputStream();
               info = "這裏是服務器端,咱們接受到了你的請求信息,正在處理...處理完成!";
               outputStream.write(info.getBytes());
               outputStream.close();
           }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • Client端:
public class Client {
    public static void main(String[] args) throws IOException, InterruptedException {
        try {
            Socket socket = new Socket("localhost",8080);
            OutputStream outputStream = socket.getOutputStream();
            String info = "你好啊!";
            //輸出!
            Thread.sleep(1000);
            outputStream.write(info.getBytes());
            socket.shutdownOutput();
            //接收服務器端的響應
            InputStream inputStream = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            while ((info = br.readLine())!=null){
                System.out.println("接收到了服務端的響應!" + info);
            }
            //刷新緩衝區
            outputStream.flush();
            outputStream.close();
            inputStream.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
上面的服務端程序有個很嚴重的問題:若是有多個客戶端訪問的話,則客戶端必須按順序訪問,若是第一個鏈接連上來以後,無論發不發消息,後面的鏈接只能等待。這樣會致使大量的客戶端阻塞。
    
    爲了解決這個問題,通常會在一個客戶端創建鏈接以後,服務端啓動一個線程來處理與之的通信。可是若是客戶端數太多,就會致使服務端建立大量的線程,線程的建立、上下文切換也會致使服務器的負載大幅度升高。所以也就產生了下面要說的非阻塞IO。

非阻塞IO

Linux下,能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程是這個樣子:

在這裏插入圖片描述

當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而用戶就能夠在本次到下次再發起read詢問的時間間隔內作其餘事情,或者直接再次發送read操做。

對應到Socket服務器的代碼以下:編程

public class SelectorServer {

    public static void main(String[] args) throws IOException {

        Selector selector = Selector.open();

        ServerSocketChannel server = ServerSocketChannel.open();
        server.socket().bind(new InetSocketAddress(8080));

        // 將其註冊到 Selector 中,監聽 OP_ACCEPT 事件
        server.configureBlocking(false);//非阻塞IO的設置
        server.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            int readyChannels = selector.select();
            if (readyChannels == 0) {
                continue;
            }
            Set<SelectionKey> readyKeys = selector.selectedKeys();
            // 遍歷
            Iterator<SelectionKey> iterator = readyKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                if (key.isAcceptable()) {
                    // 有已經接受的新的到服務端的鏈接
                    SocketChannel socketChannel = server.accept();

                    // 有新的鏈接並不表明這個通道就有數據,
                    // 這裏將這個新的 SocketChannel 註冊到 Selector,監聽 OP_READ 事件,等待數據
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    // 有數據可讀
                    // 上面一個 if 分支中註冊了監聽 OP_READ 事件的 SocketChannel
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    int num = socketChannel.read(readBuffer);
                    if (num > 0) {
                        // 處理進來的數據...
                        System.out.println("收到數據:" + new String(readBuffer.array()).trim());
                        ByteBuffer buffer = ByteBuffer.wrap("返回給客戶端的數據...".getBytes());
                        socketChannel.write(buffer);
                    } else if (num == -1) {
                        // -1 表明鏈接已經關閉
                        socketChannel.close();
                    }
                }
            }
        }

Client端能夠繼續沿用上面的。服務器

該種實現方式的好處是,不用建立多個線程。在一個主線程裏即把全部客戶端鏈接都處理了。由於是非阻塞鏈接,服務端在收到鏈接後直接將該鏈接對應的socketChannel註冊到selector中,並監控該鏈接的Read操做。當有數據到來時在對Socket進行數據的傳輸處理。

可是這種方案仍然有個問題,就是主程序要不斷的輪詢,無論有沒有鏈接,鏈接是否可用。而且收到selector的事件後也不知道究竟是什麼操做準備好了,只能逐個判斷。這樣催生了後來的異步IO。異步IO的問題在合理先不講了,後面再說。

IO多路複用

寫到這裏對IO多路複用也有了個大體的瞭解,其實應該把「IO多路複用」拆解來看。框架

  • IO:對一臺設備的輸入輸出操做。能夠是磁盤IO,也能夠是SocketIO。
  • 多路:對應服務器就是多個客戶端的鏈接。每一個客戶端算一路。
  • 複用:複用同一個線程。即用一個線程完成對全部客戶端鏈接的輸入輸出處理。

這裏在舉一個現實生活中的例子。一我的開飯館(服務器),來了10個客人(客戶端鏈接),這時老闆是選擇僱傭10個服務員(線程)分別爲每一個客人服務仍是用一個服務員來監聽全部客人的動做等着爲各個客人服務呢,相信你們都會作出正確的選擇。異步

參考資料:
https://www.zhihu.com/questio...
https://www.javadoop.com/post...socket

相關文章
相關標籤/搜索