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

文章目录

    • 文章引入
    • 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,未来不会太差,只要我们不要太懒就行, 咱们下期见.
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

cocosCreator 之 微信小游戏打包

版本&#xff1a; v3.8.0 环境&#xff1a; Mac 介绍 cocosCreator 支持将游戏发布到多个小游戏平台&#xff0c;并提供了打包等流程处理。 本篇文章主要讲述下微信小游戏的发布流程相关。更多内容参考官方文档&#xff1a; 发布到小游戏平台 微信小游戏的发布相关&#xff…

vue3 DOM元素渲染完成之后执行

在Vue 3中&#xff0c;可以使用nextTick函数来在DOM元素渲染完成之后执行代码。nextTick函数会在下次DOM更新循环结束之后执行提供的回调函数。 例如&#xff0c;在Vue 3的组件中&#xff0c;可以这样使用nextTick函数&#xff1a; import { nextTick } from vue;export defa…

多线程与高并发——并发编程(3)

文章目录 三、锁1 锁的分类1.1 可重入锁、不可重入锁1.2 乐观锁、悲观锁1.3 公平锁、非公平锁1.4 互斥锁、共享锁2 深入synchronized2.1 类锁、对象锁2.2 synchronized的优化2.3 synchronized实现原理2.4 synchronized的锁升级2.5 重量级锁底层 ObjectMonitor3 深入ReentrantLo…

并发编程的故事——并发之共享模型

并发之共享模型 文章目录 并发之共享模型一、多线程带来的共享问题二、解决方案三、方法中的synchronize四、变量的线程安全分析五、习题六、Monitor七、synchronize优化八、wait和notify九、sleep和wait十、park和unpark十一、重新理解线程状态十二、多把锁十三、ReentrantLoc…

CocosCreator3.8研究笔记(二)windows环境 VS Code 编辑器的配置

一、设置文件显示和搜索过滤步骤 为了提高搜索效率以及文件列表中隐藏不需要显示的文件&#xff0c; VS Code 需要设置排除目录用于过滤。 比如 cocoscreator 中&#xff0c;编辑器运行时会自动生成一些目录&#xff1a;build、temp、library&#xff0c; 所以应该在搜索中排除…

云计算的三个主要服务模型:IaaS、PaaS 和 SaaS

文章目录 介绍基础设施即服务&#xff08;Infrastructure as a Service&#xff0c;IaaS&#xff09;平台即服务&#xff08;Platform as a Service&#xff0c;PaaS&#xff09;软件即服务&#xff08;Software as a Service&#xff0c;SaaS&#xff09; 区别基础设施即服务&…

C++面试题(吴)-计算机网络部分(2)和常见设计模式

目录 1.网络部分 3.32 说说浏览器从输入 URL 到展现页面的全过程 3.33 简述 HTTP 和 HTTPS 的区别&#xff1f; 3.34 说说 HTTP 中的 referer 头的作用 3.35 说说 HTTP 的方法有哪些 3.36 简述 HTTP 1.0&#xff0c;1.1&#xff0c;2.0 的主要区别 3.37 说说 HTTP 常见的…

vue组装模板(侧边栏+顶部+主体)--项目阶段4

目录 一、前言介绍 二、结构解析 三、页面拆分 &#xff08;一&#xff09;页面拆分 1.侧边栏页面&#xff08;固定&#xff09;--Aside.vue 2.顶部页面&#xff08;固定&#xff09;--Header.vue 3.主体页面&#xff08;不固定的&#xff09;--示例用UserView…

PYTHON用户流失数据挖掘:建立逻辑回归、XGBOOST、随机森林、决策树、支持向量机、朴素贝叶斯和KMEANS聚类用户画像...

原文链接&#xff1a;http://tecdat.cn/?p24346 在今天产品高度同质化的品牌营销阶段&#xff0c;企业与企业之间的竞争集中地体现在对客户的争夺上&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 “用户就是上帝”促使众多的企业不惜代价去争夺尽可能多的客…

C#,《小白学程序》第七课:列表(List)应用之一“编制高铁车次信息表”

1 文本格式 /// <summary> /// 车站信息类 class /// </summary> public class Station { /// <summary> /// 编号 /// </summary> public int Id { get; set; } 0; /// <summary> /// 车站名 /// </summary>…

bug复刻,解决方案---在改变div层级关系时,导致传参失败

