BIO到NIO、多路复用器, 从理论到实践, 结合实际案例对比各自效率与特点(上)

news2024/11/11 7:25:52

文章目录

    • 文章引入
    • IO模型及概念梳理
    • BIO
      • 简单介绍
      • 代码样例
      • 压测结果
    • NIO(单线程模型)
      • 简单介绍
      • 与BIO的比较
      • 代码样例
      • 压测结果
    • 多路复用器
      • 问题引入

文章引入

如果你对BIO、NIO、多路复用器有些许疑惑, 那么这篇文章就是肯定需要看的, 本文将主要从概念, 代码实现、发展历程的角度去突出并解决各个IO模型问题, 并用实际代码样例进行演示, 使你更加直观.

本文在介绍各个IO模型后, 都将给出样例代码接收处理5000个连接, 打印出各自处理需要的时间, 各自效率一览便知.

IO模型及概念梳理

Java中的IO模型主要有三种BIO和NIO.AIO.其实主要是同步阻塞,同步不阻塞,异步不阻塞的区别。
这里的同步,异步,阻塞,非阻塞解释如下:

  1. 同步:当触发读写请求后,你的程序自己去读写数据, 即自己写代码去做读写这个动作,那么就是同步的.
    简单来说就是应用发送完指令要参与这个过程,直到返回数据.
  2. 异步:当触发读写请求后(这里不是去缓冲区读写,仅仅告诉内核我想去读取数据),直接返回,内核帮助你完成整个操作后,帮你将数据从内核复制到应用空间,再通知你.
    简单来说就是应用发送完指令不再参与整个过程,直接返回等待通知.
  3. 阻塞:试图对缓冲区进行读写时,当前不可读或者不可写(内核数据没有准备好的情况下),程序进行等待,直到可以操作。
  4. 非阻塞:试图对缓冲区进行读写时,当前不可读或者不可写,读取函数马上返回

看到这里你可能还是对这些概念似懂非懂, 没关系, 当你看到实际演示代码时就自然懂了

当然, 需要提一下, 本篇文章主要讨论同步阻塞和同步非阻塞, 异步非阻塞还没有实际运用起来, 比如Netty,Tomcat现在还依然是NIO. 所以AIO暂不在本文讨论范围内.

BIO

简单介绍

BIO, 即BLOCKING IO, 同步阻塞IO, 那它可能阻塞在哪里呢? 主要是以下两个步骤

  1. accept()
    当服务端准备好入等待客户端连接时, 也就是执行accept方法时, 会发生阻塞, 一直等到有客户端连接进来
  2. read()
    当创建好一个连接后, 这个socket对象去读取数据的时候, 会发生阻塞, 一直等到对方发送过来数据, 并且能够读取返回

代码样例

服务端代码 :

