各位观众老爷,早上好!我是老码农,今天跟大家聊聊PHP里那些“见多识广”的 I/O 多路复用技术,什么Stream Select
、Poll
、Epoll
,听起来是不是像武林秘籍?别怕,咱们把它拆解了,保证你听完能用它们在PHP的世界里“降妖伏魔”。
开场白:为啥需要“多路复用”?
想象一下,你是一个餐厅服务员,只有一个服务员,但是有很多顾客同时点餐。传统的做法是:
- 跑到A顾客那里问:“点啥?”
- 跑到B顾客那里问:“点啥?”
- 跑到C顾客那里问:“点啥?”
- ……
如果顾客很多,你就会累死。而且,如果A顾客点了菜之后要等很久才能做好,你还得一直守着他,浪费时间。
这种模式,就类似于传统的阻塞I/O。 PHP脚本就像这个服务员,每个连接就像一个顾客。如果PHP要处理多个连接,就得一个一个地处理,如果某个连接阻塞了(比如等待网络数据),整个PHP进程就会卡住,其他连接就得等着。
所以,我们需要更高效的服务员,他能同时观察多个顾客,谁准备好了就先服务谁。这就是 I/O 多路复用的思想。
主角登场:Stream Select、Poll、Epoll
这三位就是PHP I/O 多路复用的三大法宝,它们的作用都是监控多个文件描述符(file descriptor,简称fd),看看哪些fd准备好了(可读、可写、出错)。
-
文件描述符(fd)是什么?
简单来说,fd就是操作系统给每个打开的文件(包括socket)分配的一个数字编号。你可以把它想象成餐厅里每个顾客的桌号。PHP通过fd来操作这些文件或socket。
-
三者的区别:
特性 Stream Select Poll Epoll 实现方式 轮询 轮询 事件通知 文件描述符限制 有上限(通常1024) 理论上无上限 理论上无上限 性能 较低 中等 较高 适用场景 连接数较少 连接数中等 连接数非常多 操作系统支持 所有 POSIX标准 Linux特有
1. Stream Select:最原始的“巡逻员”
stream_select()
函数是PHP里最基础的 I/O 多路复用函数,它会监控你指定的socket集合,看看哪些socket可以读、可以写、或者出错。
函数原型:
int stream_select ( array &$read , array &$write , array &$except , int $seconds [, int $microseconds = 0 ] )
$read
:需要监控的可读socket数组,函数返回时,只有可读的socket会被保留在数组中。$write
:需要监控的可写socket数组,函数返回时,只有可写的socket会被保留在数组中。$except
:需要监控的异常socket数组,函数返回时,只有异常的socket会被保留在数组中。$seconds
:超时时间(秒)。$microseconds
:超时时间(微秒)。
代码示例:
<?php
// 创建两个socket
$socket1 = stream_socket_server("tcp://127.0.0.1:8080", $errno, $errstr);
$socket2 = stream_socket_server("tcp://127.0.0.1:8081", $errno, $errstr);
if (!$socket1 || !$socket2) {
die("Failed to create sockets: $errstr ($errno)n");
}
stream_set_blocking($socket1, 0); // 设置非阻塞模式
stream_set_blocking($socket2, 0); // 设置非阻塞模式
$read = [$socket1, $socket2];
$write = [];
$except = [];
while (true) {
// 复制数组,因为 stream_select 会修改原数组
$read_copy = $read;
$write_copy = $write;
$except_copy = $except;
// 监控socket
$num_changed_streams = stream_select($read_copy, $write_copy, $except_copy, null); // 第四个参数 null 表示一直阻塞直到有事件发生
if ($num_changed_streams === false) {
echo "stream_select() failedn";
break;
} elseif ($num_changed_streams > 0) {
// 遍历可读socket
foreach ($read_copy as $socket) {
if ($socket == $socket1) {
// socket1 有新的连接
$new_conn = stream_socket_accept($socket1);
if ($new_conn) {
echo "New connection on socket1n";
$read[] = $new_conn; // 将新连接添加到监控列表中
stream_set_blocking($new_conn, 0);
}
} elseif ($socket == $socket2) {
// socket2 有新的连接
$new_conn = stream_socket_accept($socket2);
if ($new_conn) {
echo "New connection on socket2n";
$read[] = $new_conn; // 将新连接添加到监控列表中
stream_set_blocking($new_conn, 0);
}
} else {
// 其他socket 有数据可读
$data = fread($socket, 1024);
if ($data === false || strlen($data) === 0) {
// 连接关闭或出错
echo "Connection closed or errorn";
fclose($socket);
$key = array_search($socket, $read);
if ($key !== false) {
unset($read[$key]); // 从监控列表中移除
}
} else {
echo "Received data: " . $data . "n";
fwrite($socket, "Echo: " . $data); // Echo back
}
}
}
// 处理可写socket(这里省略,因为示例主要关注可读)
// 处理异常socket(这里省略)
}
}
fclose($socket1);
fclose($socket2);
?>
Stream Select 的缺点:
- 轮询:
stream_select()
需要轮询整个socket数组,效率较低。想象一下,你每次都要把所有顾客都看一遍,才能知道谁准备好了,效率肯定不高。 - 文件描述符限制: 在一些老版本的操作系统上,
stream_select()
有文件描述符数量的限制(通常是1024)。
2. Poll:升级版的“巡逻员”
Poll
也是一种 I/O 多路复用技术,它比 stream_select
稍先进一些。理论上,Poll
没有文件描述符数量的限制。
PHP里没有直接对应的poll()
函数,通常需要借助扩展,或者使用stream_select()
模拟。 由于直接使用扩展较少,此处重点讲解使用stream_select()
模拟poll()
的行为。
模拟poll()
的代码示例:
<?php
// 创建两个socket
$socket1 = stream_socket_server("tcp://127.0.0.1:8080", $errno, $errstr);
$socket2 = stream_socket_server("tcp://127.0.0.1:8081", $errno, $errstr);
if (!$socket1 || !$socket2) {
die("Failed to create sockets: $errstr ($errno)n");
}
stream_set_blocking($socket1, 0); // 设置非阻塞模式
stream_set_blocking($socket2, 0); // 设置非阻塞模式
$sockets = [$socket1, $socket2]; // 所有的socket
while (true) {
$read = $sockets; // 所有socket都参与读事件的监控
$write = [];
$except = [];
// 监控socket
$num_changed_streams = stream_select($read, $write, $except, null);
if ($num_changed_streams === false) {
echo "stream_select() failedn";
break;
} elseif ($num_changed_streams > 0) {
// 遍历可读socket
foreach ($read as $socket) {
if ($socket == $socket1 || $socket == $socket2) {
// socket1 or socket2 有新的连接
$new_conn = stream_socket_accept($socket);
if ($new_conn) {
echo "New connection on " . ($socket == $socket1 ? "socket1" : "socket2") . "n";
$sockets[] = $new_conn; // 将新连接添加到监控列表中
stream_set_blocking($new_conn, 0);
}
} else {
// 其他socket 有数据可读
$data = fread($socket, 1024);
if ($data === false || strlen($data) === 0) {
// 连接关闭或出错
echo "Connection closed or errorn";
fclose($socket);
$key = array_search($socket, $sockets);
if ($key !== false) {
unset($sockets[$key]); // 从监控列表中移除
}
} else {
echo "Received data: " . $data . "n";
fwrite($socket, "Echo: " . $data); // Echo back
}
}
}
// 处理可写socket(这里省略)
// 处理异常socket(这里省略)
}
}
fclose($socket1);
fclose($socket2);
?>
Poll 的优点 (相对 Stream Select):
- 理论上没有文件描述符数量的限制。
- 在一些系统上,
Poll
可能比stream_select
更高效,因为它使用更底层的数据结构。
Poll 的缺点:
- 仍然需要轮询整个socket数组,效率相对较低。
3. Epoll:最先进的“情报员”
Epoll
是 Linux 系统特有的一种 I/O 多路复用技术,它采用了事件通知机制,只有当socket真正有事件发生时,才会通知PHP进程。
Epoll 的优势:
- 高效: 不需要轮询,只有活跃的socket才会被处理。想象一下,情报员只在你需要的顾客准备好了的时候才告诉你,效率当然高。
- 支持大量连接: 可以轻松处理数万甚至数十万个并发连接。
PHP里使用Epoll需要借助扩展,比如event
扩展。
使用event
扩展的代码示例:
<?php
// 创建EventBase
$base = new EventBase();
// 创建两个socket
$socket1 = stream_socket_server("tcp://127.0.0.1:8080", $errno, $errstr);
$socket2 = stream_socket_server("tcp://127.0.0.1:8081", $errno, $errstr);
if (!$socket1 || !$socket2) {
die("Failed to create sockets: $errstr ($errno)n");
}
stream_set_blocking($socket1, 0);
stream_set_blocking($socket2, 0);
// 可读事件回调函数
$read_callback = function ($socket, $event, $arg) use ($base, &$read_event) {
if ($socket == $arg['server_socket']) {
// 有新的连接
$new_conn = stream_socket_accept($socket);
if ($new_conn) {
echo "New connection on " . ($socket == $socket1 ? "socket1" : "socket2") . "n";
stream_set_blocking($new_conn, 0);
//为新连接创建新的可读事件
$read_event_client = new Event($base, $new_conn, Event::READ | Event::PERSIST, $read_callback, ['server_socket' => $arg['server_socket']]);
$read_event_client->add();
}
} else {
// 有数据可读
$data = fread($socket, 1024);
if ($data === false || strlen($data) === 0) {
// 连接关闭或出错
echo "Connection closed or errorn";
fclose($socket);
$event->del(); //删除事件
} else {
echo "Received data: " . $data . "n";
fwrite($socket, "Echo: " . $data); // Echo back
}
}
};
// 为两个server socket分别注册READ事件
$read_event1 = new Event($base, $socket1, Event::READ | Event::PERSIST, $read_callback, ['server_socket' => $socket1]);
$read_event1->add();
$read_event2 = new Event($base, $socket2, Event::READ | Event::PERSIST, $read_callback, ['server_socket' => $socket2]);
$read_event2->add();
// 循环处理事件
$base->loop();
fclose($socket1);
fclose($socket2);
?>
代码解释:
- 创建EventBase:
EventBase
是event
扩展的核心,它负责管理所有的事件。 - 创建Event:
Event
对象代表一个事件,我们需要指定监控的socket、事件类型(Event::READ
、Event::WRITE
等)、以及回调函数。 - 添加事件:
$event->add()
将事件添加到EventBase
中,开始监控。 - 循环处理事件:
$base->loop()
会一直循环,直到没有事件需要处理。当有事件发生时,event
扩展会自动调用相应的回调函数。 Event::PERSIST
表示持久事件,触发一次后不会自动删除,可以一直监听。
Epoll 的缺点:
- 只能在 Linux 系统上使用。
- 使用起来比
stream_select
更复杂。
总结:选哪个好?
- 连接数较少:
stream_select
够用,简单易懂。 - 连接数中等: 尝试使用其他扩展提供的
poll
或libevent
扩展,或者使用stream_select
模拟。 - 连接数非常多: 必须使用
Epoll
(或者其他类似的事件通知机制),例如libevent
扩展。
实际应用场景:
- 高性能服务器: 比如 Node.js、Nginx 等,它们都使用了 I/O 多路复用技术来处理大量的并发连接。
- 聊天室: 可以使用 I/O 多路复用技术来实时接收和发送消息。
- 游戏服务器: 可以使用 I/O 多路复用技术来处理多个玩家的请求。
结尾:掌握“多路复用”,走向PHP高并发之路!
I/O 多路复用是构建高性能PHP应用的关键技术之一。理解了它的原理,你就能更好地利用PHP处理高并发,构建更强大的应用。希望今天的讲解对大家有所帮助!下次有机会再跟大家聊聊其他PHP底层技术。 散会!