Lecturas parciales y de cero bytes en DeflateStream, GZipStream y CryptoStream

Los métodos Read() y ReadAsync() en DeflateStream, GZipStream y CryptoStream podrían dejar de devolver tantos bytes como se solicitaron.

Anteriormente, DeflateStream, GZipStream, y CryptoStream se diferenciaban del comportamiento típico de Stream.Read y Stream.ReadAsync de las dos maneras siguientes, ambas de los cuales se abordan en este cambio:

  • No completaron la operación de lectura hasta que el búfer pasado a la operación de lectura se llenó completamente o hasta que se alcanzó el final de la secuencia.
  • Como secuencias contenedoras, no delegaron la funcionalidad de búfer de longitud cero en la secuencia que encapsulan.

Considere este ejemplo que crea y comprime 150 bytes aleatorios. A continuación, envía los datos comprimidos de un byte a la vez desde el cliente al servidor y el servidor descomprime los datos llamando a Read y solicitando todos los 150 bytes.

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

En versiones anteriores de .NET y .NET Framework, la salida siguiente muestra que solo se llamó a Read una vez. Aunque los datos estuvieran disponibles para GZipStream y devolver, Read se veía obligado a esperar hasta que estuviera disponible el número solicitado de bytes.

Read: 150 bytes
Total received: 150 bytes

En .NET 6 y versiones posteriores, la siguiente salida muestra que se llamó a Read varias veces hasta que todos los datos solicitados se recibieron. Aunque la llamada a Read solicita 150 bytes, cada llamada a Read pudo descomprimir correctamente algunos bytes (es decir, todos los bytes recibidos en ese momento) para devolver, y lo hizo:

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

Comportamiento anterior

Cuando se llamó a Stream.Read o Stream.ReadAsync en uno de los tipos de secuencia afectados con un búfer de longitud N, la operación no se completaría hasta que:

  • se leyeran N bytes de la secuencia, o
  • La secuencia subyacente devolvió 0 a raíz de una llamada a su lectura, lo que indica que no hay más datos disponibles.

Además, cuando se llamó a Stream.Read o Stream.ReadAsync con un búfer de longitud 0, la operación se realizaría correctamente de inmediato, a veces sin realizar una lectura de longitud cero en la secuencia que encapsula.

Comportamiento nuevo

A partir de .NET 6, cuando se llama a Stream.Read o Stream.ReadAsync en uno de los tipos de secuencia afectados con un búfer de longitud N, la operación se completa si:

  • Se ha leído al menos 1 byte de la secuencia, o
  • La secuencia subyacente devuelve 0 a raíz de una llamada a su lectura, lo que indica que no hay más datos disponibles.

Además, cuando se llama a Stream.Read o Stream.ReadAsync con un búfer de longitud 0, la operación se realiza correctamente una vez que una llamada con un búfer distinto de cero se realiza correctamente.

Cuando se llama a uno de los métodos de Read afectados, si la lectura puede satisfacer al menos un byte de la solicitud, independientemente de cuántos se solicitaron, devuelve tantos como pueda en ese momento.

Versión introducida

6.0

Motivo del cambio

Puede que una operación de lectura no devolviera las secuencias aunque los datos se hubieran leído correctamente. Esto significaba que no se podían usar fácilmente en ninguna situación de comunicación bidireccional en la que se usaran mensajes con un tamaño inferior al del búfer. Esto podría provocar interbloqueos: la aplicación no puede leer los datos de la secuencia necesaria para continuar con la operación. También podría provocar ralentizaciones arbitrarias, ya que el consumidor no puede procesar los datos disponibles mientras espera a que lleguen más datos.

Además, en aplicaciones de alta escalabilidad, es habitual usar lecturas de cero bytes como una manera de retrasar la asignación del búfer hasta que se necesite un búfer. Una aplicación puede emitir una lectura con un búfer vacío y, cuando se complete esa lectura, los datos estarán disponibles en breve para su consumo. A continuación, la aplicación puede volver a emitir la lectura, esta vez con un búfer para recibir los datos. Al delegar en la secuencia encapsulada si no hay datos ya descomprimidos o transformados disponibles, estas secuencias heredan ahora cualquier comportamiento de este tipo de secuencias que encapsulan.

En general, el código debe cumplir estas consideraciones:

  • No hacer suposiciones sobre una operación Read o ReadAsync de la secuencia que lea tanto como se solicitó. La llamada devuelve el número de bytes leídos, que puede ser inferior al solicitado. Si una aplicación depende de que el búfer se llene completamente antes de avanzar, puede realizar la lectura en un bucle para recuperar el comportamiento.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Esperar que una llamada a Read o ReadAsync de una secuencia no pueda completarse hasta que al menos un byte de datos esté disponible para su consumo, o hasta que la secuencia llegue a su fin, independientemente del número de bytes solicitados. Si una aplicación depende de que una lectura de cero bytes se complete inmediatamente sin esperar, puede comprobar la propia longitud del búfer y omitir la llamada por completo:

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

API afectadas