Java-IO模型分析

news2024/12/23 23:32:32

BIO(同步阻塞)

利用网络连接传输数据为例:

服务端单线程

服务端只有一个主线程处理客户端的连接和读写处理,此时如果有第二个客户端欲连接并发送消息服务端是接收不到的。
因为读写和等待accept连接都是阻塞的
在这里插入图片描述

sever端代码:

		ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
            System.out.println("等待连接。。");
            //阻塞方法
            Socket clientSocket = serverSocket.accept();
            System.out.println("有客户端连接了。。");
            handler(clientSocket);
        }

client端代码:

		Socket socket = new Socket("127.0.0.1", 9000);
        //向服务端发送数据
        socket.getOutputStream().write("HelloServer".getBytes());
        socket.getOutputStream().flush();
        System.out.println("向服务端发送数据结束");
        byte[] bytes = new byte[1024];
        //接收服务端回传的数据
        socket.getInputStream().read(bytes);
        System.out.println("接收到服务端的数据:" + new String(bytes));
        socket.close();

服务端多线程

服务端使用主线程接受客户端的accept连接并建立连接关系,利用其他线程处理读写操作,实现连接和处理任务分离。使得主线程可以在while中不断处理客户端的连接请求,而利用多个子线程处理多个客户端的读写处理。
在这里插入图片描述

改写服务端代码:

		ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
            System.out.println("等待连接。。");
            //阻塞方法
            Socket clientSocket = serverSocket.accept();
            System.out.println("有客户端连接了。。");
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

缺点:高并发场景下每一个客户端都对应生成一个线程一对一处理,并且对于只连接没有读写操作的客户端会持续阻塞线程资源,比如C10K问题,太耗费资源

NIO(非阻塞)

没有selector的笨蛋式轮询server

服务端的channel等待客户端的accept非阻塞,读read也是非阻塞。采用两个while循环单线程不断地去看看有没有客户端连接请求,将这些socketChannel加入一个集合中,不断地从该集合遍历socketChannel是否有读写操作,read也是非阻塞。所以没有读的需求就继续遍历下一个socketChannel。如果中途有新的client加入或者有read需求,那就下一次轮询的时候会处理。

// 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();

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

        // 创建NIO ServerSocketChannel,与BIO的serverSocket类似
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        System.out.println("服务启动成功");

        while (true) {
            // 非阻塞模式accept方法不会阻塞,否则会阻塞
            // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
            SocketChannel socketChannel = serverSocket.accept();
            if (socketChannel != null) { // 如果有客户端进行连接
                System.out.println("连接成功");
                // 设置SocketChannel为非阻塞
                socketChannel.configureBlocking(false);
                // 保存客户端连接在List中
                channelList.add(socketChannel);
            }
            // 遍历连接进行数据读取
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                // 非阻塞模式read方法不会阻塞,否则会阻塞
                int len = sc.read(byteBuffer);
                // 如果有数据,把数据打印出来
                if (len > 0) {
                    System.out.println("接收到消息:" + new String(byteBuffer.array()));
                } else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }

缺点:此时是对所有的client无差别进行轮询,只要是连接了的client,就会对其进行内层while遍历看是否能read。和BIO的多线程问题有点相似之处。
就是对那些不经常发消息的客户端 给了不应该有的平等的对待~hhh。想想100万个client里其实99万都是僵尸,只有1万会说话,那我的服务端不应该每次轮询都要轮询这100万个client!
正确的操作应该是实现事件驱动的轮询处理,对那些不会动的僵尸们,采用冷漠忽视的态度!
那应该如何做呢?
NIO其实已经实现了!那就是IO多路复用器,也就是selector

事件驱动的server

由于有了selector,我们只需要将serverSocket注册到selector上面,对于server Socket身上的收发事件(包括了accept),selector就能监听到。
server端创建serverSocket代码和上面的笨蛋式代码一致:

		// 创建NIO ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);

但是紧接着创建一个selector,并注册server Socket,并关注其身上的accept事件。一旦channel注册成功,会返回一个绑定的key(下面代码中的selectionKey)并将其存进selector中的selectedKeys集合中

		// 打开Selector处理Channel,即创建epoll
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
        SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);

