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

相关文章
相关标签/搜索