IO多路複用是指內核一旦發現進程指定的一個或者多個IO條件準備讀取, 它就通知該進程. IO多路複用適用以下場合: java
(1)當客戶處理多個描述字時(通常是交互式輸入和網絡套接口), 必須使用I/O複用.數組
(2)當一個客戶同時處理多個套接口時, 而這種狀況是可能的, 但不多出現.服務器
(3)若是一個TCP服務器既要處理監聽套接口, 又要處理已鏈接套接口, 通常也要用到I/O複用.網絡
(4)若是一個服務器即要處理TCP, 又要處理UDP, 通常要使用I/O複用.多線程
(5)若是一個服務器要處理多個服務或多個協議, 通常要使用I/O複用.socket
與多進程和多線程技術相比, I/O多路複用技術的最大優點是系統開銷小, 系統沒必要建立進程/線程, 也沒必要維護這些進程/線程, 從而大大減少了系統的開銷.ide
在 Java 中, Selector
這個類是 select/epoll/poll 的外包類, 在不一樣的平臺上, 底層的實現可能有所不一樣, 但其基本原理是同樣的, 其原理圖以下所示:函數
全部的 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(); } } }
這個例子的關鍵點:
SelectionKey.OP_ACCEPT
, 這個事件表明的是有客戶端發起TCP鏈接請求.Selector.open
在不一樣的系統裏實現方式不一樣
sunOS 使用 DevPollSelectorProvider, Linux就會使用 EPollSelectorProvider, 而默認則使用 PollSelectorProvider
也就是說 selector.select()
用來阻塞線程, 直到一個或多個 channle 進行 io 操做. 好比 SelectionKey.OP_ACCEPT
.
而後使用 selector.selectedKeys()
方法獲取出, 這些通道.
那麼 selector.select()
是怎麼直到已經有 io 操做了呢?
緣由是由於 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, 而後在作對應操做.
poll 方法有一個很是大的缺陷. poll 函數的返回值是一個整數, 獲得了這個返回值之後, 咱們仍是要逐個去檢查, 好比說, 有一萬個 socket 同時 poll, 返回值是3, 咱們仍是隻能去遍歷這一萬個 socket, 看看它們是否有IO動做.
這就很低效了, 因而, 就有了 epoll 的改進, epoll能夠直接經過「輸出參數」(能夠理解爲C語言中的指針類型的參數), 一個 epoll_event 數組, 直接得到這三個 socket, 這就比較快了.