博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
网络编程--多路复用器select、poll、epol,javaNIO原理和实现
阅读量:2048 次
发布时间:2019-04-28

本文共 7146 字,大约阅读时间需要 23 分钟。

网络编程–多路复用器select、poll、epol,javaNIO原理和实现

之前已经说过了BIO模型的原理和实现,并根据其不足(阻塞,多线程资源消耗等),介绍了内核的升级实现了accpet和read不阻塞的方法,以及介绍了channel和buffer的模型和实现。

上篇结束的时候提到了NIO(os层面)不足之处
在这里插入图片描述
承接上文,如果有很多的链接进来,单纯的NIO的使用,我们程序需要对所有链接进行地毯式的遍历,监听所有链接事件,大致java实现模型如下:
在这里插入图片描述

既然知道了上述模型的弊端,就会有解决的办法:如果程序不需要每次都地毯式的遍历所有链接,而是我们将所有链接(文件描述符)都告诉内核,内核只返回给我们其中有状态的IO,我们程序中只对这些链接进行遍历不就行了。

那接下来就说说这样实现思想的多路复用器的使用。

Select、poll(实现模型类似,当前以select讲解)

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

监视并等待多个文件描述符的属性变化(可读、可写或错误异常)。select()函数监视的文件描述符分 3 类,分别是writefds、readfds、和 exceptfds。调用后 select() 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间),函数才返回。当 select()函数返回后,可以通过遍历 fdset,来找到就绪的描述符。

大致模型如下:
在这里插入图片描述

将需要监控的文件描述符作为参数传递给select,select会只返回有事件的资源信息,这样就避免了地毯式遍历,浪费资源。

那么select又是怎么知道哪些是有事件的资源呢?或者说内核又是怎么实现select的呢?

select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。

  1. select的睡眠过程

支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。

select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。

下面我们看看select睡眠的详细过程。

select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。

通过上述的理解,select多路复用器有什么弊端呢?

我们发现

  1. 每次都要通过参数传递的形式将文件描述符传给select,进行对每个文件描述监听。过程中有一个文件描述符的拷贝过程,从用户控件–> 内核空间。
  2. select每次都需要对所有的文件描述符进行遍历,消耗性能。

针对上面问题,有什么改变的思想呢?

如果不用每次都将文件描述符拷贝给内核,不用每次都遍历所有文件描述符,而是让内核自己记录好所有的有事件的文件描述符,我们最终去取就行了。

2. epoll讲解

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

epoll 的3个系统调用:

  1. epoll_create :创建一个epoll对象的文件描述符,同时开辟一个内核空间,将自己放到该空间中,空间的数据结构为红黑树。
  2. epoll_ctl : 将所有需要监听的链接加入到空间中。
  3. epoll_wait : 返回所有有时间的集合链表 。

实现模型如下:

在这里插入图片描述
通过以上了解,我们对比下selet,epoll不在需要从用户端拷贝fd_set到内核空间,而且也不用对任何fd进行遍历操作,时间复杂度从O(n)直接降到了O(1),现实中,基本上都不使用select和poll,面对多连接的并发场景,基本上都选择使用epoll。

JAVA NIO对多路复用器的整合实现Selector

以上讲些的多路复用器的实现原理实现都是C和部分系统调用相关的代码,那么JAVA是怎么使用的多路复用器呢?

java在1.4之后推出了NIO(new nio) 其中一个核心组件就是selector,它就是对多路复用器的封装实现。(注:selector中的多路复用器可能是select、poll、epoll其中任意一个)

Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。

使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。

实现模型;

在这里插入图片描述
java代码演示,实现聊天功能:

