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, 这就比较快了.