Odczyty częściowe i zerowe w deflatestream, GZipStream i CryptoStream

Metody Read() i ReadAsync() w systemie DeflateStream, GZipStreami CryptoStream mogą nie zwracać już tak wielu bajtów, jak żądano.

Wcześniej, DeflateStream, GZipStreami CryptoStream różniły się od typowych i Stream.ReadAsync zachowań Stream.Read na następujące dwa sposoby, z których obie te zmiany dotyczą:

  • Nie ukończyli operacji odczytu, dopóki bufor przekazany do operacji odczytu nie został całkowicie wypełniony lub osiągnięto koniec strumienia.
  • Jako strumienie otoki nie delegowali funkcji buforu o zerowej długości do opakowującego strumienia.

Rozważmy ten przykład, który tworzy i kompresuje 150 bajtów losowych. Następnie wysyła skompresowane dane pojedynczo z klienta do serwera, a serwer dekompresuje dane przez wywołanie Read i zażądanie wszystkich 150 bajtów.

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

W poprzednich wersjach programu .NET i .NET Framework następujące dane wyjściowe pokazują, że Read było wywoływane tylko raz. Mimo że dane były dostępne do GZipStream zwrócenia, Read zostało wymuszone oczekiwanie na dostępność żądanej liczby bajtów.

Read: 150 bytes
Total received: 150 bytes

W programie .NET 6 i nowszych wersjach następujące dane wyjściowe pokazują, że Read było wywoływane wiele razy do momentu odebrania wszystkich żądanych danych. Mimo że wywołanie do żądań Read 150 bajtów, każde wywołanie Read metody było w stanie pomyślnie zdekompresować niektóre bajty (czyli wszystkie bajty, które zostały odebrane w tym czasie) do powrotu, i nie:

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

Stare zachowanie

Po Stream.Read wywołaniu lub Stream.ReadAsync wywołaniu jednego z typów strumieni, których dotyczy problem z buforem o długości N, operacja nie zostanie ukończona do momentu:

  • N bajty zostały odczytane ze strumienia lub
  • Strumień źródłowy zwrócił wartość 0 z wywołania do odczytu, co oznacza, że żadne dane nie były dostępne.

Ponadto, gdy Stream.Read operacja lub Stream.ReadAsync została wywołana z buforem o długości 0, operacja powiedzie się natychmiast, czasami bez wykonywania odczytu o zerowej długości na strumieniu, który opakowuje.

Nowe zachowanie

Począwszy od platformy .NET 6, gdy Stream.Read lub Stream.ReadAsync jest wywoływana na jednym z typów strumieni, których dotyczy problem, z buforem o długości N, operacja kończy się, gdy:

  • Co najmniej 1 bajt został odczytany ze strumienia lub
  • Strumień źródłowy zwraca wartość 0 z wywołania do odczytu, co oznacza, że nie są dostępne żadne dane.

Ponadto w przypadku Stream.Read wywołania lub Stream.ReadAsync wywołania z buforem o długości 0 operacja powiedzie się po pomyślnym wywołaniu z buforem niezerowym.

Jeśli wywołasz jedną z metod, których dotyczy Read problem, odczyt może spełnić co najmniej jeden bajt żądania, niezależnie od liczby żądań, zwraca tyle, ile może w tej chwili.

Wprowadzona wersja

6.0

Przyczyna wprowadzenia zmiany

Strumienie mogły nie zostać zwrócone z operacji odczytu, nawet jeśli dane zostały pomyślnie odczytane. Oznaczało to, że nie można ich łatwo używać w żadnej sytuacji komunikacji dwukierunkowej, w której używane są komunikaty mniejsze niż rozmiar buforu. Może to prowadzić do zakleszczenia: aplikacja nie może odczytać danych ze strumienia, który jest niezbędny do kontynuowania operacji. Może to również prowadzić do dowolnego spowolnienia, a konsument nie może przetworzyć dostępnych danych podczas oczekiwania na nadejście większej liczby danych.

Ponadto w wysoce skalowalnych aplikacjach często używa się odczytów bez bajtów jako sposobu opóźniania alokacji buforu do momentu, gdy będzie potrzebny bufor. Aplikacja może wydać odczyt z pustym buforem, a po zakończeniu odczytu dane powinny być wkrótce dostępne do korzystania. Następnie aplikacja może ponownie wydać odczyt, tym razem z buforem w celu odebrania danych. Delegowanie do opakowanego strumienia, jeśli nie jest już zdekompresowane lub przekształcone dane, te strumienie dziedziczą teraz wszelkie takie zachowanie strumieni, które opakowują.

Ogólnie rzecz biorąc, kod powinien:

  • Nie należy podejmować żadnych założeń dotyczących strumienia Read lub ReadAsync operacji odczytu tak samo, jak żądano. Wywołanie zwraca liczbę odczytanych bajtów, która może być mniejsza niż żądana wartość. Jeśli aplikacja zależy od całkowitego wypełnienia buforu przed postępem, może wykonać odczyt w pętli, aby odzyskać zachowanie.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Spodziewaj się, że strumień Read lub ReadAsync wywołanie może nie zostać ukończone, dopóki co najmniej bajt danych nie będzie dostępny do użycia (lub strumień osiągnie jego koniec), niezależnie od liczby żądanych bajtów. Jeśli aplikacja zależy od zakończenia odczytu zero bajtów natychmiast bez oczekiwania, może sprawdzić długość buforu i całkowicie pominąć wywołanie:

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

Dotyczy interfejsów API