Dela via


Del- och nollbyteläsningar i DeflateStream, GZipStream och CryptoStream

Metoderna Read() och ReadAsync()DeflateStream, GZipStreamoch CryptoStream kanske inte längre returnerar så många byte som begärdes.

DeflateStreamTidigare, , GZipStreamoch CryptoStream avvek från typiskt Stream.Read beteende och Stream.ReadAsync beteende på följande två sätt, som båda dessa ändringar adresserar:

  • De slutförde inte läsåtgärden förrän antingen bufferten som skickades till läsåtgärden var helt fylld eller så nåddes slutet av dataströmmen.
  • Som omslutningsströmmar delegerade de inte buffertfunktioner med noll längd till den ström de omsluter.

Tänk på det här exemplet som skapar och komprimerar 150 slumpmässiga byte. Den skickar sedan komprimerade data en byte i taget från klienten till servern, och servern dekomprimerar data genom att anropa Read och begära alla 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;
            }
        }
    }
}

I tidigare versioner av .NET och .NET Framework visas följande utdata som Read bara anropades en gång. Även om data var tillgängliga för GZipStream att returnera Read tvingades de vänta tills det begärda antalet byte var tillgängligt.

Read: 150 bytes
Total received: 150 bytes

I .NET 6 och senare versioner visas följande utdata som Read anropades flera gånger tills alla begärda data togs emot. Även om anropet till Read begär 150 byte, kunde varje anrop till Read dekomprimera några byte (det vill säga alla byte som hade tagits emot vid den tiden) för att returnera, och det gjorde det:

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

Gammalt beteende

När Stream.Read eller Stream.ReadAsync anropades på en av de berörda strömtyperna med en buffert av längd Nskulle åtgärden inte slutföras förrän:

  • N byte hade lästs från strömmen, eller
  • Den underliggande strömmen returnerade 0 från ett anrop till dess läsning, vilket indikerar att inga fler data var tillgängliga.

Dessutom, när Stream.Read eller Stream.ReadAsync anropades med en buffert av längd 0, skulle åtgärden lyckas omedelbart, ibland utan att göra en nolllängdsläsning på strömmen som den omsluter.

Nytt beteende

Från och med .NET 6, när Stream.Read eller Stream.ReadAsync anropas på någon av de berörda strömtyperna med en buffert av längd N, slutförs åtgärden när:

  • Minst 1 byte har lästs från strömmen, eller
  • Den underliggande strömmen returnerar 0 från ett anrop till dess läsning, vilket indikerar att inga fler data är tillgängliga.

Stream.Read När eller Stream.ReadAsync anropas med en buffert av längd 0 lyckas åtgärden också när ett anrop med en icke-nollbuffert lyckas.

När du anropar en av de berörda Read metoderna, om läsningen kan uppfylla minst en byte av begäran, oavsett hur många som begärdes, returneras så många som möjligt vid den tidpunkten.

Version introducerad

6,0

Orsak till ändringen

Strömmarna kanske inte har returnerats från en läsåtgärd även om data hade lästs. Detta innebar att de inte lätt kunde användas i någon dubbelriktad kommunikationssituation där meddelanden som var mindre än buffertstorleken användes. Detta kan leda till dödlägen: programmet kan inte läsa data från dataströmmen som krävs för att fortsätta åtgärden. Det kan också leda till godtyckliga avmattningar, där konsumenten inte kan bearbeta tillgängliga data i väntan på att fler data ska tas emot.

I mycket skalbara program är det också vanligt att använda läsningar med noll byte som ett sätt att fördröja buffertallokeringen tills en buffert behövs. Ett program kan utfärda en läsning med en tom buffert, och när läsningen är klar bör data snart vara tillgängliga att använda. Programmet kan sedan utfärda läsningen igen, den här gången med en buffert för att ta emot data. Genom att delegera till den omslutna strömmen om inga redan dekomprimerade eller transformerade data är tillgängliga ärver dessa strömmar nu alla sådana beteenden hos de strömmar som de omsluter.

I allmänhet bör kod:

  • Gör inga antaganden om en ström Read eller ReadAsync åtgärd som läser så mycket som begärdes. Anropet returnerar antalet lästa byte, vilket kan vara mindre än vad som begärdes. Om ett program är beroende av att bufferten fylls i helt innan det fortsätter kan det utföra läsningen i en loop för att återfå beteendet.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Förvänta dig att en ström Read eller ReadAsync ett anrop kanske inte slutförs förrän minst en byte data är tillgänglig för förbrukning (eller så når strömmen dess slut), oavsett hur många byte som begärdes. Om ett program är beroende av att en nollbyteläsning slutförs omedelbart utan att vänta kan det kontrollera själva buffertlängden och hoppa över anropet helt:

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

Berörda API:er

Se även