PHP 流超时(如果未传输任何数据)



我目前正在实现一个PHP类,该类获取图像文件并将其缓存在本地。这些图像可能来自其他本地来源,通过HTTP或使用Guzzle客户端的HTTP。使用 PHP 流包装器,我应该能够以相同的方式处理所有源代码。

我现在正在尝试做的是,如果没有数据通过流传输,则实现超时。这应该处理以下情况:

  1. 首先无法建立流。这可能应该在fopen调用时处理,而不是超时。
  2. 流已建立,但不传输任何数据。
  3. 流已建立,数据传输,但在传输过程中会停止一段时间。

我想我可以用stream_set_timeout做这一切,但我不太清楚这实际上做了什么。如果流上的任何操作花费的时间超过允许的时间,即我可以执行两次需要 0.5 秒且超时为 0.75 秒的事情,则超时是否适用?还是仅当没有通过流传输数据的时间超过允许的时间时才适用?

我尝试使用此简短脚本测试行为:

<?php
$in = fopen('https://reqres.in/api/users?delay=5', 'r');
$out = fopen('out', 'w');
stream_set_timeout($in, 1);
stream_copy_to_stream($in, $out);
var_dump(stream_get_meta_data($in)['timed_out']);

尽管reqres.in的响应延迟了 5 秒,但我总是false超时 1 秒。请问有人可以解释一下吗?

我建议您使用file_get_contentsfile_put_contents而不是流,它们支持所有包装器,您可以像将上下文传递给它们一样传递给它们fopen.一般来说,它们更容易使用,因为它们返回并接受字符串而不是流。话虽如此,我不知道您的缓存机制的性质,如果流更适合您的用例,那么您将获得更多功能:)

问题所在

这里的问题似乎是对fopen如何在阻塞模式下使用http流包装器(直到我尝试之前我也没有完全理解)的误解。对于 GET(默认值),fopen似乎在调用时执行 HTTP 请求,而不是在读取流时执行。这可以解释为什么stream_set_timeout不能按预期运行,因为它会在调用fopen后修改流上下文。

解决方案

值得庆幸的是,有一种方法可以在调用fopen之前修改超时;您可以使用上下文调用fopen。传递从stream_context_create返回的上下文(如 Sammitch 链接的那样)以正确fopen所有三种情况的超时。作为参考,以下是修改脚本的方式:

<?php
$ctx = stream_context_create(['http' => [
'timeout' => 1.0,
]]);
$in = fopen('https://reqres.in/api/users?delay=5', 'r', false, $ctx);
$out = STDOUT;
stream_copy_to_stream($in, $out);
var_dump(stream_get_meta_data($in)['timed_out']);
fclose($in);

注意:我假设您打算将流复制到 stdout 而不是"out",这在我的平台(达尔文)上不是有效的流。我还在脚本末尾关闭了 in 流,这始终是一种很好的做法。

这将创建一个超时为 1 的流,从调用fopen时开始。现在测试你的三个条件。

验证行为

  1. 首先无法建立流。这可能应该在 fopen 调用时处理,而不是超时。

这按原样正常工作 - 如果无法建立连接(服务器脱机等),fopen调用会立即触发警告。只需将脚本指向本地主机上的某个任意端口,没有任何内容在侦听。请注意,如果未成功建立连接,fopen返回 false。您必须在代码中检查这一点,以避免使用 false 作为流。

  1. 已建立,但不传输任何数据。

此方案也有效,只需使用普通 URL 运行脚本即可。这也会使fopen返回 false 并触发警告(不同的警告)。

  1. 已建立,数据传输,但在传输过程中会停止一段时间。

这是一个有趣的案例。要测试这一点,您可以编写一个脚本,将Content-Length和其他一些标头以及一些部分数据一起发送,然后等到超时,即:

<?php
header('Content-Type: text/plain');
header('Content-Length: 10');
echo "hi";
ob_flush();
sleep(10);

ob_flush是使 PHP 在睡眠和脚本退出之前写入输出(不关闭连接)所必需的。您可以使用php -S localhost:port然后将其他脚本指向localhost:port。在这种情况下,客户端脚本不会引发警告,fopen实际上返回元数据中timed_out设置为 true 的流。

结论

stream_set_timeout不适用于 HTTP GET 请求,fopen阻塞模式下fopen因为在调用请求时执行请求,而不是等待读取执行此操作。您可以将上下文传递给具有超时的fopen以解决此问题。

"读取超时">"连接超时">之间有区别。

连接超时是建立初始连接(完成TCP连接握手)的超时。 读取超时是等待读取数据的超时。如果服务器在最后一个字节后 XX 秒未发送字节,则会生成读取超时错误。

即使您看到 5 秒的延迟(响应时间) - 这可能发生在初始连接(DNS 查找、连接等)期间,而不是在读取期间。

最新更新