图解IO多路复用的原理

IO多路复用我们经常可以听到的名词,那么什么是IO多路复用呢?下面我们用图解的方式来分析什么是IO多路复用。

1、文件描述符

在我们日常的开发中无论是磁盘、网络还是终端来的数据都可以认为是文件,如下图所示:

既然是这些数据可以当做文件,那么就可以使用一套通用的接口操作它,常用的如open()、read()、write()、close()等来操作,如下图所示:

我们的现在调用open()函数,它的返回的结果是什么呢?为什么它的结果可以让read()、write()函数来继续操作文件呢?

实际上操作系统的内核会创建各种各样的数据结构来管理管理磁盘文件,所以操作系统的内核利用这些数据结构可以知道读取哪个文件并且文件在磁盘哪个位置上,如下图所示:

由于内核可以很轻松的读取到磁盘上的文件,那么现在我们的应用如果可以和操作系统的内核关联上就可以实现应用读取磁盘的文件了。如下图所示:

其实调用open()函数就是返回一个数字,这个数字用于告诉内核接下来根据这个数字在内核数据结构上找磁盘文件的位置。

我们知道在一个数组中可以直接根据数组的下标定位数组上的数据,open()函数得到的返回结果就和我们的数组下标很类似。为了更加清晰的描述open()函数返回的结果,于是取名叫做文件描述符,即就是:

文件描述符 = open("文件的位置");

拿到这个文件描述符之后,我们传给read()、write()函数来操作

#读取文件 
read("文件描述符",.....); 
#写入文件 
wirte("文件描述符",.....);

2、IO多路复用

在我们Java网络编程中学过ServerSocket,通过ServerSocket类创建服务端监听,一旦有客户端和服务器成功之后,客户端就可以实现与服务端之间数据交互,如下:

public static void main(String[] args) throws IOException { 
        //创建服务端的监听 
        ServerSocket socket = new ServerSocket(9000); 
        for (;;){ 
            System.out.println("连接socket开始"); 
            //等待客户端的连接 
            Socket tempSocket = socket.accept(); 
            System.out.println("连接socket成功"); 
            //执行业务 
            System.out.println("准备读取数据"); 
            readData(tempSocket); 
            System.out.println("读取数据成功"); 
        } 
    }

当执行到Socket tempSocket = socket.accept();代码的时候,本地模拟一个客户端连接,如下所示:

此时一旦连接成功之后,我们观察到tempSocket对象中的信息如下:

这里有fd对象中就有文件描述符的信息,当拿到文件描述符fd后可以实现客户端和服务端之间的通信了,服务端通过读取客户端发来的请求后进行业务逻辑处理,如下是服务端处理业务的逻辑:

private static void readData(Socket tempSocket) throws IOException { 
        byte[] byteArray = new byte[1024]; 
        //读取客户端的数据 
        int readResult = tempSocket.getInputStream().read(byteArray); 
        if (readResult != -1) { 
            System.out.println("获取到的数据: "+ new String(byteArray, 0, readResult)); 
        } 
        System.out.println("数据读取结束"); 
    }

如果只处理一个客户端和服务端建立连接,那么还是非常的轻松的,如果现在有若干个客户端请求连接服务器(也就是有若干个文件描述符),如下图所示:

此时如果采用串行化处理,即就是Client1、Client2、Client3依次处理连接请求。但是这样处理存在问题,我们都知道像read()、sleep()、recv()等这些都是阻塞的调用,也就是如果客户端Client1没有获取到数据,那么read()函数就没有返回而一致阻塞中,这就导致了Client2、Client3就被卡住了,如下图所示:

有若干个文件描述符需要处理,如果像这样串行化操作,执行的效率太低了,有没有一种更科学的方式来处理这若干个文件描述符呢?其实是有的,我们可以将这些文件描述符一起发送给操作系统的内核,如下图:

发送文件描述符给内核之后还要告诉内核,每当这些文件描述符中有就绪的文件描述符的时候要及时通知客户端。如下所示代码:

#监听是否有可以读写的文件描述符 
fds = wait_files({fd1,fd2,fd3,fd4}); 
#如果有可以读写的文件描述符,就执行相关的业务操作 
for(fd : fds){ 
#读取数据 
  read(fd,buff); 
#执行业务逻辑 
  doSomething(buff); 
}

wait_files()是阻塞调用的函数,它可以返回已经就绪的文件描述符集合,如下图所示:

通过for循环对就绪文件列表中的文件描述符进行操作。在这个过程中,我们得到了一个效率更高的IO操作,我们称呼此方式为IO多路复用。

IO多路复用实质是把一堆需要处理的文件描述符发送给操作系统内核,当内核监测到这些文件描述符中有就绪的文件描述符之后,将这些就绪的文件描述符返回给客户端处理。

4