【问题标题】:PHP stream timeout if no data is transferred如果没有数据传输,PHP 流超时
【发布时间】:2018-03-01 16:17:40
【问题描述】:

我目前正在实现一个获取图像文件并在本地缓存它们的 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 秒。请问有人能解释一下吗?

【问题讨论】:

标签: php php-stream-wrappers


【解决方案1】:

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

问题

这里的问题似乎是对fopen 在阻塞模式下如何与http 流包装器(在尝试之前我也没有完全理解)一起工作的误解。对于 GET (the default),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”,这在我的平台 (Darwin) 上不是有效的流。我还在脚本末尾关闭了输入流,这始终是一个好习惯。

这将创建一个超时为 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,并设置超时时间来解决此问题。

【讨论】:

  • 这很有帮助,谢谢!我必须使用stream_copy_to_stream,因为文件可能非常大,我不想将内容读入 RAM。您提出的stream_context_create 解决方案效果很好。但我意识到我可能不得不为 Guzzle 流使用另一种解决方案。 Guzzle 有自己的read_timeout
  • 啊,是的,那必须分开。 Guzzle's stream wrapper 似乎没有实现 stream_set_option 或检查来自 fopen 的选项来设置超时。奇怪的是,内置的 PHP http 包装器似乎没有区分读取和连接超时。
【解决方案2】:

“读取超时”“连接超时”是有区别的..

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-07
    相关资源
    最近更新 更多