首页  >  文章  >  后端开发  >  PHP 中的流

PHP 中的流

王林
王林原创
2024-08-26 06:33:35658浏览

Streams in PHP

无论您曾经处理过本地文件、HTTP 请求还是压缩文件,您都处理过流,但是……您真的了解它们吗?

我认为这是 PHP 中最容易被误解的概念之一,因此我看到了很多由于缺乏一些基础知识而引入的错误。

在本文中,我将尝试解释流到底是什么以及如何使用它们。我们将看到许多用于处理流的函数以及大量示例,但我无意以任何方式“重新记录”所有这些函数。

在了解什么是流之前,我们首先需要接触资源。

资源

资源只是对外部资源的引用或指针,例如文件、数据库、网络或 SSH 连接。

有多种类型的资源,例如curl - 由curl_init() 创建,进程,由proc_open 创建,流,由fopen()、opendir 等函数创建。

流是 PHP 概括具有共同行为的资源类型的方式,也就是说,资源可以线性读取和写入,就像盒式磁带一样(该死,我老了)。流的一些示例包括文件资源、HTTP 响应主体和压缩文件,仅举几例。

流非常有用,因为它们使我们能够处理大小从几个字节到几个 GB 的资源,例如,尝试完全读取它们会耗尽我们的可用内存。

使用 fopen 创建流

 fopen(
    string $filename,
    string $mode,
    bool $use_include_path = false,
    ?resource $context = null
): resource|false

fopen 打开一个 文件网络资源[1],具体取决于提供给第一个参数的路径。如前所述,该资源的类型为流:

$fileStream = fopen('/tmp/test', 'w');
echo get_resource_type($fileStream); // 'stream'

如果$filename以scheme://形式提供,则假定它是一个URL,PHP将尝试查找与路径匹配的支持的协议处理程序/包装器,例如file:// - 来处理本地文件,http:// - 用于处理远程 HTTP/S 资源,ssh2:// - 处理 SSH 连接或 php:// - 允许我们访问 PHP 自己的输入和输出流,例如 php://stdin, php://stdout 和 php://stderr。

$mode 定义了你需要对流的访问类型,即是否只需要读访问、只需要写、读写、从流的开头或末尾读/写等等。

该模式还取决于您正在处理的资源类型。例如:

$fileStream = fopen('/tmp/test', 'w');
$networkStream = fopen('https://google.com', 'r');

例如,使用包装器 https:// 打开可写流不起作用:

fopen('https://google.com', 'w'); // Failed to open stream: HTTP wrapper does not support writeable connections

[1] 仅当在 php.ini 上启用了allow_url_fopen 时,才能将 fopen 与网络或远程资源结合使用。有关更多信息,请查看文档。

现在我们有了流资源,我们可以用它们做什么?

使用 fwrite 写入文件流

fwrite(resource $stream, string $data, ?int $length = null): int|false

fwrite 使我们能够将提供给 $data 的内容写入流中。如果提供了 $length,则它仅写入给定的提供的字节数。让我们看一个例子:

$fileStream = fopen('/tmp/test', 'w');

fwrite($fileStream,  "The quick brown fox jumps over the lazy dog", 10);

在这个例子中,由于我们提供了 $length = 10,所以只写了部分内容 - “The Quick” - 忽略其余部分。

请注意,我们使用 $mode = 'w' 打开文件流,这使我们能够将内容写入文件。相反,如果我们使用 $mode = 'r' 打开文件,我们将收到一条消息,例如 fwrite(): Write of 8192 bytes failed with errno=9 Bad file detector。

让我们看另一个例子,现在将整个内容写入文件流:

$fileStream = fopen('/tmp/test', 'w');

fwrite($fileStream,  "The quick brown fox jumps over the lazy dog");

现在,由于我们还没有提供 $length,所以全部内容都已写入文件中。

写入流会将读/写指针的位置移动到序列的末尾。在这种情况下,写入流中的字符串有 44 个字符,因此,指针现在的位置应该是 43。

