Letture parziali e zero byte in DeflateStream, GZipStream e CryptoStream

I Read() metodi e ReadAsync() in DeflateStream, GZipStreame CryptoStream potrebbero non restituire più il numero di byte richiesti.

In precedenza, DeflateStream, GZipStreame CryptoStream divergevano dal comportamento tipico Stream.Read e Stream.ReadAsync nei due modi seguenti, entrambi i quali questa modifica risolve:

  • L'operazione di lettura veniva completata solo quando il buffer passato all'operazione di lettura veniva completamente riempito o quando veniva raggiunta la fine del flusso.
  • Come flussi wrapper, non delegavano la funzionalità del buffer di lunghezza zero al flusso di cui eseguivano il wrapping.

Si consideri questo esempio che crea e comprime 150 byte casuali. Invia quindi i dati compressi un byte alla volta dal client al server e il server decomprime i dati chiamando Read e richiedendo tutti i 150 byte.

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

Nelle versioni precedenti di .NET e .NET Framework, l'output seguente mostra che Read è stato chiamato una sola volta. Anche se i dati erano disponibili per GZipStream la restituzione, Read è stato forzato l'attesa fino a quando non era disponibile il numero di byte richiesto.

Read: 150 bytes
Total received: 150 bytes

In .NET 6 e versioni successive, l'output seguente mostra che Read è stato chiamato più volte fino a quando non sono stati ricevuti tutti i dati richiesti. Anche se la chiamata alle Read richieste di 150 byte, ogni chiamata a Read è stata in grado di decomprimere correttamente alcuni byte (ovvero tutti i byte ricevuti in quel momento) da restituire e ha fatto:

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 precedente

Quando Stream.Read o Stream.ReadAsync è stato chiamato su uno dei tipi di flusso interessati con un buffer di lunghezza N, l'operazione non viene completata fino a quando:

  • N byte erano stati letti dal flusso o
  • Il flusso sottostante ha restituito 0 da una chiamata alla lettura, a indicare che non sono stati disponibili altri dati.

Quando inoltreStream.Read o Stream.ReadAsync veniva chiamato con un buffer di lunghezza 0, l'operazione aveva esito positivo immediatamente, a volte senza eseguire una lettura di lunghezza zero nel flusso di cui veniva eseguito il wrapping.

Nuovo comportamento

A partire da .NET 6, quando Stream.Read o Stream.ReadAsync viene chiamato su uno dei tipi di flusso interessati con un buffer di lunghezza N, l'operazione viene completata quando:

  • Almeno 1 byte è stato letto dal flusso o
  • Il flusso sottostante restituisce 0 da una chiamata alla lettura, a indicare che non sono disponibili altri dati.

Inoltre, quando Stream.Read o Stream.ReadAsync viene chiamato con un buffer di lunghezza 0, l'operazione ha esito positivo quando una chiamata con un buffer diverso da zero avrà esito positivo.

Quando si chiama uno dei metodi interessati Read , se la lettura può soddisfare almeno un byte della richiesta, indipendentemente dal numero di richieste, restituisce il maggior numero possibile in quel momento.

Versione di introduzione

6.0

Motivo della modifica

È possibile che i flussi non siano stati restituiti da un'operazione di lettura anche se i dati sono stati letti correttamente. Ciò significava che non potevano essere usati facilmente in qualsiasi situazione di comunicazione bidirezionale in cui venivano usati messaggi con dimensioni inferiori a quelle del buffer. Ciò poteva causare deadlock: l'applicazione non è in grado di leggere i dati dal flusso necessario per continuare l'operazione. Potrebbe anche causare rallentamenti arbitrari, con il consumer che non è in grado di elaborare i dati disponibili in attesa dell'arrivo di altri dati.

Nelle applicazioni a scalabilità elevata inoltre si usano spesso letture a zero byte come modo per ritardare l'allocazione del buffer fino a quando non è necessario un buffer. Un'applicazione può rilasciare una lettura con un buffer vuoto e, al termine di tale lettura, i dati dovrebbero essere presto disponibili per l'utilizzo. L'applicazione può quindi rilasciare nuovamente la lettura, questa volta con un buffer per ricevere i dati. Tramite la delega al flusso di cui è stato eseguito il wrapping se non sono disponibili dati già decompressi o trasformati, questi flussi ereditano ora qualsiasi comportamento dei flussi di cui eseguono il wrapping.

In generale, il codice deve:

  • Non fare ipotesi sul fatto che un'operazione Read o ReadAsync del flusso legga quanto richiesto. La chiamata restituisce il numero di byte letti, che potrebbero essere inferiori a quelli richiesti. Se un'applicazione dipende dal fatto che il buffer venga completamente riempito prima dell'avanzamento, può eseguire la lettura in un ciclo per recuperare il comportamento.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Si prevede che un flusso Read o ReadAsync una chiamata non venga completato fino a quando non è disponibile almeno un byte di dati per l'utilizzo (o il flusso raggiunge la fine), indipendentemente dal numero di byte richiesti. Se un'applicazione dipende dal completamento immediato di una lettura zero byte senza attesa, può controllare la lunghezza del buffer stesso e ignorare completamente la chiamata:

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

API interessate