请注意,从已处于文件结尾的常规文件中读取 *不会* 阻塞。您将获得非阻塞的零字节读取。但是,如果输入是管道并且没有更多数据可以读取,则 *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
将被修改以指示哪些流资源实际改变了状态。array 的原始键将被保留。
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() failed\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;
}
?>
当 stream_select() 失败时,您 *不应该* 使用传递给它的参数作为参数的数组(即读取、写入、异常)的结果。虽然这样做不会触发未定义的行为,但您依赖于未指定的行为,而这种行为默认情况下是不能保证的。
在撰写本文时,PHP 7.2 解释器在 stream_select() 失败时不会修改数组(参见 https://github.com/php/php-src/blob/php-7.2.14/ext/standard/streamsfuncs.c#L842 附近的代码),因此不遵守上述建议的 PHP 程序可能会欺骗自己关于这些流的状态。
(希望有一天可以在主文档中添加此警告)
请注意,您应该更改下面的 calctimeout 函数,将结果除以 1.000.000,否则您将等待两年而不是一分钟来让套接字超时...
<?php
// 计算超时时间的函数
function calctimeout($maxtime, $starttime)
{
return ($maxtime - ((microtime(true) - $starttime) * 1000000))/1000000;
}
?>
如果您在使用 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()` 函数为阻塞流编写补丁并最终导致应用程序错误更简单。
`stream_select()` 看起来像是 POSIX `select(2)` 的一个简单包装器。
但请注意:虽然 `select(2)` 允许您传递没有文件描述符并将其用作“可移植亚秒睡眠”,但如果所有数组为空或为 null,PHP 会抱怨“警告:stream_select(): 未传递任何流数组 ****”,并且它不会睡眠,而是会立即返回。所以……如果您拥有的文件描述符数量不是静态的,您必须自己处理特殊情况。
http://bugs.php.net/bug.php?id=42682
此函数在某些带有旧版本的系统下不会返回更改的流数量,而是返回 '0'。小心。
@mbaynton at gmail dot com
一个方便的语法技巧
<?php
$r = Array($stream1, $stream2);
stream_select($r, $w = null, $x = null, 1337);
?>
我在文档的其他地方看到有人推荐它,用于阐明魔法参数,以便维护人员不必去检查函数本身,但这也能解决这里的问题。
请注意,与之前的发布者所说相反,无法将流资源用作数组的键。相反,如果您想知道正在处理哪个套接字,请考虑使用类似于以下代码的代码
<?php
$sockets = array("sock_1" => $sock1, "sock_2" => $sock2, "sock_3" => $sock_3);
$read = $write = $error = $sockets;
$num = stream_select($read, $write, $error, 10);
if ($n > 0) {
foreach ($read as $r) {
$key = array_search($r, $sockets);
// $key 将是 "sock_1"、"sock_2"、"sock_3" 等。
}
}
?>
希望这对某些人有所帮助!
简单的 `stream_select` 包装器.. 返回数组中的第一个流,并将参数 2 设置为键(以便易于识别接收到的数据)
<?php
function select($array, &$vkey, $timeout=0){
$select = array();
$null = NULL;
foreach($array as $key => $sock){
$x = count($select);
$select[$x] = $sock;
$keys[$x] = $key;
}
if(stream_select($select, $null, $null, $timeout)){
foreach($keys as $key){
if($array[$key] == $select[0]){
$vkey = $key;
return($select[0]);
}
}
}
}
$streams = array("foo" => $stream_one, "bar" => $stream_two); // 创建两个(已存在的)流的数组。
if($new = select($streams, $key, 60)){ // 将 $new 设置为下一个获取新数据的资源,将 $key 设置为 "foo" 或 "bar",具体取决于哪个。
echo $key.":".stream_get_line($new, 2048)."\n";
}
?>
注意:至少一个输入数组必须非空,否则您会收到 E_WARNING 消息
PHP 警告:stream_select(): 无法选择 [9]: 坏文件描述符 (max_fd=0)