同样是通过while不断地使用selector监听各个channel,selector.select会阻塞!直到有事件触发才会继续往下走

		while (true) {
            // 阻塞等待需要处理的事件发生
            selector.select();

           

当有一个client试图连接server时,server的serverSocketChannel会触发事件响应,此时selector.select会监听到事件响应,程序继续进行,从selector的selectorKeys中遍历key:

		while (true) {
            // 阻塞等待需要处理的事件发生
            selector.select();

            // 获取selector中注册的全部事件的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            // 遍历SelectionKey对事件进行处理
            while (iterator.hasNext()) {
				巴拉巴拉。。。。。
            }

此时触发的是accept类型事件,进入该处理分支,并通过key得到绑定的channel,即socketServerChannel,并accept客户端的连接,建立起该客户端的socketChannel,并将其注册到selector关注读事件:

		while (iterator.hasNext()) {
              SelectionKey key = iterator.next();
              // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
              if (key.isAcceptable()) {
                  ServerSocketChannel server = (ServerSocketChannel) key.channel();
                  SocketChannel socketChannel = server.accept();
                  socketChannel.configureBlocking(false);
                  // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                  SelectionKey selKey = socketChannel.register(selector, SelectionKey.OP_READ);
                  System.out.println("客户端连接成功");

然后程序遍历结束,回到最外层while,再次阻塞在selector.select,等待下一次连接或者读写事件。此时连接的client发送消息给server,打破阻塞,进入内层selectedKeys遍历,这次走的是read事件响应,从socketChannel读出消息数据。

			// 遍历SelectionKey对事件进行处理
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                    SelectionKey selKey = socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {  // 如果是OP_READ事件,则进行读取和打印
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(128);
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据,把数据打印出来
                    if (len > 0) {
                        System.out.println("接收到消息:" + new String(byteBuffer.array()));
                    } else if (len == -1) { // 如果客户端断开连接,关闭Socket
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                }
                //从事件集合里删除本次处理的key,防止下次select重复处理
                iterator.remove();
            }

selector继续阻塞直到下一次事件触发…
流程如下:
在这里插入图片描述

底层实现原理和源码

JDK1.4之前都是通过Linux内核函数select或者poll去轮询所有channel查看哪些有事件,JDK1.4之后引入selector,底层实现采用了Linux的epoll函数,实现了将有事件的channel主动放入就绪事件列表。
selector底层使用epoll_create函数创建了一个epoll对象:

// 打开Selector处理Channel,即创建epoll
Selector selector = Selector.open();
JNIEXPORT jint JNICALL
Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this)
{
    /*
     * epoll_create expects a size as a hint to the kernel about how to
     * dimension internal structures. We can't predict the size in advance.
     */
    int epfd = epoll_create(256);
    if (epfd < 0) {
       JNU_ThrowIOExceptionWithLastError(env, "epoll_create failed");
    }
    return epfd;
}

对应的将channel注册到selector中底层是将channel的文件描述符添加进epoll的一个包装数组pollWrapper(底层并没有真正和epoll fd绑定):

SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
    protected void implRegister(SelectionKeyImpl ski) {
        if (closed)
            throw new ClosedSelectorException();
        SelChImpl ch = ski.channel;
        int fd = Integer.valueOf(ch.getFDVal());
        fdToKey.put(fd, ski);
        pollWrapper.add(fd);
        keys.add(ski);
    }

然后selector.select对应底层源码为:

    protected int doSelect(long timeout) throws IOException {
        if (closed)
            throw new ClosedSelectorException();
        processDeregisterQueue();
        try {
            begin();
            pollWrapper.poll(timeout);
        } finally {
            end();
        }
        processDeregisterQueue();
        int numKeysUpdated = updateSelectedKeys();
        if (pollWrapper.interrupted()) {
            // Clear the wakeup pipe
            pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
            synchronized (interruptLock) {
                pollWrapper.clearInterrupted();
                IOUtil.drain(fd0);
                interruptTriggered = false;
            }
        }
        return numKeysUpdated;
    }

其中pollWrapper包装数组会进行轮询poll:

    int poll(long timeout) throws IOException {
        updateRegistrations();
        updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
        for (int i=0; i<updated; i++) {
            if (getDescriptor(i) == incomingInterruptFD) {
                interruptedIndex = i;
                interrupted = true;
                break;
            }
        }
        return updated;
    }

其中updateRegistrations函数会使用epoll_ctl对每一个channel fd绑定给epoll fd并监听事件:

epollCtl(epfd, opcode, fd, events);

如果有事件响应,其实是操作系统的硬中断感知并把channel fd放进ready list。
上述的epollWait底层是epoll_wait函数,会从ready list(操作系统维护)里面去看有没有就绪事件如果有就放入selectedKeys去。

总结:
epoll_create创建epoll对象,epoll_ctl实现真正注册,epoll_wait实现监听并将硬中断事件的channel fd放入ready list

对比select,poll和epoll
在这里插入图片描述

redis线程模型

基于epoll的NIO线程模型实现。

netty

简化NIO,进一步对其封装为异步非阻塞。不在AIO上封装是因为Linux底层还是用epoll模型实现AIO但是异步没有优化好。

AIO(异步)

NIO2.0(对NIO进行封装,不需要轮询ready list处理事件,而是响应式编程采用回调函数直接主动处理),NIO的select,accept和read等等都是主线程自己做的,AIO不是,AIO的accept和read等都是采用了回调函数,并且是不同的线程处理

		final AsynchronousServerSocketChannel serverChannel =
                AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));

        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
                try {
                    System.out.println("2--"+Thread.currentThread().getName());
                    // 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
                    serverChannel.accept(attachment, this);
                    System.out.println(socketChannel.getRemoteAddress());
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            System.out.println("3--"+Thread.currentThread().getName());
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, result));
                            socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
                        }

                        @Override
                        public void failed(Throwable exc, ByteBuffer buffer) {
                            exc.printStackTrace();
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
            }
        });

        System.out.println("1--"+Thread.currentThread().getName());
        Thread.sleep(Integer.MAX_VALUE);