除了写入文件之外,fwrite 还可以写入其他类型的流,例如套接字。从文档中提取的示例:

$sock = fsockopen("ssl://secure.example.com", 443, $errno, $errstr, 30);
if (!$sock) die("$errstr ($errno)\n");

$data = "foo=" . urlencode("Value for Foo") . "&bar=" . urlencode("Value for Bar");

fwrite($sock, "POST /form_action.php HTTP/1.0\r\n");
fwrite($sock, "Host: secure.example.com\r\n");
fwrite($sock, "Content-type: application/x-www-form-urlencoded\r\n");
fwrite($sock, "Content-length: " . strlen($data) . "\r\n");
fwrite($sock, "Accept: */*\r\n");
fwrite($sock, "\r\n");
fwrite($sock, $data);

$headers = "";
while ($str = trim(fgets($sock, 4096)))
$headers .= "$str\n";

echo "\n";

$body = "";
while (!feof($sock))
$body .= fgets($sock, 4096);

fclose($sock);

使用 fread 读取流

fread(resource $stream, int $length): string|false

使用 fread,您可以从流中读取 最多 $length 个字节,从当前读取指针开始。它是二进制安全的,并且可以与本地和网络资源一起使用,正如我们将在示例中看到的那样。

连续调用fread会读取一个chunk,然后将读指针移动到该chunk的末尾。例如,考虑上一个示例中写入的文件:

# Content: "The quick brown fox jumps over the lazy dog"
$fileStream = fopen('/tmp/test', 'r');

echo fread($fileStream, 10) . PHP_EOL;      // 'The quick '
echo ftell($fileStream); // 10
echo fread($fileStream, 10) . PHP_EOL;      // 'brown fox '
echo ftell($fileStream); // 20

我们很快就会回来 ftell,但它所做的只是返回读取指针的当前位置。

一旦出现以下情况之一,读取就会停止(返回 false)(从文档复制,稍后你会明白):

  • length bytes have been read
  • EOF (end of file) is reached
  • a packet becomes available or the socket timeout occurs (for network streams)
  • if the stream is read buffered and it does not represent a plain file, at most one read of up to a number of bytes equal to the chunk size (usually 8192) is made; depending on the previously buffered data, the size of the returned data may be larger than the chunk size.

I don't know if you had the same felling, but this last part is pretty cryptic, so let's break it down.

"if the stream is read buffered"

Stream reads and writes can be buffered, that is, the content may be stored internally. It is possible to disable/enable the buffering, as well as set their sizes using stream_set_read_buffer and stream-set-write-buffer, but according to this comment on the PHP doc's Github, the description of these functions can be misleading.

This is where things get interesting, as this part of the documentation is really obscure. As per the comment, setting stream_set_read_buffer($stream, 0) would disable the read buffering, whereas stream_set_read_buffer($stream, 1) or stream_set_read_buffer($stream, 42) would simply enable it, ignoring its size (depending on the stream wrapper, which can override this default behaviour).

"... at most one read of up to a number of bytes equal to the chunk size (usually 8192) is made"

The chunk size is usually 8192 bytes or 8 KiB, as we will confirm in a bit. We can change this value using stream_set_chunk_size. Let's see it in action:

$f = fopen('https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.2-x86_64.iso', 'rb');

$previousPos = 0;
$chunkSize = 1024;
$i = 1;

while ($chunk = fread($f, $chunkSize)) {
    $bytesRead = (ftell($f) - $previousPos);
    $previousPos = ftell($f);

    echo "Iteration: {$i}. Bytes read: {$bytesRead}" . PHP_EOL;

    $i++;
}

Output:

Iteration: 1. Bytes read: 1024
Iteration: 2. Bytes read: 1024
Iteration: 3. Bytes read: 1024
...
Iteration: 214016. Bytes read: 1024
Iteration: 214017. Bytes read: 169