问题描述&#xff1a; 在优化页面时&#xff0c;为了实现网页顶部遮挡效果&#xff08;内容滚动&#xff0c;顶部导航栏不随着一起滚动&#xff0c;并且覆盖&#xff09;&#xff0c;做法是将内容都放在一个div里面&#xff0c;为这个新的div设置样式&#xff0c;margin-top w…

查漏补缺 - JS三 WebAPI

目录 BOMhistory DOM操作DOM1&#xff0c;dom.children 和 dom.childNodes 区别2&#xff0c;dom.remove()3&#xff0c;其他常用 API DOM 属性1&#xff0c;标准属性2&#xff0c;自定义属性 DOM 内容DOM样式DOM事件 JavaScript 包括 EcmaScript 和 WebAPI EcmaScript 包括 语…

25 Linux可视化-Webmin和bt运维工具

25 Linux可视化-Webmin和bt运维工具 文章目录 25 Linux可视化-Webmin和bt运维工具25.1 Web运行环境简介25.2 Webmin的安装及使用25.2.1 安装webmin25.2.2 Webmin使用演示 25.3 bt(宝塔)的安装及使用25.3.1 安装宝塔25.3.2 宝塔Web登录Linux服务器25.3.3 找回宝塔登录密码 学习视…

客路旅行(KLOOK)面试(部分)(未完全解析)

一面 用过Chatgpt的哪个版本&#xff0c;了解Chatgpt版本之间的差异吗 什么是优雅部署&#xff1f;newBing: 服务启动时&#xff0c;检查依赖的组件或容器是否就绪&#xff0c;如果不就绪&#xff0c;等待或重试&#xff0c;直到就绪后再注册到服务中心&#xff0c;对外提供服…

Java之API详解之BigDecimal类的详细解析

7 BigDecimal类 7.1 引入 首先我们来分析一下如下程序的执行结果&#xff1a; public class BigDecimalDemo01 {public static void main(String[] args) {System.out.println(0.09 0.01);}} 这段代码比较简单&#xff0c;就是计算0.09和0.01之和&#xff0c;并且将其结果…

Mapbox-gl 关闭所有Popup,以及关闭按钮出现黑色边框bug

1.官方示例 var popup new mapboxgl.Popup().addTo(map);popup.remove(); 很明显&#xff0c;需要记录popup对象&#xff0c;管理起来比较麻烦。 2.本人采用div的方式关闭所有的popup&#xff0c;在map对象上新增加方法 map.closePopupmapView.popupClear function(){$(&q…

ceph架构及 IO流程

CEPH是由多个节点构成的集群&#xff0c;它具有良好的可扩展性和可靠性。节点之间相互通信以达到&#xff1a; 存储和检索数据 数据复制 监控集群的健康状况 保证数据的完整性 检测故障并恢复 基本架构如下图&#xff1a; 分布式对象存储系统RADOS是CEPH最为关键的技术&a…

WebRTC音视频通话-WebRTC推拉流过程中日志log输出

WebRTC音视频通话-WebRTC推拉流过程中日志log输出 之前实现iOS端调用ossrs服务实现推拉流流程。 推流&#xff1a;https://blog.csdn.net/gloryFlow/article/details/132262724 拉流&#xff1a;https://blog.csdn.net/gloryFlow/article/details/132417602 在推拉流过程中的…

第 3 章 栈和队列(用递归函数求解迷宫问题(求出所有解))

1. 背景说明&#xff1a; 若迷宫 maze 中存在从入口 start 到出口 end 的通道&#xff0c;则求出所有合理解并求出最优解 迷宫示意图&#xff1a; 输入文本&#xff1a; 10 10181 3 1 7 2 3 2 7 3 5 3 6 4 2 4 3 4 4 5 4 6 2 6 6 7 2 7 3 7 4 7 6 7 7 8 11 18 8 2. 示例代码…

【计算机组成 课程笔记】3.1 算数运算和逻辑运算

课程链接&#xff1a; 计算机组成_北京大学_中国大学MOOC(慕课) 3 - 1 - 301-算术运算和逻辑运算&#xff08;13-7--&#xff09;_哔哩哔哩_bilibili 计算机的核心功能就是运算&#xff0c;运算的基本类型包括算数运算和逻辑运算。想要了解计算机是如何实现运算的&#xff0c;我…