NIO、BIO、Selector

minororange發表於2022-06-07

IO

什麼是 IO,IO 是Input、Output的簡稱,即輸入輸出,服務端與客戶端互動的過程也是一種 IO。

BIO

全稱 Blocking IO,即阻塞 IO,單個執行緒在處理單個請求時,如果當前請求沒有下一步操作,當前執行緒會被卡住,如果有另一個請求進來,當前執行緒是無法響應新來的請求的。

Laravel

 public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8000);

            while (true){
                // accept 會阻塞
                Socket accept = serverSocket.accept();
                System.out.println("客戶端連線成功");
                byte[] bytes = new byte[1024];
                // read 也會阻塞
                int read = accept.getInputStream().read(bytes);

                if (read != -1) {
                    System.out.println("收到訊息:" + new String(bytes));
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

缺點:一個執行緒只能處理一個請求,無法應對高併發場景

NIO

全稱 New IO,又稱 Non-Blocking IO,即非阻塞的 IO 模型。伺服器再處理 acceptread 等操作時,並不會阻塞當前執行緒,而是繼續執行下面的程式碼。

  public static List<SocketChannel> CHANNEL_LIST = new ArrayList<>();

    public static void main(String[] args) {

        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(8000));
            // 配置 socket 為非阻塞
            serverSocketChannel.configureBlocking(false);

            while (true) {
                // 接受客戶端請求
                SocketChannel accept = serverSocketChannel.accept();
                if (accept != null) {
                    System.out.println("連線成功");
                    // 設定客戶端 socket 位非阻塞 ==> 讀操作是從這個 socket 裡面讀取
                    accept.configureBlocking(false);
                    // 把建立好連線的 socket 放入 List 中
                    CHANNEL_LIST.add(accept);
                }

                // 遍歷 List
                for (SocketChannel socketChannel : CHANNEL_LIST) {
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int read = socketChannel.read(byteBuffer);

                    if (read > 0) {
                        System.out.println("收到訊息:" + new String(byteBuffer.array()));
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

Laravel

改進:現在一個執行緒可以處理多個請求
缺點:1. 伺服器空轉,當沒有 acceptread 操作時,浪費 CPU 資源。
2. 已建立連線的 Socket List 無法快速定位當前傳送資料的 socket

Selector

 public static void main(String[] args) {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(8000));
            // 設定為非阻塞
            serverSocketChannel.configureBlocking(false);
            // 建立 Selector ==> epoll_create
            Selector selector = Selector.open();
            // 將 server socket 註冊 ACCEPT 事件到 Seletor 中  ==> epoll_ctl
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                // 獲取事件 ==> epoll_wait 如果此時這個 Selector 監聽的 socket 沒有事件發生,則會掛起
                // 處理方式類似 阻塞佇列中的 poll,佇列中沒資料時會 park 當前執行緒,來了資料會 unpark 當前執行緒
                // 阻塞佇列中的資料寫入是其他執行緒操作的,而 epoll 中的事件寫入是系統層面進行的
                selector.select();
                // 獲取有事件產生的 Keys
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                // 遍歷 Keys
                if (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    // 如果當前事件是連線事件
                    if (selectionKey.isAcceptable()) {
                        ServerSocketChannel serverChannel = (ServerSocketChannel) selectionKey.channel();
                        // 建立連線
                        SocketChannel accept = serverChannel.accept();
                        // 配置通道為非阻塞
                        accept.configureBlocking(false);
                        // 註冊當前通道到 selector 中,事件為 read
                        accept.register(selector, SelectionKey.OP_READ);
                        SocketAddress remoteAddress = accept.getRemoteAddress();
                        System.out.println("客戶端" + remoteAddress + "連線成功");
                    } else 
                    // 如果事件是讀取事件(即客戶端傳送了資料)
                    if (selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                        // 讀取資料
                        int read = channel.read(byteBuffer);

                        if (read > 0) {
                            System.out.println("收到訊息:" + new String(byteBuffer.array()));
                        }
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

Laravel

EpollSelectorImpl

linux 核心函式

  • epoll_create:建立一個epoll的控制程式碼
  • epoll_ctl:向epoll中註冊事件
  • epoll_wait:返回 epoll 中已註冊的事件

EPollSelectorProvider.openSelector()

 EPollSelectorImpl(SelectorProvider sp) throws IOException {
    super(sp);
    long pipeFds = IOUtil.makePipe(false);
    fd0 = (int) (pipeFds >>> 32);
    fd1 = (int) pipeFds;
    // 建立一個裝 socket 控制程式碼的陣列
    pollWrapper = new EPollArrayWrapper();
    pollWrapper.initInterrupt(fd0, fd1);
    fdToKey = new HashMap<>();
}
void initInterrupt(int fd0, int fd1) {
    outgoingInterruptFD = fd1;
    incomingInterruptFD = fd0;
    //將管道的讀取端註冊
    epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);
}

EpollSelector.implRegister

protected void implRegister(SelectionKeyImpl ski) {
    if (closed)
        throw new ClosedSelectorException();
    SelChImpl ch = ski.channel;
    int fd = Integer.valueOf(ch.getFDVal());
    fdToKey.put(fd, ski);
    // fd 為當前 socket
    pollWrapper.add(fd);
    keys.add(ski);
}

EpollSelector.doSelect()

protected int doSelect(long timeout) throws IOException {
    if (closed)
        throw new ClosedSelectorException();
    processDeregisterQueue();
    try {
        begin();
        // 從 socket 陣列中取出事件 這一步如果沒有事件更新就會掛起
        pollWrapper.poll(timeout);
    } finally {
        end();
    }
    processDeregisterQueue();
    int numKeysUpdated = updateSelectedKeys();
    if (pollWrapper.interrupted()) {
        // Clear the wakeup pipe
        pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
        synchronized (interruptLock) {
            pollWrapper.clearInterrupted();
            IOUtil.drain(fd0);
            interruptTriggered = false;
        }
    }
    return numKeysUpdated;
}

int poll(long timeout) throws IOException {
    //更新epoll事件,實際呼叫`epollCtl`加入到epollfd中
    updateRegistrations();
    //獲取已就緒的檔案控制程式碼
    updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
    //如是喚醒檔案控制程式碼,則跳過,設定interrupted=true
    for (int i=0; i<updated; i++) {
        if (getDescriptor(i) == incomingInterruptFD) {
            interruptedIndex = i;
            interrupted = true;
            break;
        }
    }
    return updated;
}

private void updateRegistrations() {
    synchronized (updateLock) {
        int j = 0;
        while (j < updateCount) {
            int fd = updateDescriptors[j];
            short events = getUpdateEvents(fd);
            boolean isRegistered = registered.get(fd);
            int opcode = 0;

            if (events != KILLED) {
                //已經註冊過
                if (isRegistered) {
                    //修改或刪除
                    opcode = (events != 0) ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
                } else {
                    //新增
                    opcode = (events != 0) ? EPOLL_CTL_ADD : 0;
                }
                if (opcode != 0) {
                    epollCtl(epfd, opcode, fd, events);
                    if (opcode == EPOLL_CTL_ADD) {
                        //增加到registered快取是否已註冊
                        registered.set(fd);
                    } else if (opcode == EPOLL_CTL_DEL) {
                        registered.clear(fd);
                    }
                }
            }
            j++;
        }
        updateCount = 0;
    }
}

NIO、BIO、Selector

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章