Partilhar via


Leituras parciais e de zero byte em DeflateStream, GZipStream e CryptoStream

Os Read() métodos e ReadAsync() em DeflateStream, GZipStreame CryptoStream podem não retornar mais tantos bytes quanto foram solicitados.

Anteriormente, DeflateStream, , e CryptoStream divergiu do típico Stream.Read e Stream.ReadAsync do comportamento das duas maneiras a seguir, ambas abordadas GZipStreampor esta alteração:

  • Eles não concluíram a operação de leitura até que o buffer passado para a operação de leitura estivesse completamente preenchido ou o final do fluxo fosse atingido.
  • Como fluxos de wrapper, eles não delegaram a funcionalidade de buffer de comprimento zero ao fluxo que encapsulam.

Considere este exemplo que cria e compacta 150 bytes aleatórios. Em seguida, ele envia os dados compactados um byte de cada vez do cliente para o servidor, e o servidor descompacta os dados chamando Read e solicitando todos os 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;
            }
        }
    }
}

Em versões anteriores do .NET e do .NET Framework, a saída a seguir mostra que Read foi chamado apenas uma vez. Mesmo que os dados estivessem disponíveis para GZipStream retornar, Read foi forçado a esperar até que o número solicitado de bytes estivesse disponível.

Read: 150 bytes
Total received: 150 bytes

No .NET 6 e versões posteriores, a saída a seguir mostra que foi chamado várias vezes até que Read todos os dados solicitados fossem recebidos. Embora a chamada para Read solicite 150 bytes, cada chamada para Read foi capaz de descompactar com êxito alguns bytes (ou seja, todos os bytes que haviam sido recebidos naquele momento) para retornar, e fez:

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

Comportamento antigo

Quando Stream.Read ou Stream.ReadAsync foi chamado em um dos tipos de fluxo afetados com um buffer de comprimento N, a operação não seria concluída até:

  • N bytes foram lidos do fluxo, ou
  • O fluxo subjacente retornou 0 de uma chamada para sua leitura, indicando que não havia mais dados disponíveis.

Além disso, quando Stream.Read ou Stream.ReadAsync foi chamado com um buffer de comprimento 0, a operação seria bem-sucedida imediatamente, às vezes sem fazer uma leitura de comprimento zero no fluxo que envolve.

Novo comportamento

A partir do .NET 6, quando Stream.Read ou Stream.ReadAsync é chamado em um dos tipos de fluxo afetados com um buffer de comprimento N, a operação é concluída quando:

  • Pelo menos 1 byte foi lido do fluxo, ou
  • O fluxo subjacente retorna 0 de uma chamada para sua leitura, indicando que não há mais dados disponíveis.

Além disso, quando Stream.Read ou Stream.ReadAsync é chamado com um buffer de comprimento 0, a operação é bem-sucedida quando uma chamada com um buffer diferente de zero for bem-sucedida.

Quando você chama um dos métodos afetados Read , se a leitura puder satisfazer pelo menos um byte da solicitação, independentemente de quantos foram solicitados, ele retornará tantos quanto puder naquele momento.

Versão introduzida

6,0

Razão para a alteração

Os fluxos podem não ter retornado de uma operação de leitura, mesmo que os dados tenham sido lidos com êxito. Isso significava que eles não podiam ser prontamente usados em qualquer situação de comunicação bidirecional em que mensagens menores do que o tamanho do buffer estavam sendo usadas. Isso pode levar a impasses: o aplicativo não consegue ler os dados do fluxo necessários para continuar a operação. Pode também conduzir a abrandamentos arbitrários, com o consumidor incapaz de processar os dados disponíveis enquanto aguarda a chegada de mais dados.

Além disso, em aplicativos altamente escaláveis, é comum usar leituras de zero bytes como uma forma de atrasar a alocação de buffer até que um buffer seja necessário. Um aplicativo pode emitir uma leitura com um buffer vazio e, quando essa leitura for concluída, os dados logo estarão disponíveis para consumo. O aplicativo pode então emitir a leitura novamente, desta vez com um buffer para receber os dados. Ao delegar ao fluxo encapsulado se nenhum dado já descompactado ou transformado estiver disponível, esses fluxos agora herdam qualquer comportamento dos fluxos que encapsulam.

Em geral, o código deve:

  • Não faça suposições sobre um fluxo Read ou ReadAsync operação lendo tanto quanto foi solicitado. A chamada retorna o número de bytes lidos, que pode ser menor do que o solicitado. Se um aplicativo depender do buffer estar completamente preenchido antes de progredir, ele poderá executar a leitura em um loop para recuperar o comportamento.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Espere que um fluxo Read ou ReadAsync chamada não seja concluído até que pelo menos um byte de dados esteja disponível para consumo (ou o fluxo atinja seu fim), independentemente de quantos bytes foram solicitados. Se um aplicativo depender de uma leitura de zero byte concluída imediatamente sem esperar, ele pode verificar o comprimento do buffer em si e ignorar a chamada completamente:

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

APIs afetadas

Consulte também