public class ServerSocketTest {
    public static void main(String[] args) {
        ServerSocket server = null;
        try {
            server = new ServerSocket();
            server.bind(new InetSocketAddress(9090));
            System.out.println("server up use 9090");
            long startTime = System.currentTimeMillis();
            int count = 0;
            while(true){
                try {
                    //真正开启监听
                    Socket client = server.accept();
                    System.out.println("client port: "+client.getPort());
                    new Thread(() -> {
                        while(true){
                            try {
                                InputStream in = client.getInputStream();
                                BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                                char[] data = new char[1024];
                                int num = reader.read(data);

                                if(num>0){
                                    System.out.println("client read some data :"+new String(data));
                                }else if(num==0){
                                    System.out.println("client read data nothing......");
                                }else{
                                    System.out.println("client read data error......");
                                    client.close();
                                }

                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();
                    if(++count >= 5000){
                        System.out.println("处理5000个连接用时:"+(System.currentTimeMillis()-startTime)/1000+"s");
                        break;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.exit(0);


    }
}

简单介绍:
上述代码每次拿到一个新的连接后, 会新new一个线程去进行真正的数据读取发送, 主线程就干一件事情, 监听建立新连接.

为什么要这样做呢?

因为如果都在一个线程里, 那么一个服务端只能处理一个客户端请求, 因为读请求也是阻塞的, 会影响到主线程监听建立新连接, 根本没法玩, 玩不动.

客户端代码:

public class C10K {

    public static void main(String[] args) throws InterruptedException {
        InetSocketAddress serverAddr = new InetSocketAddress("192.168.68.2", 9090);
        //线性发起10000个连接
        byte[] bytes = "hello".getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        for (int i = 10000; i < 15000; i++) {
            try {
                //建立连接
                SocketChannel client = SocketChannel.open();
                client.bind(new InetSocketAddress("192.168.68.248",i));
                client.connect(serverAddr);
                //发送数据,这里每次发送固定的,且只是单一的发送固定数据, 就共用一个buffer
                //正常复杂读写为每个连接单独维护buffer更安全可靠
                buffer.put(bytes);
                buffer.flip();
                client.write(buffer);
                buffer.clear();


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

        }
        System.out.println("连接都处理完毕,未避免程序停止关闭连接,这里直接等待......");
        Thread.sleep(50000);
    }
}


压测结果

直接用上述的客户端代码, 不断想服务端发起连接请求, 测试结果如下:
在这里插入图片描述

NIO(单线程模型)

简单介绍

NIO, 这个N有两层含义
对于JDK而言: 是New IO, jdk nio包的类既支持阻塞模型,也支持非阻塞模型
对于操作系统: 是 NO BLOCKING

我们这里讲的主要是操作系统层面的,即非阻塞IO, BIO会在accept()和read()方法阻塞, 而NIO则均不会阻塞, 会直接返回,

比如accept()方法, 如果有新连接, 就返回连接对象, 如果没有, 则立即返回null.这一点可以在后序的样例代码中体会到.

与BIO的比较

传统BIO的劣势: 每新来一个连接就要new一个线程, 非常非常耗费资源,支持不了数量庞大的连接.

NIO的优势 : 可以通过一个或几个线程来解决N个IO连接的处理.比如以下的样例, 处理5000个连接只用了一个线程.

代码样例

服务端代码:

public class SocketNIO {

    public static void main(String[] args) throws Exception {


        ArrayList<SocketChannel> clients = new ArrayList<>();


        //通道, 可读可写, 是JDK new io 中的工具类
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(9090));
        //设置为非阻塞, 即no blocking, 当设置了这一步进行accept时便不会阻塞,会直接返回
        //如果设置为true, 就相当于BIO
        serverSocketChannel.configureBlocking(false);
        //ByteBuffer同时支持读写,有指针控制
        //写切换成读需要调用flip方法,就是将指针移动至未读的位置
        //读切换成写需要调用compact方法, 把读取过的字节往前覆盖掉,指针移动至还未写入数据的位置
        //这里可以采用堆内或者堆外的方式分配内存,  Direct就是堆外的方式
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //ByteBuffer allocate = ByteBuffer.allocateDirect(4096);
        long startTime = System.currentTimeMillis();
        while (true) {
            //这里如果没有接收到客户端, jdk api便会返回null
            //如果有, 便会返回相应对象
            SocketChannel client = serverSocketChannel.accept();

            if(client==null){
                continue;
            }
            //这里设置的是read不进行阻塞
            client.configureBlocking(false);
            int port = client.socket().getPort();
            System.out.println("add client port:"+port);
            clients.add(client);

            for (int i = 0; i < clients.size(); i++) {
                SocketChannel curClient = clients.get(i);
                //这里不阻塞,直接返回
                //没有数据的话直接返回-1
                int read = curClient.read(buffer);
                if(read<=0){
                    continue;
                }
                //对于buffer,刚刚是写,现在进行读操作,调用flip
                buffer.flip();

                byte[] bytes = new byte[buffer.limit()];
                buffer.get(bytes);

                String str = new String(bytes);
                System.out.println(curClient.socket().getRemoteSocketAddress()+" -->" +str);

                //一次循环之后又需要进行写入,这里直接clear清0,就无需调用compact方法
                buffer.clear();

            }
            if(clients.size() >=  5000){
                System.out.println("处理5000个连接用时:"+(System.currentTimeMillis()-startTime)/1000+"s");
                break;

            }

        }
        serverSocketChannel.close();

    }
}

测试使用的客户端代码还是和上面的一样, 这里不再放了.

压测结果

还是用和BIO相同的客户端代码, 压测结果如下:

可能会比较惊讶, 竟然比BIO用时还多? 那这个NIO究竟慢在哪里呢?
继续往下看就知道了, 这也是为什么会有多路复用器.

在这里插入图片描述

多路复用器

问题引入

  1. 从上面两个比较结果看, 同样处理5000个请求, 直接使用NIO竟然比BIO用时还久, 这个一方面是因为上述NIO代码只用了一个线程, 如果分摊到几个线程, 比如5个, 每个线程负责1000个连接, 那速度会快一些

  2. 第二个最主要的点是传统NIO每次都是主动调用,主动去查看有没有新连接, 对于每一个连接有没有新数据,
    每次循环的时间复杂度是0(n) n代表连接数, 也就是假如有10000个链接, 每次都要挨个循环一遍, 看看其缓冲区有没有准备好的数据, 并且这个操作(read)需要系统调用,调用内核级别的方法,涉及到用户态内核态切换,相当于10000次的系统调用, 成本很高
    但是可能10000个连接里面可能某一个时刻,只有100个连接是有数据的, 这样循环就会造成浪费

这里引入了多路复用器, 但是多路复用器原理是什么, 怎么使用, 真实效率到底怎么样?

可直接点击链接观看BIO到NIO、多路复用器, 从理论到实践, 结合实际案例对比各自效率与特点(下)

今天的分享就到这里了,有问题可以在评论区留言,均会及时回复呀.
我是bling,未来不会太差,只要我们不要太懒就行, 咱们下期见.
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/932098.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于FPGA的Lorenz混沌系统verilog开发,含testbench和matlab辅助测试程序

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 将vivado的仿真结果导入到matlab显示三维混沌效果&#xff1a; 2.算法运行软件版本 vivado2019.2 matlab2022a 3.部分核心程序 testbench如下所…

怎么对App进行功能测试

测试人员常被看作是bug的寻找者&#xff0c;但你曾想过他们实际是如何开展测试的吗&#xff1f;你是否好奇他们究竟都做些什么&#xff0c;以及他们如何在一个典型的技术项目中体现价值&#xff1f;本文将带你经历测试人员的思维过程&#xff0c;探讨他们测试app时的各种考虑. …

MyBatis-Plus框架技术总结

MybatisPlus 1、概述 MybatisPlus是一款Mybatis增强工具&#xff0c;用于简化开发&#xff0c;提高效率。 它在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 ​ 官网&#xff1a; https://mp.baomidou.com/ 2、快速入门 2.0、准备工作 ①准…

PROFIBUS主站转MODBUS TCP网关

1.产品功能 YC-DPM-TCP网关在Profibus总线侧实现主站功能&#xff0c;在以太网侧实现ModbusTcp服务器功能。可将Profibus DP从站接入到ModbusTcp网络&#xff1b;通过增加DP/PA耦合器&#xff0c;也可将Profibus PA从站接入ModbusTcp网络。YC-DPM-TCP网关最多支持125个Profibu…

wireshark 流量抓包例题重现

目录 要求 黑客攻击的第一个受害主机的网卡IP地址黑客对URL的哪一个参数实施了SQL注入第一个受害主机网站数据库的表前缀 第一个受害主机网站数据库的名字 要求 &#xff08;1&#xff09;黑客攻击的第一个受害主机的IP地址 &#xff08;2&#xff09;黑客对URL的某一参数实…

在服务器上搭建Jenkins

目录 1.服务器要求 2.官方文档 3.在服务器上下载Jenkins 3.1 下载war包 3.2 将war包上传到服务器的一个目录下 3.3 启动jenkins 3.3.1 jdk版本升级 1&#xff09;下载jdk17 2&#xff09;解压到当前文件夹 3&#xff09;配置路径 4.jenkins配置 4.1 填写初始密码&a…

直线模组在搬运行业的应用

近几年&#xff0c;国内直线模组的研发发展非常快&#xff0c;直线模组应用的范围也在一直在扩大&#xff0c;发展到今天&#xff0c;已经被广泛应用到各种设备中&#xff0c;尤其是在搬运行业中&#xff0c;是搬运行业中必不可少的传动元件之一。 直线模组在搬运中的应用&…

算法笔记(一):时间复杂度

省略的技巧 已知 f ( n ) f(n) f(n) 来说&#xff0c;求 g ( n ) g(n) g(n) 表达式中相乘的常量&#xff0c;可以省略&#xff0c;如 f ( n ) 100 ∗ n 2 f(n) 100*n^2 f(n)100∗n2 中的 100 100 100 多项式中数量规模更小&#xff08;低次项&#xff09;的表达式&#…

vue create -p dcloudio/uni-preset-vue my-project创建文件报错443

因为使用vue3viteuniappvant4报错&#xff0c;uniapp暂不支持vant4&#xff0c;所以所用vue2uniappvant2 下载uni-preset-vue-master 放到E:\Auniapp\uni-preset-vue-master 在终端命令行创建uniapp vue create -p E:\Auniapp\uni-preset-vue-master my-project

mysql profiling profiles profile

要想优化一条 Query&#xff0c;我们就需要清楚的知道这条 Query 的性能瓶颈到底在哪里&#xff0c;是消耗的 CPU计算太多&#xff0c;还是需要的的 IO 操作太多&#xff1f;要想能够清楚的了解这些信息&#xff0c;在 MySQL 5.0 和 MySQL 5.1正式版中已经可以非常容易做到了&a…

云计算中的数据安全与隐私保护策略

文章目录 1. 云计算中的数据安全挑战1.1 数据泄露和数据风险1.2 多租户环境下的隔离问题 2. 隐私保护策略2.1 数据加密2.2 访问控制和身份验证 3. 应对方法与技术3.1 零知识证明&#xff08;Zero-Knowledge Proofs&#xff09;3.2 同态加密&#xff08;Homomorphic Encryption&…

cublas_v2.h没有那个文件和目录,解决

我的是orin&#xff0c;使用的cuda11.4&#xff0c;后来发现通过sudo jetson_release看到的CUDA是没有安装的。 定位到问题是&#xff1a; 使用ls /usr/local/ -lha查看软连接&#xff0c;如下&#xff1a; 能够发现cuda这个软连接是有问题的&#xff0c;他链接的是cuda10.2 …

几种在JavaScript中创建对象的方式!

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 字面量方式⭐ 构造函数方式⭐ Object.create()方式⭐ 工厂函数方式⭐ ES6类方式⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门…

Python分享之redis(2)

Hash 操作 redis中的Hash 在内存中类似于一个name对应一个dic来存储 hset(name, key, value) #name对应的hash中设置一个键值对&#xff08;不存在&#xff0c;则创建&#xff0c;否则&#xff0c;修改&#xff09; r.hset("dic_name","a1","aa&quo…

java八股文面试[多线程]——sleep wait join yield

sleep和wait有什么区别 sleep 方法和 wait 方法都是用来将线程进入阻塞状态的&#xff0c;并且 sleep 和 wait 方法都可以响应 interrupt 中断&#xff0c;也就是线程在休眠的过程中&#xff0c;如果收到中断信号&#xff0c;都可以进行响应并中断&#xff0c;且都可以抛出 In…

ServiceManger Binder的处理流程

陌生知识点如下&#xff1a; BinderProxy&#xff1a;是将Native层的BpBinder对象进行封装后传给Java层使用的Binder对象android_util_binder: Binder在JNI层的相关注册&#xff0c;处理&#xff0c;转换封装接口BpBinder:Binder驱动在Native层的封装。IPCThreadState&#xf…

3.BGP状态机和路由注入方式

BGP状态机 BGP路由的生成 不同于IGP路由协议,BGP自身并不会发现并计算产生路由,BGP将GP路由表中的路由注入到BGP路由表中,并通过Update报文传递给BGP对等体。 BGP注入路由的方式有两种: Networkimport-route与IGP协议相同,BGP支持根据已有的路由条目进行聚合,生成聚合路由…

【Linux】多线程概念线程控制

文章目录 多线程概念Linux下进程和线程的关系pid本质上是轻量级进程id&#xff0c;换句话说&#xff0c;就是线程IDLinux内核是如何创建一个线程的线程的共享和独有线程的优缺点 线程控制POSIX线程库线程创建线程终止线程等待线程分离 多线程概念 Linux下进程和线程的关系 在…

Pygame编程(9)font模块

Pygame编程&#xff08;9&#xff09;font模块 函数示例 函数 pygame.font.init 初始化字体模块init() -> None pygame.font.quit 反初始化字体模块quit() -> None pygame.font.get_init True,如果字体模块已初始化get_init() -> bool pygame.font.get_default_font …

VUE笔记(九)vuex

一、vuex的简介 1、回顾组件之间的通讯 父组件向子组件通讯&#xff1a;通过props实现 子组件向父组件通讯&#xff1a;通过自定义事件($emit)方式来实现 兄弟组件之间的通讯&#xff1a;事件总线&#xff08;$eventBus&#xff09;、订阅与发布方式来实现 跨级组件的通讯…