有三个线程异步非阻塞处理。

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

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

相关文章

CSS:服务器字体 与 响应式布局(用法 + 例子 + 效果)

文章目录 服务器字体定义 服务器字体使用例子 响应式布局设备类型设备特性例子 服务器字体 解决字体不一致而产生的。 首先&#xff0c;在网上把字体下载好。 定义 服务器字体 font-face{font-family:字体名称;src:url(字体资源路径); }使用 在需要使用的选择器里加上 font…

抖音关键词搜索小程序排名怎么做

抖音关键词搜索小程序排名怎么做 1 分钟教你制作一个抖音小程序。 抖音小程序就是我的视频&#xff0c;左下方这个蓝色的链接&#xff0c;点进去就是抖音小程序。 如果你有了这个小程序&#xff0c;发布视频的时候可以挂载这个小程序&#xff0c;直播的时候也可以挂载这个小…

camera hal|如何学习一个新平台

全网最具价值的Android Camera开发学习系列资料~ 作者:8年Android Camera开发,从Camera app一直做到Hal和驱动~ 欢迎订阅,相信能扩展你的知识面,提升个人能力~ 我自己目前从事的是android camera hal 的工作,工作上接触到的芯片平台要么是高通的,要么是mtk的。 其实…

034_小驰私房菜_[问题复盘] Qcom平台,某些三方相机拍照旋转90度

全网最具价值的Android Camera开发学习系列资料~ 作者:8年Android Camera开发,从Camera app一直做到Hal和驱动~ 欢迎订阅,相信能扩展你的知识面,提升个人能力~ 【一、问题】 某些三方相机,预览正常,拍照旋转90度 【二、问题排查】 1 ) HAL这边Jpeg编码数据在哪个地方…

FPGA----Vivado SDK创建并使用静态链接库(C/C++代码移植)

1、在进行SoC开发时&#xff0c;PS端的C/C代码可能涉及到核心算法需要移植操作&#xff0c;为此&#xff0c;本文讲述了如何将C/C代码打包为.a文件供程序调用 2、文章以我的程序为例&#xff0c;逐步讲述代码生成静态链接库并调用的方法。 下面是我程序的目录结构&#xff0c…

MySQL入门学习教程(三)

上一章给大家说的是数据库的视图&#xff0c;存储过程等等操作&#xff0c;这章主要讲索引&#xff0c;以及索引注意事项&#xff0c;如果想看前面的文章&#xff0c;url如下&#xff1a; MYSQL入门全套(第一部)MYSQL入门全套(第二部) 索引简介 索引是对数据库表中一个或多个…

python中的运算符号含义,python基本运算符的操作

本篇文章给大家谈谈python的运算符号有哪些类型&#xff0c;以及python各运算符号的功能说明&#xff0c;希望对各位有所帮助&#xff0c;不要忘了收藏本站喔。 1.算数运算符&#xff08;最常见的&#xff09; 标准算数运算符&#xff08;加减乘除&#xff09; 取余运算…

贪心 二分查找和二分答案 递推与递归

贪心 知识点 局部最优解->整体最优解 贪心算法理论基础&#xff01;_哔哩哔哩_bilibili 选择的贪心策略必须具备无后效性&#xff0c;即某个状态以前的过程不会影响以后的状态&#xff0c;只与当前状态有关。 证明贪心策略的有效性 反证法 数学归纳法 例题 376.摆…

不同版本Idea部署Maven和Tomcat教学