What happened in this case was clear:

  • We wanted up to 1024 bytes in each fread call and that's what we got
  • In the last call there were only 169 bytes remainder, which were returned
  • When there was nothing else to return, that is, EOF was reached fread returned false and the loop finished.

Now let's increase considerably the length provided to fread to 1 MiB and see what happens:

$f = fopen('https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.2-x86_64.iso', 'rb');

$previousPos = 0;
$chunkSize = 1048576; // 1 MiB
$i = 1;

while ($chunk = fread($f, $chunkSize)) {
    $bytesRead = (ftell($f) - $previousPos);
    $previousPos = ftell($f);

    echo "Iteration: {$i}. Bytes read: {$bytesRead}" . PHP_EOL;

    $i++;
}

Output:

Iteration: 1. Bytes read: 1378
Iteration: 2. Bytes read: 1378
Iteration: 3. Bytes read: 1378
...
Iteration: 24. Bytes read: 1074
Iteration: 25. Bytes read: 8192
Iteration: 26. Bytes read: 8192
...
Iteration: 26777. Bytes read: 8192
Iteration: 26778. Bytes read: 8192
Iteration: 26779. Bytes read: 293

So, even though we tried to read 1 MiB using fread, it read up to 8192 bytes - same value that the docs said it would. Interesting. Let's see another experiment:

$f = fopen('https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.2-x86_64.iso', 'rb');

$previousPos = 0;
$chunkSize = 1048576; // 1 MiB
$i = 1;

stream_set_chunk_size($f, $chunkSize); // Just added this line

while ($chunk = fread($f, $chunkSize)) {
    $bytesRead = (ftell($f) - $previousPos);
    $previousPos = ftell($f);

    echo "Iteration: {$i}. Bytes read: {$bytesRead}" . PHP_EOL;

    $i++;
}

And the output:

Iteration: 1. Bytes read: 1378
Iteration: 2. Bytes read: 1378
Iteration: 3. Bytes read: 1378
...
Iteration: 12. Bytes read: 533
Iteration: 13. Bytes read: 16384
Iteration: 14. Bytes read: 16384
...
Iteration: 13386. Bytes read: 16384
Iteration: 13387. Bytes read: 16384
Iteration: 13388. Bytes read: 13626

Notice that now fread read up to 16 KiB - not even close to what we wanted, but we've seen that stream_set_chunk_size did work, but there are some hard limits, that I suppose that depends also on the wrapper. Let's put that in practice with another experiment, using a local file this time:

$f = fopen('alpine-standard-3.20.2-x86_64.iso', 'rb');

$previousPos = 0;
$chunkSize = 1048576; // 1 MiB
$i = 1;

while ($chunk = fread($f, $chunkSize)) {
    $bytesRead = (ftell($f) - $previousPos);
    $previousPos = ftell($f);

    echo "Iteration: {$i}. Bytes read: {$bytesRead}" . PHP_EOL;

    $i++;
}

Output:

Iteration: 1. Bytes read: 1048576
Iteration: 2. Bytes read: 1048576
...
Iteration: 208. Bytes read: 1048576
Iteration: 209. Bytes read: 1048576

Aha! So using the local file handler we were able to fread 1 MiB as we wanted, and we did not even need to increase the buffer/chunk size with stream_set_chunk_size.

Wrapping up

I think that now the description is less cryptic, at least. Let's read it again (with some interventions):

if the stream is read buffered ...

and it does not represent a plain file (that is, local, not a network resource), ...

at most one read of up to a number of bytes equal to the chunk size (usually 8192) is made (and in our experiments we could confirm that this is true, at least one read of the chunk size was made); ...

depending on the previously buffered data, the size of the returned data may be larger than the chunk size (we did not experience that, but I assume it may happen depending on the wrapper).

There is definitely some room to play here, but I will challenge you. What would happen if you disable the buffers while reading a file? And a network resource? What if you write into a file?

ftell

ftell(resource $stream): int|false

ftell returns the position of the read/write pointer (or null when the resource is not valid).

