DeflateStream、GZipStream 和 CryptoStream 中的部分和零位元組讀取

Read()GZipStream和 上的DeflateStreamReadAsync()CryptoStream 方法可能不再傳回所要求的位元元組數目。

先前、DeflateStreamGZipStream、 和 CryptoStream 會以下列兩種方式與一般Stream.ReadStream.ReadAsync行為不同,這兩種變更都會解決:

  • 在傳遞至讀取作業的緩衝區已完全填滿,或到達串流結尾後,才會完成讀取作業。
  • 若是包裝函式串流,不會將長度為零的緩衝區功能委派給所包裝的串流。

假設這個範例會建立和壓縮 150 個隨機位元組。 然後,它會一次將壓縮的數據從用戶端傳送到伺服器,伺服器會呼叫 Read 並要求全部 150 個字節來解壓縮數據。

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

在舊版 .NET 和 .NET Framework 中,下列輸出顯示 Read 只呼叫一次。 即使數據可供 GZipStream 傳回, Read 仍被迫等到要求的位元組數目可用為止。

Read: 150 bytes
Total received: 150 bytes

在 .NET 6 和更新版本中,下列輸出會顯示 Read 呼叫多次,直到 收到所有 要求的數據為止。 即使呼叫 Read 要求 150 個字節,但 每個 對 的呼叫 Read 都能夠成功解壓縮 一些 位元組(也就是當時已收到的所有位元組)以傳回,而且它確實:

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

舊的行為

在長度為緩衝區N的其中一個受影響的數據流類型上呼叫 或 Stream.ReadAsyncStream.Read,作業在下列情況下才會完成:

  • 已從串流讀取 N 個位元組,或
  • 基礎數據流從對讀取的呼叫傳回 0,表示沒有更多數據可供使用。

此外,以長度為 0 的緩衝區呼叫時 Stream.ReadStream.ReadAsync,作業會立即成功,有時不會對其包裝的串流執行長度為零的讀取。

新的行為

從 .NET 6 開始,在長度為 N 的其中一個受影響串流類型上呼叫 Stream.ReadStream.ReadAsync 時,作業會在下列情況發生時完成:

  • 已從數據流讀取至少 1 個字節,或
  • 基礎數據流會從對讀取的呼叫傳回 0,表示沒有可用的數據。

此外,使用長度為0的緩衝區呼叫 或 Stream.ReadAsyncStream.Read,一旦具有非零緩衝區的呼叫成功,作業就會成功。

當您呼叫其中一個受影響的 Read 方法時,如果讀取可以滿足至少一個字節的要求,無論要求多少, 它都會傳回當時的次數。

導入的版本

6.0

變更原因

即使已成功讀取資料,串流也可能尚未從讀取作業傳回。 這表示串流無法立即用於任何雙向通訊情境,因為訊息要小於所使用的緩衝區大小。 這可能會導致死結:應用程式無法從串流讀取繼續執行作業所需的資料。 這也可能導致任意速度變慢,取用者無法在等待更多數據送達時處理可用的數據。

此外,在擴充彈性高的應用程式中,通常會使用零位元組讀取的方式來延遲緩衝區配置,直到需要緩衝區為止。 應用程式可以發出緩衝區空白的讀取,而且當讀取完成時,資料應該很快就可供取用。 應用程式接著可以再次發出讀取,這次會使用緩衝區來接收資料。 如果沒有已經解壓縮或轉換的資料可用,這些串流現在可以透過委派到已包裝的串流,繼承其包裝之串流的任何這類行為。

一般而言,程式碼應該:

  • 不假設串流 ReadReadAsync 作業盡可能依要求數目讀取。 呼叫會傳回讀取的位元組數目,可能小於所要求的位元元組數目。 如果應用程式相依的緩衝區在進行之前已完全填滿,可以在迴圈中執行讀取,以重新取得行為。

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • 預期數據流 ReadReadAsync 呼叫可能尚未完成,直到至少有一位元組的數據可供取用(或數據流到達其結尾),不論要求多少個字節。 如果應用程式相依於立即完成且不等候的零位元組讀取,則可以自行檢查緩衝區長度,並完全略過呼叫:

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

受影響的 API