NIO多路复用底层原理(SelectPollEPoll)

Posted LuckyWangxs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NIO多路复用底层原理(SelectPollEPoll)相关的知识,希望对你有一定的参考价值。

NIO

一、NIO概述

1. BIO

        BIO,即Blockig IO,阻塞IO,一个线程对应一个连接,如果你的服务器有很多用户,每个用户都需要与你的服务器建立一个连接,那么你有多少用户,你的服务器就得创建多少个线程,显然是不显示的,而且每个线程是阻塞的,只要你连接上,我当前的线程就会等着客户端发送数据,然后处理,如果客户端没断开连接,也没发送数据,那服务端的线程就会一直等待,资源被浪费。其模型如下

2. NIO

        NIO一种说法叫 New IO,另一种说法叫Non Blockig IO即非阻塞IO,它是JDK1.4引入的,与BIO不同的是,它是一个线程可以顾及多个连接,在这里引入了IO多路复用模型,多个连接注册到同一个多路复用器,然后多路复用器轮询各个连接,有收发数据的就去处理,其模型如下

二、文件描述符

        本篇文章的NIO是基于Linux的,尽可能浅显易懂,但核心的文件描述符关键词不能忽略,首先给大家介绍一下什么是文件描述符:
        文件描述符是计算机科学中的一个术语,是一个指向文件的引用的抽象化概念。它往往只适用于Unix、Linux这种系统。直白地说,在Linux系统下,一切皆文件,我们不管有什么操作,都离不开对文件的读写,而文件描述符实际上就是一个索引值,它会指向一个文件,这个文件维护了进程对文件操作的记录,说白了,看着这个描述符指向的文件,我就知道我下一步要读写哪个文件了。

三、Select、Poll、EPoll

1. Select

        服务器端的一个线程处理客户端的多个连接请求,解决了BIO一个线程对应一个连接请求的问题,其底层依赖于操作系统的内核,这里的核心为多路复用器,这个多路复用器会管理所有客户端的连接,一旦有数据收发事件触发,便会去轮询所有连接,执行相应业务逻辑,那么,我们想一下,如果你客户端有100个连接,实际有收发数据的只有5个,那么轮询100个,是不是有大量无效循环呢?当然这是JDK1.4刚出BIO时的多路复用器的实现方式,采用的select 模型,而且,当时限制客户端只能有1024个连接,即多路复用器管理的集合最大为1024,那么我有超过1024个用户来连接你的服务端,却发现连不上?你认为合理吗?如果你的是游戏服务器,你认为还会有人玩吗?
        实际上,在操作系统底层,调用了int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)函数, 这是由操作系统提供的,其中的fd_set为文件描述符的集合,而与服务端建立了连接后都会有一个文件描述符存放在这个集合,这个函数会遍历这个集合,以处理有读写需求的连接

2. Poll模型

        后来又对多路复用器做了优化,底层采用poll 模型,去掉了1024的限制,理论上来说不限个数,但其余的实现方式一样,那么又有新的问题出现,如果我客户端有一百万连接,那么只有1000个连接有收发,那么我却要遍历100万个,而如果有收发数据的恰好在集合最末尾,也就是集合遍历到最后才会执行,那么对于客户端来讲意味着什么呢?
Poll模型与Select模型没什么大区别,本质问题没有解决,还是会有很多无效循环,在量大的情况下,非常影响性能

3. EPoll模型

        在JDK1.5,对NIO又进行了一次优化,底层采用了EPoll模型,上图即为采用EPoll 实现的NIO模型,当我们创建多路复用器时,会调用操作系统提供的函数epoll_create,并生成一个epfd,即epoll文件描述符,这个描述符指向的文件所存储的是注册到的多路复用器的事件的文件描述符
        其执行原理: EPoll是基于事件驱动模型,就跟Java的swt一样,只不过在这里的事件并不是人为触发的,而是由操作系统感知的,比如,当前你的服务器有一个新的连接请求发过来,先按照类型将其注册到多路复用器上,然后内核感知到这个请求,采用中断将其文件描述符就绪事件队列当中,后续epoll_wait会处理,比如连接好以后便通过epoll_ctl注册该连接的读事件,当该通道有读请求时,依旧由操作系统内核感知并中断,把它的文件描述符赋复制到epfd中,然后epoll_wait处理后续。
实际上,EPoll模型的底层核心函数有三个:

  1. int epoll_create(int size)

