参考文献
5种IO模型:https://blog.mailjob.net/posts/3565199751.html
github代码下载:https://github.com/mailjobblog/dev_php_io/tree/master/test/multiplexing
Epoll 多路复用是如何转起来的:https://mp.weixin.qq.com/s/Py2TE9CdQ92fGLpg-SEj_g
IO多路复用原理
简单解读
在 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 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
IO多路复用详解(可略过直接看代码)
非阻塞情况下无可用数据时,应用程序每次轮询内核看数据是否准备好了也耗费CPU,能否不让它轮询,当内核缓冲区数据准备好了,以事件通知当机制告知应用进程数据准备好了呢?应用进程在没有收到数据准备好的事件通知信号时可以忙写其他的工作。此时IO多路复用就派上用场了。
IO多路复用中文比较让人头大,IO多路复用的原文叫 I/O multiplexing,这里的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流. 发明它的目的是尽量多的提高服务器的吞吐能力。实现一个线程监控多个IO请求,哪个IO有请求就把数据从内核拷贝到进程缓冲区,拷贝期间是阻塞的!现在已经可以通过采用mmap地址映射的方法,达到内存共享效果,避免真复制,提高效率。
像select、poll、epoll 都是I/O多路复用的具体的实现。
select
select是第一版IO复用,提出后暴漏了很多问题。
- select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
- select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但不会告诉是那个sock上有数据,只能自己遍历查找。
- select 只能监视1024个链接。
- select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现这个sock不用,要收回,这个select 不支持的。
poll
poll 修复了 select 的很多问题。
- poll 去掉了1024个链接的限制。
- poll 从设计上来说不再修改传入数组。
但是poll仍然不是线程安全的, 这就意味着不管服务器有多强悍,你也只能在一个线程里面处理一组 I/O 流。你当然可以拿多进程来配合了,不过然后你就有了多进程的各种问题。
epoll
epoll 可以说是 I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
- epoll 现在是线程安全的。
- epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
- epoll 内核态管理了各种IO文件描述符, 以前用户态发送所有文件描述符到内核态,然后内核态负责筛选返回可用数组,现在epoll模式下所有文件描述符在内核态有存,查询时不用传文件描述符进去了。
三者对比
横轴 Dead connections 是链接数的意思,叫这个名字只是它的测试工具叫deadcon。纵轴是每秒处理请求的数量,可看到epoll每秒处理请求的数量基本不会随着链接变多而下降的。poll 和/dev/poll 就很惨了。但 epoll 有个致命的缺点是只有linux支持。
比如平常Nginx为何可以支持4W的QPS是因为它会使用目标平台上面最高效的I/O多路复用模型。
原生PHP代码演示
服务端中主要用到了 stream_select
方法,通过 select 方法查找出可以操作的文件描述符,对其进行读写操作。
server.php
<?php
require_once __DIR__."/../../vendor/autoload.php";
use DevPhpIO\Multiplexing\Worker;
$server = new Worker('0.0.0.0', 9500);
$server->on('connect', function($server, $client){
dd($client, "客户端成功建立连接");
});
$server->on('receive', function(Worker $server, $client, $data){
dd($data, "处理client的数据");
$server->send($client, "hello i’m is server");
// $server->close($client);
});
$server->on('close', function($server, $client){
dd($client, "连接断开");
});
$server->start();
client.php
<?php
require_once __DIR__."/../../vendor/autoload.php";
// 连接服务端
$fp = stream_socket_client("tcp://127.0.0.1:9500");
fwrite($fp, "hello world");
dd(fread($fp, 65535));
// 这里阻塞 10s 是为了便于演示
sleep(10);
fwrite($fp, "第二个消息");
dd(fread($fp, 65535));
演示截图