DeflateStream、GZipStream、CryptoStream での部分的な読み取りとゼロバイトの読み取り

DeflateStreamGZipStreamCryptoStream 上の Read()ReadAsync() メソッドは、要求されたバイト数を返さなくなる可能性があります。

以前の DeflateStreamGZipStreamCryptoStream は、次の 2 つの点で一般的な Stream.ReadStream.ReadAsync の動作と異なっていましたが、どちらもこの変更で対処されています。

  • 読み取り操作に渡されたバッファーが完全に満たされるまで、またはストリームの終了に達するまで、読み取り操作は完了しませんでした。
  • ラッパー ストリームとして、それらがラップするストリームに長さゼロのバッファー機能をデリゲートしませんでした。

ランダムに 150 バイトを作成して圧縮するこの例を考えてみましょう。 次に、圧縮されたデータをクライアントからサーバーに 1 バイトずつ送信し、サーバーで 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 が 1 回だけ呼び出されたことを示しています。 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 のバッファーを使って、影響を受けるストリームの種類の 1 つで Stream.Read または Stream.ReadAsync を呼び出した場合、次のいずれかの状態になるまで操作は完了しませんでした。

  • N バイトがストリームから読み取られた。
  • 読み取りの呼び出しによって基になるストリームが、利用可能なデータがもう存在しないことを示す 0 を返した。

また、Stream.Read または Stream.ReadAsync が長さ 0 のバッファーを指定して呼び出された場合、操作はすぐに成功し、ラップしているストリームに対して長さゼロの読み取りが行われないことがありました。

新しい動作

.NET 6 以降では、長さ N のバッファーを使用する、影響を受けるストリーム型の 1 つで Stream.Read または Stream.ReadAsync が呼び出された場合、操作は次のいずれかの状態になると完了します。

  • ストリームから少なくとも 1 バイトが読み取られた。
  • 読み取りの呼び出しによって基になるストリームが、利用可能なデータがもう存在しないことを示す 0 を返す。

また、Stream.Read または Stream.ReadAsync が長さ 0 のバッファーを指定して呼び出された場合、0 ではないバッファーでの呼び出しが成功すると、操作は成功します。

影響を受ける Read メソッドの 1 つを呼び出すと、読み取りで要求を 1 バイトでも満たすことができる場合は、要求された数に関係なく、"その時点でできる限りの数を返します"。

導入されたバージョン

6.0

変更理由

データが正常に読み取られた場合でも、ストリームが読み取り操作から戻らない可能性がありました。 これは、バッファー サイズより小さいメッセージが使用される双方向通信の状況では、それらを簡単に使用できなかったことを意味します。 この結果、デッドロックが発生する可能性があります。アプリケーションで、操作を続けるために必要なストリームからデータを読み取ることができません。 また、さらにデータが到着するのを待機している間、コンシューマーは使用可能なデータを処理できないため、速度が低下する可能性もありました。

また、拡張性の高いアプリケーションでは、バッファーが必要になるまでバッファーの割り当てを遅らせる方法として、ゼロバイトの読み取りを使用するのが一般的です。 アプリケーションでは、空のバッファーで読み取りを発行でき、読み取りが完了すると、すぐにデータを使用できる必要があります。 その後、アプリケーションで、今度はデータを受信するバッファーを使用して、もう一度読み取りを発行できます。 すでに圧縮解除または変換されたデータがない場合は、ラップされたストリームに委任することにより、これらのストリームで、ラップしているストリームのそのような動作が継承されるようになります。

一般に、コードでは次のようにする必要があります。

  • ストリームの Read または ReadAsync 操作で要求した量のデータが読み取られたものと想定しないようにします。 呼び出しから返される読み取られたバイト数は、要求した量より少ない可能性があります。 バッファーが完全に埋まってから処理を続行する必要があるアプリケーションの場合は、ループで読み取りを実行することによって動作を実現できます。

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • 要求したバイト数に関係なく、少なくとも 1 バイトのデータが使用可能になるまで (またはストリームが終了するまで)、ストリームの Read または ReadAsync の呼び出しが完了しない可能性があるものと想定します。 ゼロ バイトの読み取りが待機なしですぐに完了する必要があるアプリケーションの場合は、バッファーの長さ自体を調べて、呼び出しを完全にスキップできます。

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

影響を受ける API