Teilweise und Null-Byte-Lesevorgänge in DeflateStream, GZipStream und CryptoStream

Die Methoden Read() und ReadAsync() auf DeflateStream, GZipStream und CryptoStream geben möglicherweise nicht mehr so viele Bytes zurück, wie angefordert wurden.

Zuvor wichen DeflateStream, GZipStream und CryptoStream vom typischen Verhalten von Stream.Read und Stream.ReadAsync in den folgenden beiden Punkten ab, die beide durch diese Änderung behoben werden:

  • Der Lesevorgang wurde von ihnen erst abgeschlossen, als der an den Lesevorgang übergebene Puffer vollständig gefüllt war oder das Ende des Streams erreicht wurde.
  • Als Wrapperstreams haben sie keine Pufferfunktionen der Länge 0 (null) an den Stream delegiert, den sie umschließen.

Betrachten Sie dieses Beispiel, das 150 zufällige Bytes erstellt und komprimiert. Anschließend sendet er die komprimierten Daten Byte für Byte vom Client an den Server. Der Server dekomprimiert die Daten, indem er Read aufruft und alle 150 Bytes anfordert.

using System.IO.Compression;
using System.Net;
using System.Net.Sockets;

internal class Program
{
    private static async Task Main()
    {
        // Connect two sockets and wrap a stream around each.
        using (Socket listener = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
        using (Socket client = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
        {
            listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
            listener.Listen(int.MaxValue);
            client.Connect(listener.LocalEndPoint!);
            using (Socket server = listener.Accept())
            {
                var clientStream = new NetworkStream(client, ownsSocket: true);
                var serverStream = new NetworkStream(server, ownsSocket: true);

                // Create some compressed data.
                var compressedData = new MemoryStream();
                using (var gz = new GZipStream(compressedData, CompressionLevel.Fastest, leaveOpen: true))
                {
                    byte[] bytes = new byte[150];
                    new Random().NextBytes(bytes);
                    gz.Write(bytes, 0, bytes.Length);
                }

                // Trickle it from the client stream to the server.
                Task sendTask = Task.Run(() =>
                {
                    foreach (byte b in compressedData.ToArray())
                    {
                        clientStream.WriteByte(b);
                    }
                    clientStream.Dispose();
                });

                // Read and decompress all the sent bytes.
                byte[] buffer = new byte[150];
                int total = 0;
                using (var gz = new GZipStream(serverStream, CompressionMode.Decompress))
                {
                    int numRead = 0;
                    while ((numRead = gz.Read(buffer.AsSpan(numRead))) > 0)
                    {
                        total += numRead;
                        Console.WriteLine($"Read: {numRead} bytes");
                    }
                }
                Console.WriteLine($"Total received: {total} bytes");

                await sendTask;
            }
        }
    }
}

In früheren Versionen von .NET und .NET Framework zeigt die folgende Ausgabe, dass Read nur einmal aufgerufen wurde. Obwohl Daten für die Rückgabe von GZipStream verfügbar waren, war Read gezwungen zu warten, bis die angeforderte Anzahl von Bytes verfügbar war.

Read: 150 bytes
Total received: 150 bytes

In .NET 6 und späteren Versionen zeigt die folgende Ausgabe, dassRead mehrfach aufgerufen wurde, bis alle angeforderten Daten empfangen wurden. Obwohl der Aufruf von Read 150 Bytes anfordert, konnte jeder Aufruf von Read erfolgreich einige Bytes dekomprimieren (d. h. alle Bytes, die zu diesem Zeitpunkt empfangen wurden), um zurückzukehren, und das tat er auch:

Read: 1 bytes
Read: 101 bytes
Read: 4 bytes
Read: 4 bytes
Read: 2 bytes
Read: 2 bytes
Read: 2 bytes
Read: 2 bytes
Read: 3 bytes
Read: 2 bytes
Read: 3 bytes
Read: 2 bytes
Read: 2 bytes
Read: 2 bytes
Read: 2 bytes
Read: 1 bytes
Read: 2 bytes
Read: 1 bytes
Read: 1 bytes
Read: 1 bytes
Read: 2 bytes
Read: 1 bytes
Read: 1 bytes
Read: 2 bytes
Read: 1 bytes
Read: 1 bytes
Read: 2 bytes
Total received: 150 bytes

Altes Verhalten

Wenn Stream.Read oder Stream.ReadAsync für einen der betroffenen Streamtypen mit einem Puffer der Länge N aufgerufen wurde, wurde der Vorgang nur in folgenden Fällen abgeschlossen:

  • Es wurden N Bytes aus dem Stream gelesen, oder
  • Der zugrunde liegende Stream gab bei einem Aufruf des Lesevorgangs 0 zurück, was darauf hinweist, dass keine Daten mehr verfügbar sind.

Zudem war der Vorgang beim Aufrufen von Stream.Read oder Stream.ReadAsync mit einem Puffer der Länge 0 sofort erfolgreich – manchmal ohne einen Lesevorgang der Länge 0 (null) für den Stream, den er umschließt.

Neues Verhalten

Ab .NET 6 gilt: Wenn Stream.Read oder Stream.ReadAsync für einen der betroffenen Streamtypen mit einem Puffer der Länge N aufgerufen wird, wird der Vorgang nur in folgenden Fällen abgeschlossen:

  • Mindestens ein Byte wurde aus dem Stream gelesen, oder
  • Der zugrunde liegende Stream gab bei einem Aufruf des Lesevorgangs 0 zurück, was darauf hinweist, dass keine Daten mehr verfügbar sind.

Zudem ist der Vorgang beim Aufrufen von Stream.Read oder Stream.ReadAsync mit einem Puffer der Länge 0 erfolgreich, sobald ein Aufruf mit einem Puffer, der nicht null ist, erfolgreich abgeschlossen wurde.

Wenn Sie eine der betroffenen Read Methoden aufrufen, kann der Lesevorgang mindestens ein Byte der Anforderung erfüllen, unabhängig davon, wie viele angefordert wurden, so viele wie in diesem Momentzurückgegeben werden.

Eingeführt in Version

6.0

Grund für die Änderung

Die Streams wurden möglicherweise nicht von einem Lesevorgang zurückgegeben, auch wenn die Daten erfolgreich gelesen wurden. Dies bedeutete, dass sie nicht ohne Weiteres in einer bidirektionalen Kommunikationssituation verwendet werden konnten, in der Nachrichten verwendet wurden, die kleiner als die Puffergröße waren. Dies kann zu Deadlocks führen: Die Anwendung kann die Daten nicht aus dem Stream lesen, der zum Fortsetzen des Vorgangs erforderlich ist. Außerdem kann dies zu willkürlichen Verlangsamungen führen, da der Consumer verfügbare Daten nicht verarbeiten kann, während er auf das Eintreffen zusätzlicher Daten wartet.

Darüber hinaus ist es in hochgradig skalierbaren Anwendungen üblich, Null-Byte-Lesevorgänge zu verwenden, um die Pufferzuordnung zu verzögern, bis ein Puffer benötigt wird. Eine Anwendung kann einen Lesevorgang mit einem leeren Puffer ausgeben, und wenn dieser Lesevorgang abgeschlossen ist, sollten bald Daten für die Verarbeitung verfügbar sein. Die Anwendung kann dann den Lesevorgang erneut mit einem Puffer zum Empfangen der Daten erstellen. Durch Delegieren an den umschlossenen Stream, wenn keine bereits dekomprimierten oder transformierten Daten verfügbar sind, erben diese Streams jetzt alle derartigen Verhaltensweisen von den Streams, die sie umschließen.

Im Allgemeinen sollte Code:

  • Keine Mutmaßungen über den Read- oder ReadAsync-Vorgang eines Streams anstellen, beim dem so viel wie angefordert gelesen wird. Der Aufruf gibt die Anzahl der gelesenen Bytes zurück, die möglicherweise kleiner ist als angefordert. Wenn eine Anwendung davon abhängt, dass der Puffer vor dem Fortschritt vollständig gefüllt wird, kann sie den Lesevorgang in einer Schleife ausführen, um das Verhalten zurückzuerlangen.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Gehen Sie davon aus, dass der Read- oder ReadAsync-Aufruf eines Streams möglicherweise nicht abgeschlossen wird, bevor mindestens ein Byte Daten zur Verarbeitung verfügbar ist (oder der Stream das Ende erreicht), unabhängig davon, wie viele Bytes angefordert wurden. Wenn eine Anwendung davon abhängt, dass ein Null-Byte-Lesevorgang sofort ohne Wartezeit abgeschlossen wird, kann sie die Pufferlänge selbst überprüfen und den Aufruf vollständig überspringen:

    int bytesRead = 0;
    if (!buffer.IsEmpty)
    {
        bytesRead = stream.Read(buffer);
    }
    

Betroffene APIs

Siehe auch