目录 一、2019版Idea 1.1. Maven配置 1.2. Tomcat配置 二、2023版Idea 2.1 Maven配置 2.2. Tomcat配置 一、2019版Idea 1.1. Maven配置 在这篇 http://t.csdn.cn/oetKq 我已经详细讲述了Maven的下载安装及配置&#xff0c;本篇就直接开始实操 : 1. 首先进入设置搜索Mave…

6939. 数组中的最大数对和

题目描述&#xff1a; 给你一个下标从 0 开始的整数数组 nums 。请你从 nums 中找出和 最大 的一对数&#xff0c;且这两个数数位上最大的数字相等。 返回最大和&#xff0c;如果不存在满足题意的数字对&#xff0c;返回 -1 。 示例&#xff1a; 解题思路&#xff1a; 使用数组…

PyQt5同一界面实现不同窗口跳转

目录 一、目的 二、QStacked Widget 二、QTabWidget 三、实战演示 1、在Qt Designer中编辑界面文件 2、编写逻辑文件用于显示 四、QStackedWidget、QTabWidget可以相互嵌套使用,效果奇佳 五、附录——生成新的窗口进行跳转,跳转的同时关闭另外一个界面 1、第一个跳转…

Java之多态

多态 多态的实现条件重写重写的定义重写的例子方法重写的条件 多态思想动态绑定与静态绑定 作者简介&#xff1a; zoro-1&#xff0c;目前大一&#xff0c;正在学习Java&#xff0c;数据结构等 作者主页&#xff1a;zoro-1的主页 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&…

7-7 整数152的各位数字

本题要求编写程序&#xff0c;输出整数152的个位数字、十位数字和百位数字的值。 输入格式&#xff1a; 本题无输入。 输出格式&#xff1a; 按照以下格式输出&#xff1a; 152 个位数字 十位数字*10 百位数字*100代码长度限制 16 KB 时间限制 400 ms 内存限制 64…

Mac 卸载appium

安装了最新版的appium 2.0.1,使用中各种问题&#xff0c;卡顿....,最终决定回退的。记录下卸载的过程 1.打开终端应用程序 2.卸载全局安装的 Appium 运行以下命令以卸载全局安装的 Appium&#xff1a; npm uninstall -g appium 出现报错&#xff1a;Error: EACCES: permiss…

命题与命题联结词

一、命题 什么是命题&#xff1f; 具有确切真值的陈述句称为命题。疑问句、感叹句、祈使句都不是命题。 例如&#xff1a; 是命题 1加1等于3雪是黑色的 不是命题有&#xff1a; 太好啦&#xff01;X0X>1 原子命题&#xff08;简单命题&#xff09;——不能分解的…

1572. 矩阵对角线元素的和

题目描述&#xff1a; 给你一个正方形矩阵 mat&#xff0c;请你返回矩阵对角线元素的和。 请你返回在矩阵主对角线上的元素和副对角线上且不在主对角线上元素的和。 示例&#xff1a; 解题思路&#xff1a; 同时求对角线和副对角线上元素的和再减去重合的元素 相关代码&#xf…

期权定价模型系列【5】—ETF期权数据

1.前言 对期权定价模型进行研究时&#xff0c;往往需要匹配的实际数据&#xff0c;国内上市时间超过两年、主流的ETF期权包括华夏上证50ETF期权、沪深300ETF期权等&#xff0c;其对应的标的资产分别为华夏上证50ETF、华泰柏瑞沪深300ETF、嘉实沪深300ETF。 2.上证50ETF期权合约…

刨根问底,不再纠结Linux 文件权限问题

Linux 与Windows的区别 与Windows 系统不一样&#xff0c;在Linux系统中&#xff0c;无论是系统内核还是应用程序&#xff0c;都是文件。正如此&#xff0c;当你学习Linux中遇到问题时&#xff0c;总能看到热心网友的解决方法&#xff1a; rm -rf * 一旦运行此命令&#x…

Docker的基本概念及镜像加速器的配置

1.Docker的概念 由于代码运行环境不同&#xff0c;代码运行会出现水土不服的情况。运用docker容器会把环境进行打包&#xff0c;避免水土不服。docker是一种容器技术&#xff0c;它解决软件跨环境迁移的问题。 2&#xff0c;安装Docker 3.Docker架构 4.Docker镜像加速器的配…

模拟实现消息队列(以 RabbitMQ 为蓝本)

目录 1. 需求分析1.1 介绍一些核心概念核心概念1核心概念2 1.2 消息队列服务器&#xff08;Broker Server&#xff09;要提供的核心 API1.3 交换机类型1.3.1 类型介绍1.3.2 转发规则&#xff1a; 1.4 持久化1.5 关于网络通信1.5.1 客户端与服务器提供的对应方法1.5.2 客户端额外…