IO模型

2021-05-23 21:50|来源: 网路

Unix与java的I/O模型

Unix 下共有五种 I/O 模型:阻塞 I/O、非阻塞 I/O、I/O 多路复用(select、poll、epoll)信号驱动 I/O(SIGIO)和异步 I/O(Posix.1的aio_系列函数),而java除了其中的信号驱动式之外,其他均有支持;

输入操作的两个阶段

理解I/O模型,首先要理解一个输入操作所必须包含的2个阶段:

  1. 等待数据准备好;
  2. 从内核向进程复制数据;

对于套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待的分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

IO模型详解

1. 阻塞式IO模型

进程调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲中或者发生错误才返回。这就是阻塞式IO模型的微观图示。

针对阻塞IO模型的传统服务设计则如上图,服务器对每个client连接都会启动一个专门的线程去维护,服务器中的逻辑Handler需要在各自的线程中执行,这种模型对线程的需求较多,面对高并发的场景,会造成CPU资源浪费;原来的tomcat就是这种模式,只不过现在也支持NIO了。

常见写法(服务端):
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @program test
 * @description: bio
 * @author: cys
 * @create: 2020/06/30 16:20
 */
public class BIOServer {

    //线程池机制
    //1. 创建一个线程池
    //2. 如果有客户端连接了,创建一个线程与之通讯(单独写一个方法)
    public static void main(String[] args) throws IOException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器启动了");
        while (true) {
            //监听,等待客户端连接
            final Socket socket = serverSocket.accept();
            System.out.println("有客户端连接");
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    handler(socket);
                }
            });
        }
    }

    private static void handler(Socket socket) {
        byte[] bytes = new byte[1024];
        try (InputStream inputStream = socket.getInputStream();) {
            int read = 0;
            while ((read = inputStream.read(bytes)) != -1) {
                System.out.println(new String(bytes, 0, read));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

2.非阻塞IO

img

  • 前三次调用时,数据未准备好,内核就会立即返回一个错误码(EWOULDBLOCK),此时请求线程不会被阻塞;
  • 第四次时,数据准备好,它被复制到应用进程缓冲区,recvfrom成功返回。
  • 由此可见:请求线程将不断请求内核,查看数据是否准备好。这种轮询操作,一样会消耗大量的CPU资源,所以在java的实现中,会采用同时支持I/O复用的方式支持非阻塞。

3.I/O复用模型

img

如图I/O复用模型将阻塞点由真正的I/O系统调用转移到了对select、poll或者epoll系统函数的调用上。单纯看这个微观图,有可能还会觉得与阻塞I/O区别不大,甚至于我们多增加了I/O调用还增加了性能损耗。其实不然,使用select以后最大的优势是用户可以在一个线程内同时处理多个连接的I/O请求

java NIO实现一个聊天的功能(服务端)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

/**
 * @program test
 * @description:
 * @author: cys
 * @create: 2020/07/02 10:37
 */
public class GroupChatServer {

    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int port = 6667;

    public GroupChatServer() {
        try {
            selector = Selector.open();
            listenChannel = ServerSocketChannel.open();
            listenChannel.socket().bind(new InetSocketAddress(port));
            listenChannel.configureBlocking(false);
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public void listen() {
        try {
            while (true) {
                if (selector.select(2000) > 0) {
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        if (key.isAcceptable()) {//连接事件
                            SocketChannel socketChannel = listenChannel.accept();
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector, SelectionKey.OP_READ);
                        }
                        if (key.isReadable()) {//读取事件,即通道可读了
                            read(key);
                        }
                        iterator.remove();
                    }
                } else {
                    System.out.println("等待。。。。");
                }
            }
        } catch (IOException e) {

        } finally {

        }
    }

    //读取客户端消息
    private void read(SelectionKey selectionKey) {

        SocketChannel socketChannel = null;
        try {
            //定义一个SocketChannel
            socketChannel = (SocketChannel) selectionKey.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = socketChannel.read(buffer);
            if (count > 0) {//读取到了数据
                String str = new String(buffer.array());
                System.out.println("from 客户端" + str);
                //向其他的客户端转发消息
                sendInfoToOtherClients(str, socketChannel);
            }

        } catch (IOException e) {
            try {
                System.out.println(socketChannel.getRemoteAddress() + "离线了");
                selectionKey.cancel();
                socketChannel.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        } finally {
        }
    }

    private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
        System.out.println("服务器转发消息中");
        //遍历所有注册到Selector上的channel,并排除self
        for (SelectionKey key : selector.keys()) {
            Channel targetChannel = key.channel();
            if (targetChannel instanceof SocketChannel && targetChannel != self) {
                SocketChannel desc = (SocketChannel) targetChannel;
                //将msg存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                desc.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        new GroupChatServer().listen();
    }
}

信号驱动式I/O模型

img

如图,我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动式I/O(signal-driven I/O).

5.异步I/O模型

img

它由POSIX规范定义,工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。与信号驱动式I/O不同的是:信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。

区别

img

如图,前四种模型都属于同步I/O模型,因为其中真正的 I/O操作将阻塞进程。而异步I/O是完全不会阻塞请求I/O的。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 IO 复用模型模式为主。

参考: ​ https://cloud.tencent.com/developer/article/1677776

相关问答

更多
  • 使用NIO方式处理并发性能好的前提是 每个请求耗时不能高,但是这样对现有的request response处理要有额外处理。   如果不能达到这个要求,用 NIO反而会是整体性能下降。   现在一般的处理流程: 请求进来,查数据库,执行业务逻辑,渲染,然后返回。整体耗时很长。
  • blocking I/O nonblocking I/O I/O multiplexing (select and poll) signal driven I/O (SIGIO) asynchronous I/O (the POSIX aio_functions)—————异步IO模型最大的特点是 完成后发回通知。 阻塞与否,取决于实现IO交换的方式。 异步阻塞是基于select,select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄. 异步非阻塞直接在完成后通 ...
  • 回复 totopper 非阻塞IO一般放在while循环中,过一段时间查看一下数据是否准备好;IO多路复用中一般使用的是阻塞的IO
  • 什么是 IO 模型[2022-05-09]

    服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型。 (2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。 (3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型 ...
  • linux socket中也有select 除此之外还有 poll epoll的轮询效率比select高多了
  • 上面的服务器端代码片段不会在回调中显示对Agent.publishCreate的调用,而是显示publishUpdate 。 根据我的理解,publishCreate仅在使用蓝图时自动触发 - 但不适用于上面的Agent.create之类的程序调用。 因此,将其从publishUpdate更改为publishCreate可能会在您的上下文中修复它。 The server side code snippet above doesn't show a call to Agent.publishCreate in ...
  • 解决方案是使用“embedsMany”关系类型。 "relations": { "addresses": { "model": "address", "type": "embedsMany", "options": { "autoId": false, "validate": true } } The solution was to use the "embedsMany" relation t ...
  • 所以我想出了问题。 当您进行更新( CRUD )时,执行此操作的套接字不会收到更新。 在解决这个问题之前,我花了几个小时。 因此,如果CRUD操作成功,那么我所做的就是对数据采取行动,就像我将在带有on的模型on列出的那样 So i figured out the problem. When you make an update (CRUD), the socket performing such operation does not receive an update. I spent hours bef ...
  • Angular不知道soketio事件,因此当它发生时不会运行摘要周期。 您需要手动触发此过程: app.controller('ChatController', function ($scope) { this.username = ''; this.setUsername = function (username) { this.username = username; }; socket.on('success', (function (contro ...
  • 我不确定qm69的解决方案是否是未来与平均值兼容的最佳解决方案。 在mean.io文档http://learn.mean.io/中,它指出开发人员不应该更改任何核心包,包括用户包。 mean.io模式是将任何和所有扩展作为自定义包实现。 并使用$ viewPathProvider.override方法覆盖默认视图。 其次,用户包基本上是安全/认证功能,而不是定期接收更新的配置文件实施。 改变这种情况很可能会破坏未来的修复并引发安全漏洞。 我的建议是使用means软件包系统实现一个配置文件,并为User服务添 ...