I/O事件驱动模式

·  阅读 150

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

I/O事件驱动模式

知识索引

  • 网络服务发展历程
  • 传统I/O模型
  • 事件驱动模式

1 网络I/O发展历程

在一般的网络服务当中大都具备一些相同的处理流程:

1:读取请求数据
2:对请求数据进行解码
3:对数据进行处理;
4:对回复数据进行编码
5:发送回复
复制代码

当然在实际应用中每一步的运行效率都是不同的,例如其中可能涉及到xml解析文件传输web页面的加载、计算服务等不同功能。

在网络I/O发展历程中共分为以下几类,阻塞I/O(BIO即blocking IO)非阻塞I/O(NIO即nonblocking IO)多路复用I/O(IO multiplexing)信号驱动I/O(signal driven IO)异步I/O(asynchronous IO),下面我们逐一进行介绍:

阻塞I/O:顾名思义,是阻塞的,这个阻塞指的是在发生`I/O`是进程必须阻塞等待,比如客户端发送一份数据,这个数据在操作系统还没有收集完成时,用户进程必须阻塞等待。直到`OS`(操作系统)将数据准备完成,`OS`会将数据拷贝到用户内存,然后返回结果,用户进程解除阻塞状态。举例来说就是原本烧两壶水需要两个人,两个人坐在水壶旁边,一边打盹一边等着听水壶响的声音(阻塞等待),这样等到水开了就能立刻使用,这两个人在这期间不能做任何事情。
非阻塞I/O:在该场景下,客户端发送一份数据,这个数据在操作系统还没有收集完成时,用户进程会不断向`OS`进行询问(`system call`),如果数据没有准备好,`OS`会返回`未完成`状态,用户进程判断结果为`未完成`时继续轮询直到`OS`将数据准备完全。也就是非阻塞`I/O`是通过用户进程的轮询机制实现的,还是以烧开水为例,一个人一直全神贯注的盯着两个水壶(轮询),这样等到水开了就能立刻通知其他人过来。
信号驱动I/O:当用户线程发起一个IO请求操作,会给socket注册一个信号函数,然后用户线程会继续执行,当内核就绪时就会发一个信号给用户线程,用户线程收到信号后,便会在信号函数中调用IO读写操作进行实际的读写操作,此方式并不常用,不进行详细介绍。
多路复用I/O:也是JDK NIO实现的基础,NIO是基于Reactor模型实现的,Reactor模型就是一个基于多路复用I/O的事件驱动模型,它的本质是通过一种机制(系统内核缓冲`I/O`数据),当用户进程调用了`select`,那么整个进程会被阻塞,而同时,OS会“监视”所有`select`负责的`socket`,当任何一个`socket`中的数据准备好了,`select`就会返回。这个时候用户进程再调用`read`操作,将数据从`OS`拷贝到用户进程。还是烧开水的案例,现在水壶变得智能了,水开的时候会给指定的人发短信,这样只需要一个人就可以监控多壶水,不只是烧开水这个事件,其他的蒸米饭的事件也可以这样实现,这就是`I/O多路复用`技术所做的事,让线程资源可以从阻塞中解放出来进行重复利用,大大提高了`cpu`的效率。
异步I/O:也是JDK AIO实现的基础,AIO是基于Proactor模型实现的,Proactor模型就一个是基于异步I/O的事件驱动模型,用户进程发起read操作之后,不会阻塞。而另一方面,OS收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,OS会等待数据准备完成,然后将数据拷贝到用户内存的缓冲区,当这一切都完成之后,OS会给用户进程发送一个信号,告诉它read操作完成了,其本质和JDK`的Feature是一致的。还是以烧开水为例,现在开水不用我们自己烧了,而是社区送温暖,需要水的时候,社区会烧好送过来,社区的角色就类似于操作系统,它会帮助用户进程完成基本的读写操作,从而完全解放用户进程,也就是例子中烧开水的那个人。
复制代码

2 传统I/O模型(bio)

传统I/O模型也称为bio,即阻塞I/O会为每一个连接的处理开启一个新的线程,大致的示意图如下:

2.1 bio的代码实现

Server

/**
 * Copyright (c) 2022 itmentu.com, All rights reserved.
 *
 * @Author: yang
 */
public class Server {
    public static void main(String[] args) {
        try {
            // 创建Socket服务端,监听2000端口
            ServerSocket ss = new ServerSocket(2000);
            while (true)
                // 创建一个线程监听该端口的事件
                new Thread(new ServerHandler(ss.accept())).start();
        } catch (IOException ex) {
            /* ... */ }
    }
}
复制代码

代码说明:

1:创建服务端Socket,监听2000端口
2:每当服务端接受到Socket,就创建一个线程进行处理
3:在接受到socket连接以前,ss.accept()一直处于阻塞状态
复制代码

ServerHandler

/**
 * Copyright (c) 2022 itmentu.com, All rights reserved.
 *
 * @Author: yang
 */
public class ServerHandler implements Runnable {
    final Socket socket;

    ServerHandler(Socket s) {
        socket = s;
    }

     public void run() {
        try {
            // 1.创建byte数组用来接收输入
            byte[] input = new byte[1024];
            // 2.从输入流中读入到input数组中
            while (socket.getInputStream().read(input)!=1){
                // 3.处理输入获得响应输出
                byte[] output = process(input);
                // 4.写入到响应流中
                socket.getOutputStream().write(output);
            }
        } catch (IOException ex) {
        }
    }

    private byte[] process(byte[] imput) {
        System.out.println("服务端线程"+Thread.currentThread().getId()+"接受请求数据:"+new String(imput));
        return "success".getBytes();
    }
}
复制代码

代码说明:

1:通过一个while循环不断读取socket的输入流中产生的数据
2:在读取到数据之前,socket.getInputStream().read是阻塞的
3:process方法将接受到的byte数组通过New String解码(默认解码"UTF-8“)后打印,并响应"success"(默认编码"UTF-8“)
复制代码

Client

/**
 * Copyright (c) 2022 itmentu.com, All rights reserved.
 *
 * @Author: yang
 */
public class Client {

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            // 1.创建socket连接
            try {
                Socket socket = new Socket("127.0.0.1", 2000);
                new ClientSocketHandler(socket).start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码

代码说明:

1:for循环创建两个客户端socket,通过ClientSocketHandler进行处理
2:ClientSocketHandler继承Thread,start()表示开启一个线程
复制代码

ClientSocketHandler

/**
 * Copyright (c) 2022 itmentu.com, All rights reserved.
 *
 * @Author: yang
 */
public class ClientSocketHandler extends Thread {

    Socket socket;

    public ClientSocketHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 2; i++) {
                // 1.发送请求
                String msg = "请求"+i;
                System.out.println("客户端发送请求:"+msg);
                socket.getOutputStream().write(msg.getBytes());
                socket.getOutputStream().flush();
                // 2.获取响应
                byte[] input = new byte[1024];
                socket.getInputStream().read(input);
                System.out.println(new String("客户端获得响应:"+new String(input)));
                // 3.间隔时间2s
                Thread.sleep(2000);
            }
        } catch (Exception e) {
        }
    }
}
复制代码

代码说明:

1:通过Socket每2s发送一次数据,共两次
复制代码

测试结果

服务端:

image-20220116143224970

客户端:

image-20220116143247073

测试结果说明

可以看到同一个客户端Socket发送的请求,由同一个服务端线程进行处理,服务端线程监听的Socket没有接收到数据的时候该线程处于阻塞状态

2.2 bio模式的缺点

线程是宝贵的资源,在bio模式中,每一个socket对使用一个线程进行处理,而Socket存在阻塞,这就导致cpu空转,线程资源被浪费,在上述案例中,服务端的线程阻塞表现在两方面:

  • 服务端ServerSocketaccept()方法接受到Socket连接以前一直处于阻塞状态
  • Socket.getInputStream().read在读取到数据之前一直处于阻塞状态

3 事件驱动模式

3.1 什么是事件驱动模式

基于事件驱动的架构设计通常比其他架构模型更加有效,因为可以节省一定的性能资源,事件驱动模式下通常不需要为每一个客户端建立一个线程,这意味这更少的线程开销,更少的上下文切换和更少的锁互斥,但任务的调度可能会慢一些,而且通常实现的复杂度也会增加,相关功能必须分解成简单的非阻塞操作,类似与GUI的事件驱动机制,当然也不可能把所有阻塞都消除掉,特别是GCpage faults(内存缺页中断)等。由于是基于事件驱动的,所以需要跟踪服务的相关状态(因为你需要知道什么时候事件会发生);下图是AWT中事件驱动设计的一个简单示意图,可以看到,在不同的架构设计中的基于事件驱动的IO操作使用的基本思路是一致的;

3.2 基于事件驱动的两种I/O模型

3.2.1 Reactor

反应器模型,是基于 I/O多路复用实现的一种事件驱动模型,中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上,一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。单线程的Reactor模型示意图如下:

img

在Reactor模式一章中我们将进行详细介绍

3.2.2 Proactor

相比于Reactor模式是基于I/O复用模型事件驱动模型,Proactor是基于异步I/O的事件驱动模型

Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。 可以看出和Reactor的区别:Reactor是在事件发生时就通知事先注册的事件(读写由处理函数完成);Proactor是在事件发生时进行异步I/O(读写由OS即操作系统完成),待I/O完成事件分离器才调度处理器来处理。因此,Reactor 可以理解为,来了事件操作系统通知应用进程,让应用进程来处理,而 Proactor可以理解为来了事件操作系统来处理,处理完再通知应用进程。这里的事件就是有新连接有数据可读有数据可写的这些 I/O 事件这里的处理包含从驱动读取到内核以及从内核读取到用户空间

jdkAIO就是Proactor模式的实现.

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改