重新认识BIO、NIO、IO多路复用、Select、Poll、Epollo它们之间的关系

news/2025/2/22 14:49:26

目录

一、背景

二、名词理解

(1)BIO

(2)NIO

(3)IO多路复用

Select%E3%80%81Poll%E3%80%81Epollo-toc" style="margin-left:80px;">(4)SelectPollEpollo

三、他们之间的关系总结

一、背景

最近又在学习网络IO相关知识,对我们常说的BIO、NIO、IO多路复用、SelectPollEpollo之间到底是什么关系,进行重新学习以及总结,特此记录一下。希望对一样迷惑的朋友也有一定的帮助

二、名词理解

(1)BIO

BIO它是Blocking IO的缩写,它的关键的特点就是阻塞IO,什么叫阻塞IO呢,就是你可以理解为它处理连接、读写事件是阻塞的,它采用阻塞方式进行数据读写操作。即当一个线程在执行IO操作时,若没有数据可读,则该线程会一直阻塞等待,直到有数据可读或者超时。如果还不理解我们不妨看看具体的BIO服务端实现的代码,加深自己的理解。

  • BIO服务端代码
package bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * bio - server 类
 *
 * @author chen
 * @date 2024年03月10日 10:43
 */
public class BioServer {
    public static void main(String[] args) {
        try {
            System.out.println("*****服务端启动******");
            //定义一个ServerSocket对象进行一个服务端的接口注册
            ServerSocket serverSocket = new ServerSocket(8888);
            //监听客户端的Socket链接请求
            Socket socket = serverSocket.accept();
            //从socket对象中得到一个字节输入流对象
            InputStream is = socket.getInputStream();
            //将字节输入流包装成一个缓冲字符输入流(不能直接将字节输入流包装成缓冲字符输入流,先将字节输入流转成字符输入流,再转成缓冲字符输入流)
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println("服务端收到信息:"+msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

这里我们看到BIO的accept和read方法都是阻塞方法,也就是说线程一旦调用就会进行阻塞

看到这里我们不妨可以思考一下,那我如果想实现多连接处理应该怎么实现?

既然我们一个连接一个线程,那是否我们可以看更多的线程去处理连接,没错,早期的BIO是这样都是这样去处理的,如下代码,加入线程池后的处理

package bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * TODO 类功能描述
 *
 * @author chen
 * @date 2024年03月10日 10:43
 */
public class BioMoreThreadServer {
    public static ExecutorService executorService = Executors.newFixedThreadPool(2);
    public static void main(String[] args) {
        try {
            System.out.println("*******服务端启动*********");
            //注册端口
            ServerSocket ss = new ServerSocket(8888);
            //定义一个死循环,负责不断的接收客户端的Socket的连接请求
            while (true){
                Socket socket = ss.accept();
                //创建一个独立的线程来处理这个客户端socket的通信需求
//                new ServerThreadReader(socket).start();
                //用线程池替代,线程池只是解决了防止频繁创建和销毁线程,并没有解决可以更多的支持连接这个问题(连接数还是有限)
                executorService.submit(new ServerThreadReader(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

这里需注意一个细节,加入线程池后,并没有解决能够支持更多的连接的问题,仅仅只是防止线程频繁创建以及销毁,降低系统压力。

看完了BIO我们会提出一个疑问,那如果能够支持更大的连接数呢?

(2)NIO

 NIO是Java 1.4引入的新的IO模型,它采用了多路复用器(Selector)机制,通过少量线程同时管理多个通道,实现了单线程同时处理多个请求的效果。NIO具有高并发性、高吞吐量和更高的可靠性,适合处理连接数多且连接时间较短的场景。NIO的核心组件包括通道(Channel)、缓冲区(Buffer)和选择器(Selector)。选择器允许程序同时等待多个通道上的事件,如连接、数据到达等。

从我个人理解的角度,就是新的网络IO模型,增加了在网络IO读取过程中的一些特性,例如非阻塞、高吞吐、高并发性。那么他跟IO多路复用有什么关系呢?

(3)IO多路复用

IO多路复用(IO Multiplexing)一种同步IO模型,单个进程/线程就可以同时处理多个IO请求。一个进程/线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个进程/线程。

这里从我个人的角度理解,以盖房子为例,NIO是一个基础,提供了大量的基础工具(通道[Channel]、缓冲区[Buffer]和选择器[Selector]等等),就像一个我们要盖房子的各种工具已经给你备好了,接下来我们要盖一个怎样的房子(IO多路复用),相当于我们用NIO的技术实现了IO多路复用,如果没有NIO本身这项技术的支持,就不可能实现IO多路复用。

IO多路复用的代码实现

package nio;

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;

/**
 * TODO 类功能描述
 *
 * @author chen
 * @date 2024年03月10日 13:01
 */
public class NioServer {
    public static void main(String[] args) {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(8888));
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 无限判断当前线程状态,如果没有中断,就一直执行while内容。
            while(!Thread.currentThread().isInterrupted()){
                // 获取准备就绪的channel
                if (selector.select() == 0) {
                    continue;
                }
                // 获取到对应的 SelectionKey 对象
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = keys.iterator();
                // 遍历所有的 SelectionKey 对象
                while (keyIterator.hasNext()){
                    // 根据不同的SelectionKey事件类型进行相应的处理
                    SelectionKey key = keyIterator.next();
                    if (!key.isValid()){
                        continue;
                    }
                    if (key.isAcceptable()){
                        accept(serverSocketChannel,selector,key);
                    }
                    if(key.isReadable()){
                        read(key);
                    }
                    // 移除当前的key
                    keyIterator.remove();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void read(SelectionKey key) {
        try {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            //清除缓冲区,准备接受新数据
            ByteBuffer readBuffer = ByteBuffer.allocate(1024);//调整缓冲区大小为1024字节
            int numRead = socketChannel.read(readBuffer);;
            String str = new String(readBuffer.array(),0,numRead);
            System.out.println("read String is: " + str);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private static void accept(ServerSocketChannel serverSocketChannel, Selector selector, SelectionKey key) {
        try {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            // 注册客户端读取事件到selector
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("client connected " + socketChannel.getRemoteAddress());
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

那这个时候IO多路复用算是明白了 ~~

那此时有提出一个疑问:那么IO多路复用跟我们常说的SelectPollEpollo又有什么关系?

Select%E3%80%81Poll%E3%80%81Epollo">(4)SelectPollEpollo

SelectPollEpollo的概念我这里就不一一叙说

总结起来他们就是操作系统函数,不同的操作系统(window、linux)有不同的实现,而且这项技术是不断演化,从早期的Select、演化到Poll、再到Epollo。上面讲到IO多路复用能够使用一个线程监听连接、读写事件,它们在操作系统底层是如何实现的呢?(任何的Java逻辑都是会回归操作系统,例如Java的创建线程,底层也是调用操作系统函数进行线程的创建)其实我们可以追到Select方法的源码,最终会调用本地方法,这里答案也就揭晓了,这三个操作系统函数,是操作系统帮我实现能够使用一个线程监听多个事件的功能,它们是实现IO多路复用的在操作系统层面的基石。

protected int doSelect(long var1) throws IOException {
        if (this.channelArray == null) {
            throw new ClosedSelectorException();
        } else {
            this.timeout = var1;
            this.processDeregisterQueue();
            if (this.interruptTriggered) {
                this.resetWakeupSocket();
                return 0;
            } else {
                this.adjustThreadsCount();
                this.finishLock.reset();
                this.startLock.startThreads();

                try {
                    this.begin();

                    try {
                        this.subSelector.poll();
                    } catch (IOException var7) {
                        this.finishLock.setException(var7);
                    }

                    if (this.threads.size() > 0) {
                        this.finishLock.waitForHelperThreads();
                    }
                } finally {
                    this.end();
                }

                this.finishLock.checkForException();
                this.processDeregisterQueue();
                int var3 = this.updateSelectedKeys();
                this.resetWakeupSocket();
                return var3;
            }
        }
    }

三、他们之间的关系总结

最后我画一张图来总结它们的关系

NIO本身这项技术本身是一个大的基石,我们利用了这个基石实现了IO多路复用,而SelectPollEpollo这些操作系统函数,是我们操作系统层面的基石,借助这种机制能够帮我们实现事件监听,实现单个线程处理多个事件。从而实现IO多路复用机制,这样就解决了BIO连接数的限制,能够支持处理更多的连接。


http://www.niftyadmin.cn/n/5429373.html

相关文章

大语言模型RAG-langchain models (二)

大语言模型RAG-langchain models (二) 往期文章&#xff1a;大语言模型RAG-技术概览 (一) 文章目录 大语言模型RAG-langchain models (二)**往期文章&#xff1a;[大语言模型RAG-技术概览 (一)](https://blog.csdn.net/tangbiubiu/article/details/136651625)**核心模块总览Mod…

微信小程序(五十九)使用鉴权组件时原页面js自动加载解决方法(24/3/14)

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.使用覆盖函数的方法阻止原页面的自动执行方法 2.使用判断实现只有当未登录时才进行方法覆盖 源码&#xff1a; app.json {"pages": ["pages/index/index","pages/logs/logs"],…

Pytorch学习 day11(优化器、使用和修改现有网络模型)

优化器 在模型的训练过程中&#xff0c;我们根据反向传播来获取每个网络节点的对应梯度&#xff0c;之后通过某种算法&#xff08;优化器&#xff09;根据对应梯度来不断更新节点的参数&#xff0c;最终达到最小loss的结果。优化器有很多&#xff0c;但有两个参数是所有优化器…

Spring Bean的生命周期流程

前言 Java 中的公共类称之为Java Bean&#xff0c;而 Spring 中的 Bean 指的是将对象的生命周期&#xff0c;交给Spring IoC 容器来管理的对象。所以 Spring 中的 Bean 对象在使用时&#xff0c;无需通过 new 来创建对象&#xff0c;只需要通过 DI&#xff08;依赖注入&#x…

适用于系统版本:CentOS 6/7/8的基线安全检测脚本

#!/bin/bash #适用于系统版本&#xff1a;CentOS 6/7/8 echo "----------------检测是否符合密码复杂度要求----------------" #把minlen&#xff08;密码最小长度&#xff09;设置为8-32位&#xff0c;把minclass&#xff08;至少包含小写字母、大写字母、数字、特殊…

某赛通电子文档安全管理系统 DecryptApplication 任意文件读取漏洞(2024年3月发布)

漏洞简介 某赛通电子文档安全管理系统 DecryptApplication 接口处任意文件读取漏洞&#xff0c;未经身份验证的攻击者利用此漏洞获取系统内部敏感文件信息&#xff0c;导致系统处于极不安全的状态。 漏洞等级高危影响版本*漏洞类型任意文件读取影响范围>1W 产品简介 …

如何高效使用ChatGPT技巧

第一章:了解ChatGPT ChatGPT是一种基于人工智能的自然语言处理模型,旨在实现对话生成。它是OpenAI公司开发的一款先进的语言模型,使用了大规模的预训练数据和深度学习技术。ChatGPT可以生成与用户进行对话的回答,提供有用的信息和交流。 1.1 ChatGPT的工作原理 ChatGPT的…

项目性能优化—性能优化的指标、目标

项目性能优化—性能优化的指标、目标 性能优化的终极目标是什么 性能优化的目标实际上是为了更好的用户体验&#xff1a; 一般我们认为用户体验是下面的公式&#xff1a; 用户体验 产品设计&#xff08;非技术&#xff09; 系统性能 ≈ 系统性能 快 那什么样的体验叫快呢…