# Content: "The quick brown fox jumps over the lazy dog"
$fileStream = fopen('/tmp/test', 'r');

fread($fileStream, 10); # "The quick "
echo ftell($fileStream); 10

stream_get_meta_data

stream_get_meta_data(resource $stream): array

stream_get_meta_data returns information about the stream in form of an array. Let's see an example:

# Content: "The quick brown fox jumps over the lazy dog"
$fileStream = fopen('/tmp/test', 'r');
var_dump(stream_get_meta_data($fileStream)): 

The previous example would return in something like this:

array(9) {
  ["timed_out"]=>
  bool(false)
  ["blocked"]=>
  bool(true)
  ["eof"]=>
  bool(false)
  ["wrapper_type"]=>
  string(9) "plainfile"
  ["stream_type"]=>
  string(5) "STDIO"
  ["mode"]=>
  string(1) "r"
  ["unread_bytes"]=>
  int(0)
  ["seekable"]=>
  bool(true)
  ["uri"]=>
  string(16) "file:///tmp/test"
}

This function's documentation is pretty honest describing each value ;)

fseek

fseek(resource $stream, int $offset, int $whence = SEEK_SET): int

fseek sets the read/write pointer on the opened stream to the value provided to $offset.
The position will be updated based on $whence:

  • SEEK_SET: Position is set to $offset, that is, if you call
  • SEEK_CUR: Position is set based on the current one, that is, current + $offset
  • SEEK_END: Position is set to End Of File + $offset.

Using SEEK_END we can provide a negative value to $offset and go backwards from EOF. Its return value can be used to assess if the position has been set successfully (0) or has failed (-1).

Let's see some examples:

# Content: "The quick brown fox jumps over the lazy dog\n"
$fileStream = fopen('/tmp/test', 'r+');

fseek($fileStream, 4, SEEK_SET);
echo fread($fileStream, 5);         // 'quick'
echo ftell($fileStream);            // 9

fseek($fileStream, 7, SEEK_CUR);
echo ftell($fileStream);            // 16, that is, 9 + 7
echo fread($fileStream, 3);         // 'fox'  

fseek($fileStream, 5, SEEK_END);    // Sets the position past the End Of File
echo ftell($fileStream);            // 49, that is, EOF (at 44th position) + 5
echo fread($fileStream, 3);         // ''  
echo ftell($fileStream);            // 49, nothing to read, so read/write pointer hasn't changed
fwrite($fileStream, 'foo');
ftell($fileStream);                 // 52, that is, previous position + 3
fseek($fileStream, -3, SEEK_END);
ftell($fileStream);                 // 49, that is, 52 - 3
echo fread($fileStream, 3);         // 'foo'  

Some important considerations

  1. As we've seen in this example, it is possible we seek past the End Of File and even read in an unwritten area (which returns 0 bytes), but some types of streams do not support it.

  2. An important consideration is that not all streams can be seeked, for instance, you cannot fseek a remote resource:

$f = fopen('https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-standard-3.20.2-x86_64.iso', 'rb');
fseek($f, 10); WARNING  fseek(): Stream does not support seeking

This obviously makes total sense, as we cannot "fast-forward" and set a position on a remote resource. The stream in this case is only read sequentially, like a cassette tape.
We can determine if the stream is seekable or not via the seekable value returned by stream_get_meta_data that we've seen before.

  1. We can fseek a resource opened in append mode (a or a+), but the data will always be appended.

rewind

rewind(resource $stream): bool

This is a pure analogy of rewinding a videotape before returning it to video store. As expected, rewind sets the position of the read/write pointer to 0, which is basically the same as calling fseek with $offset 0.

The same considerations we've seen for fseek applies for rewind, that is:

  • You cannot rewind an unseekable stream
  • rewind on a resource opened in append mode will still write from the current position - the write pointer is not updated.

How about file_get_contents?

