Lectures de ligne de code partiellement exécutée et à zéro octet dans DeflateStream, GZipStream et CryptoStream

Les méthodes Read() et ReadAsync() sur DeflateStream, GZipStreamet CryptoStream peuvent ne plus renvoyer autant d’octets que demandés.

Auparavant, DeflateStream, GZipStream, et CryptoStream avaient un comportement différent de celui typique de Stream.Read et Stream.ReadAsync, et ce pour les deux raisons suivantes. Cette modification a pour but de remédier à ces différences :

  • Ils n’ont pas terminé l’opération de lecture tant que la mémoire tampon passée à l’opération de lecture n’a pas été complètement remplie ou que la fin du flux n’a pas été atteinte.
  • En tant que flux wrapper, ils n’ont pas délégué la fonctionnalité de mémoire tampon de longueur zéro au flux qu’ils enveloppent.

Prenons cet exemple qui crée et compresse 150 octets aléatoires. Il envoie ensuite les données compressées un octet à la fois du client au serveur, et le serveur décompose les données en appelant Read et en demandant la totalité des 150 octets.

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;
            }
        }
    }
}

Dans les versions précédentes de .NET et .NET Framework, la sortie suivante montre que Read n’a été appelé qu’une seule fois. Même si les données étaient disponibles pour que GZipStream les renvoie, Read était contraint d’attendre jusqu’à ce que le nombre d’octets demandé soit disponible.

Read: 150 bytes
Total received: 150 bytes

Dans .NET 6 et les versions ultérieures, la sortie suivante indique que Read a été appelé plusieurs fois jusqu’à ce que toutes les données demandées aient été reçues. Même si l’appel à Read demande 150 octets, chaque appel à Read a pu décompresser avec succès quelques octets (autrement dit, tous les octets qui avaient été reçus à ce moment-là) pour les renvoyer, et c’est ce qu’il a fait :

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

Ancien comportement

Lorsque Stream.Read ou Stream.ReadAsync a été appelé sur l’un des types de flux affectés avec une mémoire tampon de longueur N, l’opération ne se termine pas tant que :

  • N octets avaient été lus à partir du flux, ou
  • Le flux sous-jacent renvoie 0 lors d’un appel à sa lecture, indiquant qu’il n’y a plus de données disponibles.

En outre, lorsque Stream.Read ou Stream.ReadAsync a été appelé avec une mémoire tampon de longueur 0, l’opération réussit immédiatement, parfois sans effectuer une lecture de longueur nulle sur le flux qu’il enveloppe.

Nouveau comportement

À compter de .NET 6, lorsque Stream.Read ou Stream.ReadAsync a été appelé sur l’un des types de flux affectés avec une mémoire tampon de longueur N, l’opération ne se termine pas tant que :

  • Au moins 1 octet a été lu à partir du flux, ou
  • Le flux sous-jacent renvoie 0 d’un appel à sa lecture, indiquant qu’il n’y a plus de données disponibles.

Également, lorsque Stream.Read ou Stream.ReadAsync est appelé avec une mémoire tampon de longueur 0, l’opération réussit une fois qu’un appel avec une mémoire tampon autre que zéro réussit.

Lorsque vous appelez l’une des méthodes affectées Read , si la lecture peut satisfaire au moins un octet de la demande, quel que soit le nombre demandé, elle renvoie autant d’octets que possible à ce moment-là.

Version introduite

6.0

Raison du changement

Les flux peuvent ne pas être retournés à partir d’une opération de lecture même si les données avaient été lues avec succès. Cela signifiait qu’ils ne pouvaient pas être facilement utilisés dans une situation de communication bidirectionnelle où des messages inférieurs à la taille de la mémoire tampon étaient utilisés. Cela peut entraîner des interblocages : l’application ne peut pas lire les données du flux nécessaire pour poursuivre l’opération. Cela pourrait également entraîner des ralentissements arbitraires, le consommateur ne pouvant pas traiter les données disponibles en attendant l’arrivée d’autres données.

En outre, dans les applications hautement évolutives, il est courant d’utiliser des lectures à zéro octet pour retarder l’allocation de mémoire tampon jusqu’à ce qu’une mémoire tampon soit nécessaire. Une application peut émettre une lecture avec une mémoire tampon vide et une fois cette lecture terminée, les données sont bientôt disponibles pour être consommées. L’application peut ensuite émettre à nouveau la lecture, cette fois avec une mémoire tampon pour recevoir les données. En déléguant au flux inclus dans un wrapper si aucune donnée déjà décompressée ou transformée n’est disponible, ces flux héritent désormais de tout comportement des flux qu’ils enveloppent.

En général, le code doit :

  • ne pas faire d’hypothèses sur une opération de flux Read ou ReadAsync lisant autant que ce qui a été demandé. L’appel renvoie le nombre d’octets lus, qui peut être inférieur à ce qui a été demandé. Si une application dépend du remplissage complet de la mémoire tampon avant de progresser, elle peut effectuer la lecture dans une boucle pour retrouver le comportement.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Attendez-vous à ce qu’un appel de flux Read ou ReadAsync ne se termine pas tant qu’au moins un octet de données n’est pas disponible pour la consommation (ou que le flux atteint sa fin), quel que soit le nombre d’octets demandés. Si une application dépend d’une lecture de zéro octet qui se termine immédiatement sans attendre, elle peut vérifier la longueur de la mémoire tampon elle-même et ignorer entièrement l’appel :

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

API affectées