PHP 如果没有数据传输,流超时

PHP stream timeout if no data is transferred

我目前正在实施一个 PHP class 来获取图像文件并在本地缓存它们。这些图像可能来自其他本地源,通过 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 一样向它们传递上下文。它们通常更易于使用,因为它们 return 并且接受字符串而不是流。话虽这么说,我不知道你的缓存机制的性质,如果流更适合你的用例,那么你会更有力量:)

问题

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

解决方案

幸运的是,有一种方法可以在调用 fopen 之前修改超时;您可以使用上下文调用 fopen。将上下文 returned 从 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);

注意:我假设您打算将流复制到标准输出而不是 "out",这在我的平台 (Darwin) 上不是有效流。我还在脚本末尾关闭了输入流,这始终是个好习惯。

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

验证行为

  1. The stream cannot be established in the first place. This should probably be handled at the fopen call and not with a timeout.

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

  1. The stream is established but no data is transferred.

这种情况也适用,只是 运行 脚本与您的正常 URL。这也会使 fopen return 为假并触发警告(不同的警告)。

  1. The stream is established, data is transferred but it stops some time during transfer.

这是一个有趣的案例。要对此进行测试,您可以编写一个脚本来发送 Content-Length 和其他一些 headers 以及一些部分数据,然后等到超时,即:

<?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 实际上 return 是元数据中 timed_out 设置为 true 的流。

结论

stream_set_timeout 不适用于 HTTP GET 请求和 fopen 在阻塞模式下 因为 fopen 在调用时执行请求而不是等待阅读这样做。您可以通过超时将上下文传递给 fopen 来解决此问题。

"read time out""connection time out"是有区别的..

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

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