请注意,从一个处于文件末尾的普通文件中读取 *不会* 阻塞。您将获得一个非阻塞的、零字节的读取操作。但是,如果输入是一个管道,并且没有更多数据可供读取,则 `stream_select` *将会* 阻塞。
(PHP 4 >= 4.3.0, PHP 5, PHP 7, PHP 8)
stream_select — 在给定的流数组上运行等效于 select() 系统调用的操作,并由秒和微秒指定的超时时间
&$read
,&$write
,&$except
,$seconds
,$microseconds
= null
stream_select() 函数接受流数组并等待它们的状态发生变化。它的操作等效于 socket_select() 函数,除了它作用于流。
read
列在 read
数组中的流将被监视,以查看是否有字符可供读取(更准确地说,是查看读取是否不会阻塞 - 特别是,流资源在文件末尾也处于就绪状态,在这种情况下,fread() 将返回一个零长度字符串)。
write
列在 write
数组中的流将被监视,以查看写入是否不会阻塞。
except
列在 except
数组中的流将被监视,以查看是否有到达的高优先级异常(“带外”)数据。
注意:
当 stream_select() 返回时,数组
read
、write
和except
将被修改以指示哪些流资源实际上改变了状态。数组的原始键将被保留。
seconds
seconds
和 microseconds
一起构成超时参数,seconds
指定秒数,而 microseconds
指定微秒数。timeout
是 stream_select() 在返回之前等待的最长时间的上限。如果 seconds
和 microseconds
都设置为 0
,则 stream_select() 不会等待数据 - 而是会立即返回,指示流的当前状态。
如果 seconds
为 null
,则 stream_select() 可以无限期阻塞,仅在监视的流之一上发生事件时(或如果信号中断系统调用)才会返回。
使用 0
的超时值允许您即时轮询流的状态,但是,在循环中使用 0
的超时值不是一个好主意,因为它会导致您的脚本消耗过多的 CPU 时间。
最好指定几秒钟的超时值,尽管如果您需要同时检查和运行其他代码,则使用至少 200000
微秒的超时值将有助于减少脚本的 CPU 使用率。
请记住,超时值是将经过的最大时间;stream_select() 将在请求的流准备好使用时立即返回。
microseconds
参见 seconds
描述。
如果成功,stream_select() 将返回修改后的数组中包含的流资源的数量,如果超时在发生任何有趣的事情之前过期,则该数量可能为零。如果发生错误,则返回 false
并引发警告(如果系统调用被传入信号中断,则可能发生这种情况)。
版本 | 描述 |
---|---|
8.1.0 |
microseconds 现在可以为 null。 |
示例 #1 stream_select() 示例
此示例检查是否已为 $stream1
或 $stream2
上的读取接收数据。由于超时值为 0
,因此它将立即返回
<?php
/* 准备读取数组 */
$read = array($stream1, $stream2);
$write = NULL;
$except = NULL;
if (false === ($num_changed_streams = stream_select($read, $write, $except, 0))) {
/* 错误处理 */
} elseif ($num_changed_streams > 0) {
/* 至少在一个流上发生了有趣的事情 */
}
?>
注意:
由于当前 Zend 引擎中的限制,无法将像
null
这样的常量修饰符直接作为参数传递给期望此参数按引用传递的函数。相反,请使用临时变量或最左侧成员为临时变量的表达式<?php
$e = NULL;
stream_select($r, $w, $e, 0);
?>
注意:
在检查错误时务必使用
===
运算符。由于 stream_select() 可能返回 0,因此与==
的比较将评估为true
<?php
$e = NULL;
if (false === stream_select($r, $w, $e, 0)) {
echo "stream_select() 失败\n";
}
?>
注意:
如果您在数组中返回的流上进行读/写操作,请注意它们不一定读取/写入您请求的全部数据。请做好即使只能读/写一个字节的准备。
注意:
某些流(如
zlib
)无法被此函数选择。
注意:Windows 兼容性
在 Windows 下,对 proc_open() 返回的文件描述符使用 stream_select() 将失败并返回
false
。来自控制台的
STDIN
在任何输入事件可用时都会更改状态,但从流中读取仍可能阻塞。
请注意,从一个处于文件末尾的普通文件中读取 *不会* 阻塞。您将获得一个非阻塞的、零字节的读取操作。但是,如果输入是一个管道,并且没有更多数据可供读取,则 `stream_select` *将会* 阻塞。
维护与多个客户端的连接可能很棘手,PHP 脚本是单线程进程,因此如果您希望同时执行多个操作(例如等待新连接和等待新数据),您将不得不使用某种多路复用。
<?php
$socket = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
stream_set_blocking($socket, 0);
$connections = [];
$read = [];
$write = null;
$except = null;
while (1) {
// 查找新的连接
if ($c = @stream_socket_accept($socket, empty($connections) ? -1 : 0, $peer)) {
echo $peer.' connected'.PHP_EOL;
fwrite($c, 'Hello '.$peer.PHP_EOL);
$connections[$peer] = $c;
}
// 等待任何流数据
$read = $connections;
if (stream_select($read, $write, $except, 5)) {
foreach ($read as $c) {
$peer = stream_socket_get_name($c, true);
if (feof($c)) {
echo 'Connection closed '.$peer.PHP_EOL;
fclose($c);
unset($connections[$peer]);
} else {
$contents = fread($c, 1024);
echo $peer.': '.trim($contents).PHP_EOL;
}
}
}
}
?>
如果您希望为循环中的 `stream_select` 设置一个绝对最大执行时间,则务必递减传递给 `stream_select` 的 `max_time` 值。
<?php
// 以毫秒为单位的最大执行时间
$maxtime = 200000;
// 循环开始的时间
$starttime = microtime(true);
// 原始套接字数组
$r = $orig_sockets;
// 计算超时时间的函数
function calctimeout($maxtime, $starttime)
{
return $maxtime - ((microtime(true) - $starttime) * 1000000);
}
while (stream_select($r, $w = null, $e = null, 0, calctimeout($maxtime, $starttime)) !== 0)
{
// 循环遍历显示活动的套接字
foreach ($r as $socket) {
// $socket 发生通信
}
// stream_select 会修改 $r 的内容
// 在循环中,我们应该用原始数组替换它
$r = $orig_sockets;
}
?>
请注意,您应该更改下面的 `calctimeout` 函数,将结果除以 1.000.000,否则您将等待两年而不是一分钟才能使套接字超时...
<?php
// 计算超时时间的函数
function calctimeout($maxtime, $starttime)
{
return ($maxtime - ((microtime(true) - $starttime) * 1000000))/1000000;
}
?>
当 `stream_select()` 失败时,您**不应**使用作为参数传递给它的数组(即 `read`、`write`、`except`)的结果。虽然这样做不会触发未定义的行为,但您依赖于未指定的行为,根据定义,这种行为没有保证。
在撰写本文时,PHP 7.2 解释器在 `stream_select()` 失败时不会修改数组(请参见 https://github.com/php/php-src/blob/php-7.2.14/ext/standard/streamsfuncs.c#L842 周围的代码),因此不注意上述建议的 PHP 程序可能会欺骗自己,使其认为这些流的状态是正确的。
(希望将来有一天可以在主文档中添加此警告)
如果您在使用 `stream_select` 的非阻塞套接字时遇到无法解释的问题,请使用以下命令禁用缓冲区:
stream_set_read_buffer($socket, 0);
stream_set_write_buffer($socket, 0);
由于某种原因,当写入(总计)约 256k 时,套接字在读取时开始返回 FALSE,但始终出现在 `stream_select` 数组中。这解决了该问题。(对我们而言。)
请注意,在返回时,“read” 的键将为基于零的、按顺序编号的,仅根据准备好读取数据的流进行编号。换句话说,如果您想知道“read” 中放置的原始流中的哪一个已准备好,则没有直接的方法来了解这一点。
如果您想知道哪个原始流是哪个,您可以使用“==”,或者可能设置一个反向映射数组,其中流是键,而原始“read”数组的键是数据。
如果您尝试将 `stream_select()` 与 `fread()` 一起使用,则可能会遇到一些错误的组合(https://bugs.php.net/bug.php?id=52602 和 https://bugs.php.net/bug.php?id=51056)。从 PHP 5.5.10 开始,`fread()` 和 `stream_select()` 不能可靠地一起使用。
如果您需要 `stream_select()` 并且不需要加密连接(例如 TLS),请使用 `stream_socket_recvfrom()` 而不是 `fread()`。
我找不到在 PHP 中使用阻塞函数可靠地处理加密连接的方法;非阻塞可能是唯一的方法。
如果您将 `stream_select()` 与阻塞流一起使用,则您的操作是错误的!
仅仅因为此函数在一个或多个数组中返回了一些内容,**并不意味着**将来的读取或写入操作不会阻塞。
上面这句话是关于流操作你将阅读到的最重要的句子。在使用阻塞流时使用 `stream_select()` 是一个非常常见的初学者错误,并且在追踪此函数和类似 `select()` 系统函数的使用时会导致严重的头痛。PHP(实际上是底层的操作系统)应该验证提供的流集不是阻塞的,并在任何套接字设置为阻塞时抛出错误/异常,以便人们被迫修复他们的代码。`stream_select()` 的文档充其量是误导性的。
如果你想要一个非阻塞流,那么将流设置为非阻塞。否则,就忍受阻塞流。毕竟,阻塞的全部意义就在于——无限期地阻塞,直到操作完成。`select()` 仅适用于非阻塞流。任何其他用法都会导致非常难以追踪的错误。
几年前,在遇到我提到的这些错误后,我收到了上述教训。我修复了我的代码,现在在其他地方遇到类似问题时也会纠正类似的错误。为非阻塞流编写代码比尝试为阻塞流使用 `select()` 函数编写 hack 并最终导致应用程序错误要简单得多。
`stream_select()` 看起来像是 `POSIX select(2)` 的一个简单的包装器。
但请注意:虽然 `select(2)` 允许你传递没有文件描述符并将其用作“可移植的亚秒级睡眠”,但如果所有数组为空或为 null,PHP 会抱怨“Warning: stream_select(): No stream arrays were passed in ****”,并且它不会睡眠,它会立即返回。因此……如果你的文件描述符数量不是静态的,你必须自己处理这种特殊情况。