该函数生成一个epoll专用的文件描述符。它其实是在内核申请一空间,用来存放你想关注的socketChannel fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socketChannel fd数。随你定好了。只要你有空间。

  1. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:
epfd: 由 epoll_create生成的epoll专用的文件描述符;
op: 要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD
修改、EPOLL_CTL_DEL 删除
fd: 关联的文件描述符; event:指向epoll_event的指针;如果调用成功返回0,不成功返回-1

  1. int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)

该函数用于轮询I/O事件的发生;
参数:
epfd: 由epoll_create 生成的epoll专用的文件描述符;
epoll_event: 就绪事件列表,此列表由操作系统提供 maxevents: 每次能处理的事件数;
timeout: 等待I/O事件发生的超时值;-1相当于阻塞,0相当于非阻塞。一般用-1即可 返回发生事件数。

        epoll_wait运行的原理是: 等待注册在epfd上的socketChannel fd的事件的发生,如果发生则将发生的socketChannel fd和事件类型放入到events(图中就绪事件列表)数组中。并且将注册在epfd上的socketChannel fd的事件类型给清空(这里是指多路复用器的另外一个集合),所以如果下一个循环你还要关注这个socketChannel fd的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socketChannel fd的事件类型。这时不用EPOLL_CTL_ADD,因为socketChannel fd并未清空,只是事件类型清空。这一步非常重要。

四、总结

        在Linux下,NIO底层就是用的EPoll模型,而selector的底层,是由Linux用C语言在系统级别帮我们创建了一个数据结构体,这个结构体就是EPoll实例,我们可以把它理解为Java对象,可以存取数据。

1. 大致过程如下:

        1.进程先调用epoll的create,返回一个epoll的fd; epoll通过mmap开辟一块共享空间,增删改由内核完成,查询内核和用户进程都可以 这块共享空间中有一个红黑树和一个链表
        2.进程调用epoll的ctl add/delete sfd,把新来的链接放入红黑树中,
                2.1进程调用wait(),等待事件(事件驱动)
        3.当红黑树中的fd有数据到了,就把它放入一个链表中并维护该数据可写还是可读,wait返回;
        4.上层用户空间(通过epoll)从链表中取出fd,然后调用read/write读写数据. 所以epoll也是NIO,不是AIO

NIO编写服务端实现代码

public static void main(String[] args) throws IOException 
    // 1. 获取连接通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 2. 切换成非阻塞模式
    serverSocketChannel.configureBlocking(false);
    // 3. 绑定链接, 是端口
    serverSocketChannel.bind(new InetSocketAddress(8888));
    // 4. 获取选择器
    Selector selector = Selector.open();
    // 5. 将连接通道注册到选择器, 并注册为接受连接事件
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) 
        // 6. select()为获取选择器中的就绪事件, 例如连接事件, 如果有连接请求
        // 则该事件就是就绪的, 如果一个事件都没有, 那么将阻塞在该行代码
        selector.select();
        // 7.获取所有就绪事件, 然后轮询处理
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        // 8.轮询处理就绪事件
        while (iterator.hasNext()) 
            SelectionKey next = iterator.next();
            // 按照不同的监听事件处理做相应的处理
            if (next.isAcceptable()) 
                // 8.1.1获取到当前事件的连接
                ServerSocketChannel server = (ServerSocketChannel) next.channel();
                // 8.1.2接受当前客户端的数据传输通道
                SocketChannel socketChannel = server.accept();
                // 8.1.3设置非阻塞
                if (socketChannel != null) 
                    socketChannel.configureBlocking(false);
                    // 8.1.4将当前数据传输通道注册到选择器, 并注册为读事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功了...");
             else if (next.isReadable()) 
                // 8.2.1获取到当前事件的连接
                SocketChannel socketChannel = (SocketChannel) next.channel();
                ByteBuffer buffer = ByteBuffer.allocate(128);
                int len = socketChannel.read(buffer);
                if (len > 0) 
                    System.out.println(new String(buffer.array()));
                 else if (len == -1) 
                    System.out.println("客户端断开连接...");
                    socketChannel.close();
                

以上是关于NIO多路复用底层原理(SelectPollEPoll)的主要内容,如果未能解决你的问题,请参考以下文章

java nio的实现原理

JAVA 004 网络编程 BIO NIO AIO

java之socket编程(NIO多路复用器)

从底层入手,图解 Java NIO BIO MIO AIO 四大IO模型与原理

Java网络编程和NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型

NIO中的ZeroCopy