java select epoll poll

基本概念

  IO多路複用是指內核一旦發現進程指定的一個或者多個IO條件準備讀取, 它就通知該進程. IO多路複用適用以下場合: java

  (1)當客戶處理多個描述字時(通常是交互式輸入和網絡套接口), 必須使用I/O複用.數組

  (2)當一個客戶同時處理多個套接口時, 而這種狀況是可能的, 但不多出現.服務器

  (3)若是一個TCP服務器既要處理監聽套接口, 又要處理已鏈接套接口, 通常也要用到I/O複用.網絡

  (4)若是一個服務器即要處理TCP, 又要處理UDP, 通常要使用I/O複用.多線程

  (5)若是一個服務器要處理多個服務或多個協議, 通常要使用I/O複用.socket

  與多進程和多線程技術相比, I/O多路複用技術的最大優點是系統開銷小, 系統沒必要建立進程/線程, 也沒必要維護這些進程/線程, 從而大大減少了系統的開銷.ide

clipboard.png

Selector(選擇器)

在 Java 中, Selector 這個類是 select/epoll/poll 的外包類, 在不一樣的平臺上, 底層的實現可能有所不一樣, 但其基本原理是同樣的, 其原理圖以下所示:函數

clipboard.png

全部的 Channel 都歸 Selector 管理, 這些 channel 中只要有至少一個有IO動做, 就能夠經過 Selector.select 方法檢測到, 而且使用 selectedKeys 獲得這些有 IO 的 channel, 而後對它們調用相應的IO操做.spa

我這裏有一個服務端的例子:.net

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class EpollServer {
    public static void main(String[] args) {
        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
            //不設置阻塞隊列
            ssc.configureBlocking(false);

            Selector selector = Selector.open();
            // 註冊 channel,而且指定感興趣的事件是 Accept
            ssc.register(selector, SelectionKey.OP_ACCEPT);

            ByteBuffer readBuff = ByteBuffer.allocate(1024);
            ByteBuffer writeBuff = ByteBuffer.allocate(128);
            writeBuff.put("received".getBytes());
            writeBuff.flip();

            while (true) {
                int nReady = selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();

                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    it.remove();

                    if (key.isAcceptable()) {
                        // 建立新的鏈接,而且把鏈接註冊到selector上,並且,
                        // 聲明這個channel只對讀操做感興趣。
                        SocketChannel socketChannel = ssc.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }
                    else if (key.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        readBuff.clear();
                        socketChannel.read(readBuff);

                        readBuff.flip();
                        System.out.println("received : " + new String(readBuff.array()));
                        key.interestOps(SelectionKey.OP_WRITE);
                    }
                    else if (key.isWritable()) {
                        writeBuff.rewind();
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        socketChannel.write(writeBuff);
                        key.interestOps(SelectionKey.OP_READ);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

這個例子的關鍵點:

  • 建立一個 ServerSocketChannel, 和一個 Selector, 而且把這個 server channel 註冊到 selector 上, 註冊的時間指定, 這個 channel 所感受興趣的事件是 SelectionKey.OP_ACCEPT, 這個事件表明的是有客戶端發起TCP鏈接請求.
  • 使用 select 方法阻塞住線程, 當 select 返回的時候, 線程被喚醒. 再經過 selectedKeys 方法獲得全部可用 channel 的集合.
  • 遍歷這個集合, 若是其中 channel 上有鏈接到達, 就接受新的鏈接, 而後把這個新的鏈接也註冊到 selector 中去.
  • 若是有 channel 是讀, 那就把數據讀出來, 而且把它感興趣的事件改爲寫. 若是是寫, 就把數據寫出去, 而且把感興趣的事件改爲讀.
Selector.open 在不一樣的系統裏實現方式不一樣
sunOS 使用 DevPollSelectorProvider, Linux就會使用 EPollSelectorProvider, 而默認則使用 PollSelectorProvider

也就是說 selector.select() 用來阻塞線程, 直到一個或多個 channle 進行 io 操做. 好比 SelectionKey.OP_ACCEPT.
而後使用 selector.selectedKeys() 方法獲取出, 這些通道.

那麼 selector.select() 是怎麼直到已經有 io 操做了呢?

緣由是由於 poll

poll

# include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

pollfd結構體定義以下:

struct pollfd {
    int fd;         /* 文件描述符 */
    short events;   /* 等待的事件 */
    short revents;  /* 實際發生了的事件 */
};

每個 pollfd 結構體指定了一個被監視的文件描述符, 能夠傳遞多個結構體, 指示 poll() 監視多個文件描述符.

每一個結構體的 events 域是監視該文件描述符的事件掩碼, 由用戶來設置這個域. revents 域是文件描述符的操做結果事件掩碼, 內核在調用返回時設置這個域.

events 域中請求的任何事件均可能在 revents 域中返回. 事件以下:

描述
POLLIN 有數據可讀
POLLRDNORM 有普通數據可讀
POLLRDBAND 有優先數據可讀
POLLPRI 有緊迫數據可讀
POLLOUT 寫數據不會致使阻塞
POLLWRNORM 寫普通數據不會致使阻塞
POLLWRBAND 寫優先數據不會致使阻塞
POLLMSGSIGPOLL 消息可用
POLLER 指定的文件描述符發生錯誤
POLLHUP 指定的文件描述符掛起事件
POLLNVAL 指定的文件描述符非法

說白了 poll() 能夠監視多個文件描述符.

若是返回值是 3, 咱們須要逐個去遍歷出返回值是 3 的 socket, 而後在作對應操做.

epoll

poll 方法有一個很是大的缺陷. poll 函數的返回值是一個整數, 獲得了這個返回值之後, 咱們仍是要逐個去檢查, 好比說, 有一萬個 socket 同時 poll, 返回值是3, 咱們仍是隻能去遍歷這一萬個 socket, 看看它們是否有IO動做.

這就很低效了, 因而, 就有了 epoll 的改進, epoll能夠直接經過「輸出參數」(能夠理解爲C語言中的指針類型的參數), 一個 epoll_event 數組, 直接得到這三個 socket, 這就比較快了.

相關文章
相關標籤/搜索