五大IO模型的理解及PHP简单实现

    技术2022-07-10  84

    文章目录

    五大 I/O 模型阻塞 I/O原理介绍代码实现执行结果及说明执行结果说明 非阻塞 I/O原理介绍代码实现执行结果及说明执行结果说明 I/O 多路复用原理介绍执行结果及说明执行结果说明 信号驱动 I/O代码实现执行结果及说明执行结果说明 异步 I/O代码实现执行结果及说明执行结果说明 总结

    五大 I/O 模型

    对于一次 IO 操作,数据会先被拷贝进操作系统内核空间的缓冲区中,然后才会从内核缓冲区中拷贝到用户空间。所以说,当一个 IO 操作发生时,它会经历两个阶段:

    等待数据准备。将数据从内核拷贝到用户空间

    根据这两个阶段中用户进程等待的不同方式,可以分为以下五种 I/O 模型:

    阻塞 I/O非阻塞 I/OI/O 多路复用信号驱动 I/O异步 I/O

    阻塞 I/O

    原理介绍

    当用户进程调用 recvfrom 系统函数,内核就开始了 IO 的第一个阶段:数据准备(对于网络 IO 来说,很多时候在一开始还没有数据的存在,内核需要在磁盘中读取数据并存放在内核中的缓冲区中)。数据准备完成后,内核会把在内核缓冲区中准备好的数据复制到用户空间的内存中。而在数据准备到数据复制到用户空间的整个过程中,用户进程一直是阻塞的。直到将数据复制到用户空间完成,内核返回成功,用户进程解除阻塞状态从而继续运行。

    代码实现

    blocking_nonblocking_io_server.php 服务端代码

    <?php class Worker { public $onConnect; public $onMessage; public function __construct($addr) { $this->socket = stream_socket_server($addr); echo "{$addr}服务已经启动\r\n"; } public function start() { $this->accept(); } public function accept() { while (true) { $client = stream_socket_accept($this->socket); if (is_callable($this->onConnect)) { ($this->onConnect)($this, $client); } $data = fread($client, 65535); if (is_callable($this->onMessage)) { ($this->onMessage)($this, $client, $data); } fclose($client); } } public function sentMessage($client, $data) { $response = "HTTP/1.1 200 OK\r\n"; $response .= "Content-Type: text/html;charset=UTF-8\r\n"; $response .= "Connection: keep-alive\r\n"; $response .= "Content-length: " . strlen($data) . "\r\n\r\n"; $response .= $data; fwrite($client, $response); } } // 调用 $address = 'tcp://0.0.0.0:9000'; $server = new Worker($address); $server->onConnect = function ($serv, $client) { // 模拟内核准备数据,延迟5秒钟 sleep(5); $serv->sentMessage($client, 'hello client' . "\n"); }; $server->onMessage = function ($serv, $client, $data) { echo "收到來自客戶端的消息\n"; }; $server->start();

    blocking_client.php 阻塞模型的客户端代码

    <?php $address = 'tcp://127.0.0.1:9000'; // 创建客户端套接字,默认为阻塞模型 $client = stream_socket_client($address); $time = time(); fwrite($client, 'hello server'); echo fread($client, 65536); echo "其他业务\r\n"; $m = time() - $time; echo "执行时间" . $m . "秒钟\n";

    执行结果及说明

    执行结果

    运行服务端 blocking_nonblocking_io_server.php

    运行客户端代码 blocking_client.php

    说明

    在服务端 onConnect 回调方法中增加 5 秒延迟,来模拟 io 操作中的内核准备数据的时间,之后再向客户端发送数据。因此在阻塞模式下的客户端的脚本中,发现执行到输出“其他业务”的逻辑执行时间为 5 秒。可以模拟出用户进程在等待内核准备数据的阻塞状态。

    非阻塞 I/O

    原理介绍

    套接字被设置为 nonblock(非阻塞)时,当所请求的 IO 操作无法完成时,而是返回一个错误码 ,用户进程会调用 recvfrom 函数不断对内核空间轮询,直到内核准备好数据为止,返回成功。此过程用户进程不会阻塞。

    代码实现

    blocking_nonblocking_io_server.php 服务端代码沿用。

    nonblocking_client.php 非阻塞模型的客户端代码

    <?php $address = 'tcp://127.0.0.1:9000'; $client = stream_socket_client($address); //设置套接字为非阻塞模型 stream_set_blocking($client, 0); $time = time(); echo fread($client, 65536); echo "其他业务\r\n"; $m = time() - $time; echo "执行时间" . $m . "秒钟\n"; //不断轮询,有内容则打印 while (!feof($client)) { echo fread($client, 65536); }

    执行结果及说明

    执行结果

    运行服务端 blocking_nonblocking_io_server.php

    运行客户端代码 nonblocking_client.php

    说明

    在服务端 onConnect 回调方法中增加 5 秒延迟,来模拟 io 操作中的内核准备数据的延迟。在非阻塞的模型下的客户端脚本的执行中,发现输出“其他业务”的逻辑的执行时间为 0 秒,也就是没有受到服务端延迟的影响。在最后用 while 模拟对内核的轮询,五秒后打印出服务端发给客户端的“hello client”

    I/O 多路复用

    原理介绍

    在 I/O 多路复用模型中,会用到 select 或 poll 函数, 这两个函数也会使进程阻塞,但是和阻塞 I/O 所不同的是,这两个函数可以同时阻塞多个 I/O 操作,而且可以同时对多个读操作、多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。

    从流程上来看,使用 select 函数进行 I/O 请求和同步阻塞模型没有太大的区别,甚至还多了添加监视 socket,以及调用 select 函数的额外操作,效率更差。但是,使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 I/O 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 I/O 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

    multiplexing_io_server.php 服务端代码

    <?php class Worker { public $socket; public $onConnect; public $onMessage; public $sockets; public function __construct($addr) { $this->socket = stream_socket_server($addr); stream_set_blocking($this->socket, 0); $this->sockets[(int) $this->socket] = $this->socket; echo $addr . "服务启动\r\n"; } public function accept() { while (true) { $read = $this->sockets; stream_select($read, $write, $e, 1); foreach ($read as $scoket) { if ($scoket == $this->socket) { $this->createSocket($scoket); } else { $this->sendMessage($scoket); } } } } public function sendMessage($socket) { $data = fread($socket, 65535); if ($data === '' || $data === false) { fclose($socket); unset($this->sockets[(int) $socket]); return; } if (is_callable($this->onMessage)) { ($this->onMessage)($this, $data); } } public function createSocket() { $client = stream_socket_accept($this->socket); if (is_callable($this->onConnect)) { ($this->onConnect)($this, $client); } $this->sockets[(int) $client] = $client; } public function sentMessage($client, $data = '') { $response = "HTTP/1.1 200 OK\r\n"; $response .= "Content-Type: text/html;charset=UTF-8\r\n"; $response .= "Connection: keep-alive\r\n"; $response .= "Content-length: " . strlen($data) . "\r\n\r\n"; $response .= $data; fwrite($client, $response); } public function start() { $this->accept(); } } $address = 'tcp://0.0.0.0:9000'; $server = new Worker($address); $server->onConnect = function ($serv, $client) { $serv->sentMessage($client, 'hello client' . "\n"); }; $server->onMessage = function ($client, $data) { echo "收到來自客戶端的消息\n"; echo $data; }; $server->start();

    执行结果及说明

    执行结果

    运行服务端 multiplexing_io_server.php

    通过 curl 请求 127.0.0.1:9000

    说明

    服务端中主要用到了 stream_select 方法,通过 select 方法查找出可以操作的文件描述符,对其进行读写操作。

    信号驱动 I/O

    我们定义一个处理函数并安装一个信号,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据,当用户进程接收到内核准备好数据的信号 SIGIO 后,内核把数据从内核空间复制到用户空间时,用户进程是阻塞的,阻塞直到复制数据完成。

    代码实现

    signal_server.php

    <?php class Worker { public $onConnect; public $onMessage; public $onClose; public function __construct($addr) { $this->socket = stream_socket_server($addr); echo "{$addr}服务已经启动\n"; } public function start() { $this->accept(); } public function accept() { while (true) { $client = stream_socket_accept($this->socket); //安装一个信号 pcntl_signal(SIGIO, $this->sigHandler($client)); //根据进程设置信号 posix_kill(posix_getpid(), SIGIO); // 分发信号 pcntl_signal_dispatch(); } } public function sigHandler($client) { return function () use ($client) { if (!empty($client)) { if (is_callable($this->onConnect)) { ($this->onConnect)($this, $client); } if (is_callable($this->onMessage)) { ($this->onMessage)($client); } fclose($client); } }; } public function sentMessage($client, $data) { $response = "HTTP/1.1 200 OK\r\n"; $response .= "Content-Type: text/html;charset=UTF-8\r\n"; $response .= "Connection: keep-alive\r\n"; $response .= "Content-length: " . strlen($data) . "\r\n\r\n"; $response .= $data; fwrite($client, $response); } } $address = 'tcp://0.0.0.0:9000'; $server = new Worker($address); $server->onConnect = function ($serv, $client) { $serv->sentMessage($client, 'hello client' . "\n"); }; $server->onMessage = function ($client) { echo "收到來自客戶端的消息\n"; $data = fread($client, 65535); echo $data; }; $server->start();

    执行结果及说明

    执行结果

    运行服务端 signal_server.php

    通过 curl 请求 127.0.0.1:9000

    说明

    服务端中主要用到了 php 原生的三个信号操作的方法:pcntl_signal 为指定的信号安装一个新的处理函数;posix_kill 为将信号与进程 id 进行绑定;pcntl_signal_dispatch 调用安装过的处理函数。通过这几个方法,把与 server 端连接的 socket 进行相应 IO 操作。

    异步 I/O

    用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从 kernel 的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。然后,kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel 会给用户进程发送一个 signal,告诉它 read 操作完成了。与信号 IO 的区别是:信号 IO 是当内核已经准备好数据之后,会通知用户进程数据已经准备好,可以去读取数据。而异步 IO 是内核准备好数据并且复制完成后自动返回结果。

    代码实现

    async_server.php

    <?php use Swoole\Event; class Worker { public $socket; public $onConnect; public $onMessage; public $onClose; public function __construct($addr) { $this->socket = stream_socket_server($addr); echo $addr . "服务启动\r\n"; } public function accept() { Event::add($this->socket, $this->createSocket()); } public function createSocket() { return function ($socket) { $client = stream_socket_accept($socket); if (is_callable($this->onConnect)) { ($this->onConnect)($this, $client); } Event::add($client, $this->sendClient()); }; } public function sendClient() { return function ($socket) { $meg = fread($socket, 655535); if (empty($meg)) { if (feof($socket) || !is_resource($socket)) { swoole_event_del($socket); fclose($socket); } } if (!empty($meg) && is_callable($this->onMessage)) { ($this->onMessage)($socket); } }; } public function sentMessage($client, $data = '') { $response = "HTTP/1.1 200 OK\r\n"; $response .= "Content-Type: text/html;charset=UTF-8\r\n"; $response .= "Connection: keep-alive\r\n"; $response .= "Content-length: " . strlen($data) . "\r\n\r\n"; $response .= $data; fwrite($client, $response); } public function start() { $this->accept(); } } $address = 'tcp://0.0.0.0:9000'; $server = new Worker($address); $server->onConnect = function ($serv, $client) { $serv->sentMessage($client, 'hello client' . "\n"); }; $server->onMessage = function ($client) { echo "收到來自客戶端的消息\n"; $data = fread($client, 65535); echo $data; }; $server->start();

    执行结果及说明

    执行结果

    运行服务端 async_server.php

    通过 curl 请求 127.0.0.1:9000

    说明

    async_server 的代码中,使用到了 swoole 的 Event 类,可以给 socket 的读写注册回调函数,当 socket 可操作时触发回调函数,通过异步回调的方式,进行 socket 的 IO 操作。

    代码也可以通过 php 的 Event 扩展类来实现,但是 php 的 Event 扩展类在同一个文件的事件嵌套中会存在一些问题,实现起来稍微复杂一些,因此为了简单展示 Event 的功能,而 swoole 中的 Event 与 php 的类似,调用方式比 php 自己的 Event 还简单些,因此采用了 swoole 的 Event 类实现。

    总结

    最近刚刚开始研究学习网络编程方面的东西,因此借着这片文章中把自己对五大 IO 模型的理解梳理了一遍。由于刚开始接触,对好多东西不是特别理解,或者理解的不是很到位。如果文中哪些东西阐述不正确,请大家多多指正。 希望通过后续的学习加深对网络编程方面的理解吧。

    疑惑

    php 的 Event 扩展或者 swoole 的 Event 类从字面上描述我理解应该为异步操作,是对 epoll 的一个封装。但是从 IO 模型来说,epoll 又是实现 IO 多路复用的一种手段,而 IO 多路复用又是同步 IO 。

    那么疑惑点就来了:

    Event 到底是异步还是同步呢?我查过许多文章都没有明确的解释,还有说是一种“伪异步”的说法。也许是我接触不深,对这些概念不够理解吧。

    希望高手指点迷津~

    Processed: 0.010, SQL: 9