So far we've been working directly with resources. file_get_contents is a bit different, as it accepts the file path and returns the whole file content as a string, that is, it implicitly opens the resource.

file_get_contents(
   string $filename,
   bool $use_include_path = false,
   ?resource $context = null,
   int $offset = 0,
   ?int $length = null
): string|false

Similar to fread, file_get_contents can work on local and remote resources, depending on the $filename we provide:

# Content: "The quick brown fox jumps over the lazy dog"

echo file_get_contents('/tmp/test'); // "The quick brown fox jumps over the lazy dog\n"

echo file_get_contents('https://www.php.net/images/logos/php-logo.svg'); // "<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -1 100 50">\n ..."

With $offset we can set the starting point to read the content, whereas with length we can get a given amount of bytes.

# Content: "The quick brown fox jumps over the lazy dog"
echo file_get_contents('/tmp/test', offset: 16, size: 3); // 'fox'

offset also accepts negative values, which counts from the end of the stream.

# Content: "The quick brown fox jumps over the lazy dog"
echo file_get_contents('/tmp/test', offset: -4, size: 3); // 'dog'

Notice that the same rules that govern fseek are also applied for $offset, that is - you cannot set an $offset while reading remote files, as the function would be basically fseek the stream, and we've seen that it does not work well.

The parameter context makes file_get_contents really flexible, enabling us set, for example:

  • Set a different HTTP method, such as POST instead of the default GET
  • Provide headers and content to a POST or PUT request
  • Disable SSL verification and allow self-signed certificates

We create a context using stream_context_create, example:

$context = stream_context_create(['http' => ['method' => "POST"]]);
file_get_contents('https://a-valid-resource.xyz', context: $context);

You can find the list of options you can provide to stream_context_create in this page.

$networkResource = fopen('https://releases.ubuntu.com/24.04/ubuntu-24.04-desktop-amd64.iso', 'r');

while ($chunk = fread($networkResource, 1024)) {
   doSomething($chunk);
}

Which one to use? fread, file_get_contents, fgets, another one?

The list of functions that we can use to read local or remote contents is lengthy, and each function can be seen as a tool in your tool belt, suitable for a specific purpose.

According to the docs, file_get_contents is the preferred way of reading contents of a file into a string, but, is it appropriate for all purposes?

  • What if you know that the content is large? Will it fit into memory?
  • Do you need it entirely in memory or can you work in chunks?
  • Are you going to work on local or remote files?

Ask yourself these (and other questions), make some performance benchmark tests and select the function that suits your needs the most.

PSR-7's StreamInterface

PSR defines the StreamInterface, which libraries such as Guzzle use to represent request and response bodies. When you send a request, the body is an instance of StreamInterface. Let's see an example, extracted from the Guzzle docs:

$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'http://httpbin.org/get');

$body = $response->getBody();
$body->seek(0);
$body->read(1024);

I suppose that the methods available on $body look familiar for you now :D

StreamInterface implements methods that resemble a lot the functions we've just seen, such as:

  • seek()
  • tell()
  • eof()
  • read
  • write
  • isSeekable
  • isReadable()
  • isWritable
  • and so on.

Last but not least, we can use GuzzleHttp\Psr7\Utils::streamFor to create streams from strings, resources opened with fopen and instances of StreamInterface:

use GuzzleHttp\Psr7;

$stream = Psr7\Utils::streamFor('string data');
echo $stream;                   // string data
echo $stream->read(3);          // str
echo $stream->getContents();    // ing data
var_export($stream->eof());     // true
var_export($stream->tell());    // 11

Summary

In this article we've seen what streams really are, learned how to create them, read from them, write to them, manipulate their pointers as well as clarified some obscured parts regarding read a write buffers.

If I did a good job, some of the doubts you might have had regarding streams are now a little bit clearer and, from now on, you'll write code more confidently, as you know what you are doing.

Should you noticed any errors, inaccuracies or there is any topic that is still unclear, let me know in the comments and I'd be glad to try to help.

以上是PHP 中的流的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn