PHP 中的流

王林
发布: 2024-08-26 06:33:35
原创
530 人浏览过

Streams in PHP

Ob Sie jemals mit lokalen Dateien, HTTP-Anfragen oder komprimierten Dateien zu tun hatten, Sie haben sich mit Streams beschäftigt, aber... haben Sie sie wirklich kennengelernt?

Ich denke, dass dies eines der am meisten missverstandenen Konzepte in PHP ist, und als Konsequenz habe ich gesehen, dass aufgrund des Mangels an grundlegenden Kenntnissen eine ganze Reihe von Fehlern eingeführt wurden.

In diesem Artikel versuche ich zu erklären, was Streams wirklich sind und wie man mit ihnen arbeitet. Wir werden viele Funktionen sehen, die für die Arbeit mit Streams verwendet werden, sowie viele Beispiele, aber es ist nicht meine Absicht, sie alle in irgendeiner Weise „neu zu dokumentieren“.

Bevor wir lernen, was Streams sind, müssen wir uns zunächst mit den Ressourcen befassen.

Ressourcen

Ressourcen sind einfach Verweise oder Zeiger auf externe Ressourcen, wie zum Beispiel eine Datei, eine Datenbank, ein Netzwerk oder eine SSH-Verbindung.

Es gibt verschiedene Arten von Ressourcen, wie zum Beispiel „Curl“ – erstellt durch „curl_init()“, „Process“, erstellt durch „proc_open“ und „Stream“, erstellt durch Funktionen wie „fopen()“, „opendir“ und andere.

Streams

Streams sind die Art und Weise, wie PHP Arten von Ressourcen verallgemeinert, die ein gemeinsames Verhalten aufweisen, das heißt, Ressourcen können linear gelesen und beschrieben werden, wie eine Kassette (verdammt, ich werde alt). Einige Beispiele für Streams sind Dateiressourcen, HTTP-Antworttexte und komprimierte Dateien, um nur einige zu nennen.

Streams sind unglaublich nützlich, da sie es uns ermöglichen, mit Ressourcen zu arbeiten, die zwischen einigen Bytes und mehreren GB groß sind und der Versuch, sie beispielsweise vollständig zu lesen, unseren verfügbaren Speicher erschöpfen würde.

Einen Stream mit fopen erstellen

fopen( string $filename, string $mode, bool $use_include_path = false, ?resource $context = null ): resource|false
登录后复制

fopen öffnet eineDateioder eineNetzwerkressource[1], abhängig vom Pfad, der zum ersten Parameter angegeben wird. Wie bereits erwähnt, ist diese Ressource vom Typ stream:

$fileStream = fopen('/tmp/test', 'w'); echo get_resource_type($fileStream); // 'stream'
登录后复制

Wenn $filename in der Form scheme:// angegeben wird, wird davon ausgegangen, dass es sich um eine URL handelt, und PHP versucht, einen unterstützten Protokollhandler/Wrapper zu finden, der mit dem Pfad übereinstimmt, z. B. file:// – um lokale Dateien zu verarbeiten. http:// – um mit Remote-HTTP/S-Ressourcen zu arbeiten, ssh2:// – um SSH-Verbindungen zu verarbeiten oder php:// – das ermöglicht uns den Zugriff auf PHPs eigene Eingabe- und Ausgabestreams, wie php://stdin, php: //stdout und php://stderr.

$mode definiert die Art des Zugriffs, den Sie auf den Stream benötigen, d. h., ob Sie nur Lesezugriff, nur Schreiben, Lesen und Schreiben, Lesen/Schreiben vom Anfang oder Ende des Streams usw. benötigen.

Der Modus hängt auch von der Art der Ressource ab, an der Sie arbeiten. Zum Beispiel:

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

Das Öffnen eines beschreibbaren Streams beispielsweise mit dem Wrapper https:// funktioniert nicht:

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

[1] Die Verwendung von fopen mit Netzwerk- oder Remote-Ressourcen funktioniert nur, wenn „allow_url_fopen“ in php.ini aktiviert ist. Weitere Informationen finden Sie in der Dokumentation.

Also, jetzt haben wir eine Stream-Ressource. Was können wir damit machen?

Mit fwrite in einen Dateistream schreiben

fwrite(resource $stream, string $data, ?int $length = null): int|false
登录后复制

fwrite ermöglicht es uns, die für $data bereitgestellten Inhalte in einen Stream zu schreiben. Wenn $length angegeben wird, wird nur die angegebene Anzahl an Bytes geschrieben. Sehen wir uns ein Beispiel an:

$fileStream = fopen('/tmp/test', 'w'); fwrite($fileStream, "The quick brown fox jumps over the lazy dog", 10);
登录后复制

Da wir in diesem Beispiel $length = 10 angegeben haben, wurde nur ein Teil des Inhalts geschrieben – „Der Schnelle“ – und der Rest wurde ignoriert.

Beachten Sie, dass wir den Dateistream mit $mode = 'w' geöffnet haben, wodurch wir Inhalte in die Datei schreiben konnten. Hätten wir die Datei stattdessen mit $mode = 'r' geöffnet, würden wir eine Meldung wie fwrite() erhalten: Write of 8192 bytes failed with errno=9 Bad file descriptor.

Sehen wir uns ein weiteres Beispiel an, bei dem nun der gesamte Inhalt in den Dateistream geschrieben wird:

$fileStream = fopen('/tmp/test', 'w'); fwrite($fileStream, "The quick brown fox jumps over the lazy dog");
登录后复制

Da wir nun $length nicht angegeben haben, wurde der gesamte Inhalt in die Datei geschrieben.

Beim Schreiben in einen Stream wird die Position des Lese-/Schreibzeigers an das Ende der Sequenz verschoben. In diesem Fall hat die in den Stream geschriebene Zeichenfolge 44 Zeichen, daher sollte die Position des Zeigers jetzt 43.

sein

Neben dem Schreiben in eine Datei kann fwrite auch in andere Arten von Streams schreiben, beispielsweise in Sockets. Beispiel aus den Dokumenten:

$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);
登录后复制

Streams mit Angst lesen

fread(resource $stream, int $length): string|false
登录后复制

Mit fread können SieBytes mit einer Länge von bis zuaus einem Stream lesen, beginnend mit dem aktuellen Lesezeiger. Es ist binärsicher und funktioniert mit lokalen und Netzwerkressourcen, wie wir in den Beispielen sehen werden.

Durch den aufeinanderfolgenden Aufruf von fread wird ein Block gelesen und dann der Lesezeiger an das Ende dieses Blocks verschoben. Beispiel unter Berücksichtigung der im vorherigen Beispiel geschriebenen Datei:

# 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
登录后复制

Wir werden bald auf ftell zurückkommen, aber es gibt lediglich die aktuelle Position des Lesezeigers zurück.

Der Lesevorgang stoppt (und gibt „false“ zurück), sobald eines der folgenden Ereignisse auftritt (aus den Dokumenten kopiert, Sie werden es später verstehen):

  • 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,ignoringits 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 readup to8192 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 thatnot 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 willalwaysbe 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'); // "\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中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责声明 Sitemap
PHP中文网:公益在线PHP培训,帮助PHP学习者快速成长!