package main.java.com.nio;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.*;import java.util.Iterator;/** * Created by cmm on 2021/6/12. */public class NioServer {    private ServerSocketChannel serverSocketChannel;    private Selector selector ;    NioServer(){        try {            // 1. 创建服务器端通道            serverSocketChannel = ServerSocketChannel.open();            // 2. 设置成非阻塞状态            serverSocketChannel.configureBlocking(false);            // 3. 创建一个seletor选择器            /**            创建多路复用器:优先epoll            epoll : 调用epoll_create 创建epoll对象,并开辟一个内存空间            select | poll : 创建一个数组来存放文件描述符            */            selector = Selector.open();            // 4. 建立服务器端            serverSocketChannel.bind(new InetSocketAddress("127.0.0.1",9999));                        // 5. 将该服务器通道注册到选择器,并开始监控accpet事件             /**             select | poll : 将文件描述符fd 放入数组中。             epoll : 调用epoll_ctl,将文件描述符fd放入内存空间。            */            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);            System.out.print("服务器建立成...");            lisent(selector);        }catch (Exception e){            System.out.print("服务器建立异常");        }    }    // 实施监听    private void lisent(Selector selector) {        while (true){            try {            /**              select | poll : 查看数组,返回所有fd              epoll : 查看红黑树中的fd              */              selector.keys()                 /**                   select | poll : 传入df,遍历查找有事件状态的IO                   epoll :调用epoll_wait                 */                if(selector.select(1000)>0){                     // 1. 将活动的事件接收selector.selectedKeys()---获取所有活动的事件                    Iterator iterator = selector.selectedKeys().iterator();                    while(iterator.hasNext()){                        // 2. 迭代遍历一个个有事件的selectionkey                         SelectionKey selectionKey = (SelectionKey) iterator.next();                         if(selectionKey.isAcceptable()){                             // 3.接收链接                             SocketChannel socketChannel = serverSocketChannel.accept();                             socketChannel.configureBlocking(false);                             if(socketChannel.finishConnect()){                                 System.out.println("欢迎【客户端】"+socketChannel.getRemoteAddress()+"登录");                                 // 4.并将客户端也加入selector管理,同时开启read事件监听                                 socketChannel.register(selector,SelectionKey.OP_READ);                             }                         }                         // 5 处理可读事件                         if(selectionKey.isReadable()){                             ReadContent(selectionKey);                         }                         iterator.remove();                    }                }            } catch (IOException e) {                e.printStackTrace();            }        }    }    // 读取客户端信息    private void ReadContent(SelectionKey selectionKey) throws IOException {        // 1. 接收客户端channel、        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();        // 2. 创建缓冲区        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);        // 3. 读取信息        try {            int read = socketChannel.read(byteBuffer);            if(read>0){                System.out.println("【客户端】"+socketChannel.getRemoteAddress()+"说:"+new String(byteBuffer.array()));                // 信息转发给其他客户端                for(SelectionKey selectionKeyother : selector.keys()){                    Channel channel = selectionKeyother.channel();                    // 屏蔽自己                    if(channel instanceof SocketChannel && socketChannel!=channel){                        SocketChannel socketChannelohter1 = (SocketChannel) channel;                        socketChannelohter1.write(ByteBuffer.wrap(byteBuffer.array()));                    }                }            }        }catch (Exception e){            selectionKey.cancel();            System.out.println("【客户端】"+socketChannel.getRemoteAddress()+"已下线");        }    }    public static void main(String[] args) {        new NioServer();    }}

总结

服务端将ServerSocketChannel注册到Selector,Selector轮训,通过selector.select()阻塞判断是否有监听事件到达,如果有事件到达,获取到事件SelectionKey的集合,通过迭代SelectionKey集合,判断事件类型,如果是连接事件,则把当前Channel注册到Selector,如果当前Channel有数据可读,则执行相应的操作,执行完成后移除当前SelectionKey,继续迭代处理下一个SelectionKey。

既然如上面所说, Oracle JDK在Linux已经默认使用epoll方式, 为什么netty还要提供一个基于epoll的实现呢?

这是stackoverflow上的一个问题。 Netty的核心开发者 Norman Maurer这么说的:

1. Netty的 epoll transport使用 epoll edge-triggered 而 java的 nio 使用   level-triggered。2.另外netty epoll transport 暴露了更多的nio没有的配置参数, 如   TCP_CORK, SO_REUSEADDR等等

所以后续开始说说Netty的模型和实现。

转载地址:http://wnhof.baihongyu.com/

你可能感兴趣的文章
【Python】自动化测试框架-共通方法汇总
查看>>
【Python】if相关知识点
查看>>
【Python】xpath中为什么粘贴进去代码后老报错?如何在定位元素的时候准确找到定位切入点?...
查看>>
Loadrunner解决启动浏览器后页面显示空白
查看>>
【Python】唯品会购买商品
查看>>
【JMeter】如何录制创建及得到曲线图
查看>>
【Loadrunner】Error -26601: Decompression function 错误解决、27728报错解决方案
查看>>
【其他】csv文件打开是乱码,怎么办?
查看>>
【Python】web.py初识学习
查看>>
【Python】【Web.py】python调用html【问题:echart图标调用html上未显示】
查看>>
【雅思】金山词霸-单词学习(1-40)
查看>>
【F12】谷歌浏览器F12前端调试工具 Console
查看>>
【服务器】如何在服务器发布网站?Sasa讲解
查看>>
【F12】九个Console命令,让js调试更简单
查看>>
【数据库】left join(左关联)、right join(右关联)、inner join(自关联)的区别...
查看>>
【雅思】【写作】【大作文】Advantage VS. Disadvantage
查看>>
【雅思】金山词霸-单词学习(41-80)
查看>>
【雅思】【写作】【大作文】Report
查看>>
【雅思】【作文】顾家北100句翻译
查看>>
【雅思】【写作】【大作文】Discuss both views and give your own opinion
查看>>