Částečná a nulová bajtová čtení v deflateStream, GZipStream a CryptoStream

Metody Read() a ReadAsync() metody v DeflateStreamsystému , GZipStreama CryptoStream již nemusí vracet tolik bajtů, kolik bylo požadováno.

Dříve , DeflateStream, GZipStreama CryptoStream rozbíhají se od typického Stream.Read a Stream.ReadAsync chování následujícími dvěma způsoby, z nichž obě tyto změny řeší:

  • Nedokončili operaci čtení, dokud se nedokončila buď vyrovnávací paměť předaná operaci čtení, nebo se nedokončila konec datového proudu.
  • Jako streamy obálky nelegovali funkce vyrovnávací paměti nulové délky na datový proud, který zabalí.

Vezměte v úvahu tento příklad, který vytvoří a komprimuje 150 náhodných bajtů. Potom odešle komprimovaná data po jednom bajtu z klienta na server a server dekomprimuje data voláním Read a vyžádáním všech 150 bajtů.

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

V předchozích verzích rozhraní .NET a .NET Framework ukazuje následující výstup, který Read byl volána pouze jednou. I když byla data k dispozici k GZipStream vrácení, Read byla nucena počkat, až bude požadovaný počet bajtů k dispozici.

Read: 150 bytes
Total received: 150 bytes

Následující výstup v .NET 6 a novějších verzích ukazuje, že Read se volal několikrát, dokud se nepřijala všechna požadovaná data. I když volání požadavků na Read 150 bajtů bylo možné úspěšně Read dekomprimovat některé bajty (to znamená všechny bajty, které byly v té době přijaty), a to:

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

Staré chování

Kdy Stream.Read nebo Stream.ReadAsync byl volána u některého z ovlivněných typů datových proudů s vyrovnávací pamětí délky N, operace se nedokončí, dokud:

  • N bajty byly načteny z datového proudu, nebo
  • Základní datový proud vrátil hodnotu 0 z volání ke čtení, což znamená, že nebyla k dispozici žádná další data.

Také když Stream.Read nebo Stream.ReadAsync byl volána s vyrovnávací pamětí o délce 0, operace by byla okamžitě úspěšná, někdy bez nutnosti čtení nulové délky ve streamu, který zabalí.

Nové chování

Počínaje rozhraním .NET 6, kdy Stream.Read nebo Stream.ReadAsync je volána na jednom z ovlivněných typů datových proudů s vyrovnávací pamětí délky N, operace se dokončí v těchto případech:

  • Alespoň 1 bajt byl načten z datového proudu, nebo
  • Základní datový proud vrátí hodnotu 0 z volání ke čtení, což znamená, že nejsou k dispozici žádná další data.

Stream.Read Pokud nebo Stream.ReadAsync je volána s vyrovnávací pamětí o délce 0, operace proběhne úspěšně, jakmile bude volání s nenulovou vyrovnávací pamětí úspěšné.

Když zavoláte některou z ovlivněných Read metod, může čtení splňovat alespoň jeden bajt požadavku bez ohledu na to, kolik jich bylo požadováno, vrátí v tuto chvíli tolik, kolik může.

Zavedená verze

6.0

Důvod změny

Datové proudy se nemusí vrátit z operace čtení, i když se data úspěšně přečetla. To znamenalo, že se nedají snadno použít v žádné obousměrné komunikační situaci, kdy se zprávy menší než velikost vyrovnávací paměti používaly. To může vést k zablokování: Aplikace nemůže číst data z datového proudu, který je nezbytný k pokračování operace. Může také vést k libovolným zpomalením, kdy uživatel nemůže zpracovat dostupná data při čekání na doručení dalších dat.

Ve vysoce škálovatelných aplikacích je také běžné používat nulové bajty jako způsob zpoždění přidělení vyrovnávací paměti, dokud nebude potřeba vyrovnávací paměť. Aplikace může vydat čtení s prázdnou vyrovnávací pamětí a po dokončení čtení by měla být data brzy k dispozici pro využití. Aplikace pak může znovu vydat čtení, tentokrát s vyrovnávací pamětí pro příjem dat. Delegováním na zabalený datový proud, pokud už nejsou k dispozici žádná dekomprimovaná nebo transformovaná data, tyto datové proudy teď zdědí jakékoli takové chování datových proudů, které zabalí.

Obecně platí, že kód by měl:

  • Nepředpokládáme žádné předpoklady týkající se datového proudu Read nebo ReadAsync operace, která čte tolik, jak bylo požadováno. Volání vrátí počet přečtených bajtů, což může být menší než to, co bylo požadováno. Pokud aplikace závisí na úplném vyplnění vyrovnávací paměti před průběhem, může čtení provést ve smyčce, aby znovu získalo chování.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Počítejte s tím, že datový proud Read nebo ReadAsync volání nemusí být dokončené, dokud nebude k dispozici alespoň bajt dat pro spotřebu (nebo datový proud dosáhne konce), bez ohledu na to, kolik bajtů bylo požadováno. Pokud aplikace závisí na okamžitém dokončení nulového bajtového čtení bez čekání, může zkontrolovat samotnou délku vyrovnávací paměti a zcela přeskočit volání:

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

Ovlivněná rozhraní API