首页 > 其他分享 >NIO 是不是就是IO多路复用?NO

NIO 是不是就是IO多路复用?NO

时间:2023-06-07 14:33:41浏览次数:36  
标签:NIO 多路复用 NO 阻塞 fd IO socketChannel select

似乎从互联网起源,BIO、NIO 的话题就从未间断,无论是平时还是面试。那么他们到底是什么?希望你看完这个文章彻底理解这些概念,同时这边文章也使用 Java 代码实现一个 I/O 多路复用的实例,最后到 I/O 原理。

IO 是什么?

首先要了解什么是 I/O,一次网络请求、一次磁盘读取都是I/O,所以可以泛泛的理解,数据需要通过媒介进行读取和写入都是IO。
下面是一次网络内容读取的 I/O 示意图,数据从外设(网卡)到内核空间,到用户空间(JVM)最后到应用程序的一个过程,那么阻塞和非阻塞指的是哪里呢?
在这里插入图片描述
我们换一个调用链路图说明一下阻塞的位置,第一张图解释的是 BIO,也就是同步阻塞 IO,Java 最早期的版本的 IO 就是这样实现的。当程序调用到读取IO的时候,同步阻塞住程序,直到数据从网卡写入内核空间,再写入用户空间才返回数据,程序才可以继续运行。
在这里插入图片描述
等到 Java 1.4 版本以后就支持了 NIO,其实 NIO 解决的地方是从网卡到内核空间部分的阻塞,也就是说应用程序发送一个读取 IO 的请求,如果数据还没有从网卡写入内核空间,直接返回未就绪,这样就做到了不需要程序死等到结果。等到写入内核空间以后,程序继续读取数据,这时候才会阻塞程序,如下图。
在这里插入图片描述

IO 多路复用

基本的 IO 了解了,那我们开始进入 IO 多路复用的理解。假设我们要实现一个在线聊天系统,一共有 100 人同时聊天,我们先不管“路”多不多,需要同时支持 100 人我们需要怎么做?

我们需要为每一个请求用户创建一个新的线程,才能同时支持多个用户同时使用,然鹅,随着用户越来越多,需要创建的线程也越来越多,频繁的上下文切换和线程的创建和销毁,对于系统性能影响非常大,具体的链路可以通过下面的图理解下。
在这里插入图片描述
那么是不是可以去掉多线程环节,使用一个线程处理 N 个 IO 请求,没有上下文切换和线程的创建和销毁,程序是不是反而更快呢?这就是 IO 多路复用的思路。

首先我们从字面上理解一下,多路复用的英文是 multiplexing,中文翻译为“复用”,其实简单的理解就是使用单个线程通过记录跟踪每一个流(I/O)的状态来同时管理多个I/O,以此来减少线程的创建和切换的开销,进而提高服务器的吞吐能力。那么这么解释下来就更容易理解了,看图一目了然。
在这里插入图片描述
既然使用单线程解决多个 IO 的切换问题,那么必须不能阻塞,如果阻塞了还是需要串行的读取 IO,阻塞程序的,切换就没有意义了,所以 NIO 并完全等于IO多路复用,而是解决多路复用的必要条件“非阻塞“,这也回答了标题的问题。

自己实现 IO 多路复用

上文了解了什么是 IO 多路复用自己也就可以实现了,下面就是我用 Java 写的 IO 多路复用的代码,代码只是表达意思,有问题可以互相讨论。

public void start() throws IOException {
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.bind(new InetSocketAddress(6789));
    serverSocketChannel.configureBlocking(false);
    while (true) {
        SocketChannel channel = serverSocketChannel.accept();
        register(channel);
        Set<SocketChannel> selectionKeys = selectedKeys();
        if (selectionKeys.size() != 0) {
            for (SocketChannel socketChannel : selectionKeys) {
                handle(socketChannel);
            }
        }
    }
}

private void register(SocketChannel channel) {
    if (channel != null) {
        publicKeys.add(channel);
    }
}

private Set<SocketChannel> publicKeys = new HashSet<>();

private Set<SocketChannel> selectedKeys() {
    Set<SocketChannel> publicSelectedKeys = new HashSet<>();
    for (SocketChannel fd : publicKeys) {
        if ((fd.validOps() & OP_READ) != 0) {
            publicSelectedKeys.add(fd);
        }
    }
    return publicSelectedKeys;
}

private void handle(SocketChannel socketChannel) throws IOException {
    ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    StringBuilder sb = new StringBuilder();
    while (socketChannel.read(byteBuffer) > 0) {
        byteBuffer.flip();
        sb.append(StandardCharsets.UTF_8.decode(byteBuffer));
    }
    if (sb.length() > 0) {
        System.out.println("服务端收到消息:" + sb.toString());
    }
}

逻辑相对还是比较简单,直接介绍几个关键的信息。

  1. 定义 register 方法,当有新的连接进来直接添加到 publicKeys set 集合里面,这样就有了一个地方可以存放所有的 socket 连接。
  2. 定义 selectedKeys方法,用于获取所有 ready 状态的 socket,原理是使用 (fd.validOps() & OP_READ) != 0 遍历所有的在 publicKeys里面的 socket判断是否可以读取了,然后添加到 selectedKeys 结果集里面,因为 fd.validOps() 这个判断是非阻塞的,所以可以继续运营程序。
  3. 外层 while (true) 循环获取可以读取的 socket 从而实现 IO 多路复用逻辑。

是不是逻辑很清晰?然后写一个超级简单的 Client 就可以测试验证了。

public void start() throws IOException {
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(6789));
    socketChannel.configureBlocking(false);
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNextLine()) {
        String request = scanner.nextLine();
        if (request != null && request.length() > 0) {
            socketChannel.write(StandardCharsets.UTF_8.encode(request));
        }
    }
}

系统实现

好了,那么问题来了,自己实现的 IO 多路复用不就可以了吗?为啥网上不停的吹嘘 select、poll、epoll 啥的?到这里我们一起看下 select 的源码你就懂了。

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

我们简单的对这些参数讲解下

  1. nfds: 监控的文件描述符集里最大文件描述符加1,简单理解为需要循环遍历的文件数
  2. readfds/writefds/exceptfds:读/写/异常事件的监听描述符集合。
  3. timeout:阻塞的超时时间
    如果你想更深入研究下源码,可以直接到 https://www.kernel.org/ 下载源码,找到 fs/select.c 文件即可。

话题回到最初,通过上面的参数我们了解到,系统层面的 select 的实现其实是更底层的实现了刚才我们写的循环遍历查找思路,只是直接在系统层面性能更高。

那么 poll、epoll 又是什么呢?poll 也是操作系统提供的系统调用函数,它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。epoll 则是最后的升级版本,程序调用 epoll 的时候不需要全量传递了,支持变化传递,同时不需要循环遍历,而是通过异步 IO 事件唤醒程序。

所以到这里你明白了,其实无论是系统或者是直接自己写代码都是在解决单线程复用的问题,也就是我们文章说的 IO 多路复用。

文章转载自:微信公众号-架构师社区

标签:NIO,多路复用,NO,阻塞,fd,IO,socketChannel,select
From: https://www.cnblogs.com/fantongxue/p/17463197.html

相关文章

  • [NOIP2000 提高组] 单词接龙
    题目背景注意:本题为上古NOIP原题,不保证存在靠谱的做法能通过该数据范围下的所有数据。题目描述单词接龙是一个与我们经常玩的成语接龙相类似的游戏,现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”(每个单词都最多在“龙”中出现两次),在两个单......
  • 解决使用yarn安装依赖出现“The engine "node" is incompatible with this module. Ex
    1、问题描述某天在使用yarn安装依赖的时候,突然出现如下错误导致安装依赖终止:Theengine"node"isincompatiblewiththismodule.Expectedversion"^14.18.0||^16.14.0||>=18.0.0".Got"17.9.0"2、解决办法使用如下命令忽略错误:yarnconfigsetignore-enginestr......
  • Exploiting Positional Information for Session-based Recommendation
    目录概符号说明Forward/Backward-awarenessDualPositionalEncodingQiuR.,HuangZ.,ChenT.andYinH.Exploitingpositionalinformationforsession-basedrecommendation.ACMTransactionsonInformationSystems,2021.概本文讨论了一些常用positionalencodi......
  • 在ubuntu上后台启动nodejs、使用pm2启动多个nodejs,
    前言很久以前淘到了一个非常简易的nodejs(聊天室)的源码,今天想把他放到我的ubuntu(server20.10)上。一、正文如果直接使用指令nodeapp.js,退出终端的时候会报错,可以使用pm2来实现后台运行!1.安装node.js和npmapt-getinstallnodejsapt-getinstallnpm安装好了之后可以使用whereisn......
  • QProcess提示Unknown error
    编译环境qt(5.14.2)问题:在使用QProcess进行进程交互的出现UnKnownerror错误代码://创建一个tesseract进程对象QProcess*process=newQProcess(this);process->setProcessChannelMode(QProcess::SeparateChannels);//指定tesseract可执行文件......
  • OpenMP 归约和reduction子句
    简述归约归约操作在MPI里也学过,不过那时候还不太熟悉这种操作。当时只知道MPI_Reduce可以把全局求和和集合通信封装起来,非常方便。实际上将相同的二元归约操作符重复地应用到一个序列上得到结果的计算过程都可以称为归约。python里那个难理解的reduce()函数也就是归约:1......
  • 6-7|IOS如何定义一个类
    在iOS中,可以使用Swift或Objective-C编程语言来定义类。以下是使用Swift编写一个类的示例代码:```swiftclassPerson{  varname:String  varage:Int  vargender:String    init(name:String,age:Int,gender:String){    self.name......
  • IO流 p9 转换流-InputStreamReader 和 OutputStreamWriter
    转换流-InputStreamReader和OutputStreamWriter介绍InputStreamReader:Reader的子类,可以将InputStream(字节流)包装成Reader(字符流);OutputStreamWriter:Writer的子类,实现将OutputStream(字节流)包装成Writer(字符流);当处理纯文本数据时,如果使用字符流效率更高,并且可以有效解决中文......
  • 星座 3 (Constellation 3)
    G星座3(Constellation3)总述:做法很多,一道练习各种套路的有价值的题。对横坐标考虑一个区间\([l,r]\),若该区间上建筑高度的最大值为\(x\),则该区间内\(>x\)的星星数量不能超过\(1\)个。对建筑高度建出笛卡尔树,于是每个节点对应一个区间以及区间高度最大值(即当前子树......
  • Nginx出现403 forbidden (13: Permission denied)报错的解决办法
    一、由于启动用户和nginx工作用户不一致所致1、将nginx.config的user改为和启动用户一致,命令:viconf/nginx.conf二、缺少index.html或者index.php文件,就是配置文件中indexindex.htmlindex.htm这行中的指定的文件。server{listen80;server_namelocalhost;indexindex.p......