DeflateStream, GZipStream 및 CryptoStream의 부분 및 0바이트 읽기

ReadAsync() 메서드가 DeflateStreamRead() 요청 GZipStreamCryptoStream 된 만큼 더 이상 바이트를 반환하지 않을 수 있습니다.

DeflateStreamGZipStreamCryptoStream 이전에는 다음과 같은 두 가지 방법으로 일반적인 Stream.Read 동작과 Stream.ReadAsync 다른 두 가지 방법으로 이 변경 사항을 해결했습니다.

  • 읽기 작업에 전달된 버퍼가 완전히 채워지거나 스트림의 끝에 도달할 때까지 읽기 작업을 완료하지 않았습니다.
  • 래퍼 스트림으로, 래핑하는 스트림에 길이가 0인 버퍼 기능을 위임하지 않았습니다.

150개의 임의 바이트를 만들고 압축하는 이 예제를 생각해 보세요. 그런 다음 클라이언트에서 서버로 한 번에 하나씩 압축된 데이터를 보내고, 서버는 150바이트를 모두 호출 Read 하고 요청하여 데이터를 압축 해제합니다.

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 . 데이터를 반환 Read 할 수 GZipStream 있었음에도 불구하고 요청된 바이트 수를 사용할 수 있게 될 때까지 기다려야 했습니다.

Read: 150 bytes
Total received: 150 bytes

.NET 6 이상 버전에서 다음 출력은 요청된 모든 데이터를 받을 때까지 여러 번 호출된 것을 Read 보여줍니다. 150바이트를 요청하는 호출 Read 에도 불구하고 각 호출은 반환할 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.ReadAsync 호출된 경우 Stream.Read 작업은 다음까지 완료되지 않습니다.

  • 스트림에서 N 바이트를 읽은 경우 또는
  • 기본 스트림은 호출에서 읽기로 0을 반환하여 더 이상 데이터를 사용할 수 없음을 나타냅니다.

또한 길이가 0인 버퍼로 Stream.Read 또는 Stream.ReadAsync가 호출되면 래핑되는 스트림에서 길이가 0인 읽기를 수행하지 않고 작업이 즉시 성공합니다.

새 동작

.NET 6부터 버퍼 길이가 N인 영향을 받는 스트림 형식 중 하나에서 Stream.Read 또는 Stream.ReadAsync가 호출된 경우 작업은 다음과 같은 경우에 완료됩니다.

  • 스트림에서 1 바이트 이상을 읽었거나
  • 기본 스트림은 더 이상 사용할 수 없는 데이터를 나타내는 읽기 호출에서 0을 반환합니다.

Stream.Read 또한 길이가 0인 버퍼로 호출되거나 Stream.ReadAsync 호출되면 0이 아닌 버퍼를 사용한 호출이 성공하면 작업이 성공합니다.

영향을 받는 Read 메서드 중 하나를 호출할 때 요청된 수에 관계없이 읽기가 요청의 바이트를 하나 이상 충족할 수 있는 경우 해당 시점에 가능한 한 많은 값이 반환됩니다.

도입된 버전

6.0

변경 이유

데이터를 성공적으로 읽은 경우에도 스트림이 읽기 작업에서 반환되지 않았을 수 있습니다. 즉, 버퍼 크기보다 작은 메시지가 사용되는 양방향 통신 상황에서는 쉽게 사용할 수 없습니다. 이로 인해 애플리케이션이 작업을 계속하는 데 필요한 스트림에서 데이터를 읽을 수 없어 교착 상태가 될 수 있습니다. 또한 소비자가 더 많은 데이터가 도착할 때까지 기다리는 동안 사용 가능한 데이터를 처리할 수 없으므로 임의로 느려질 수 있습니다.

또한 확장성이 뛰어난 애플리케이션에서는 버퍼 할당을 버퍼가 필요할 때까지 지연하는 방법으로 0바이트 읽기를 사용하는 것이 일반적입니다. 애플리케이션은 빈 버퍼로 읽기를 실행할 수 있으며 읽기가 완료되면 데이터를 곧 사용할 수 있게 됩니다. 그러면 애플리케이션이 이번에는 데이터를 수신할 버퍼를 사용하여 읽기를 다시 실행할 수 있습니다. 이미 압축 해제되거나 변환된 데이터를 사용할 수 없는 경우 래핑된 스트림으로 위임함으로써 이러한 스트림은 래핑하는 스트림의 동작을 상속합니다.

일반적으로 코드는 다음을 수행해야 합니다.

  • 스트림 Read 또는 ReadAsync 작업 읽기에 대해 요청한 만큼의 값을 가정하지 않습니다. 호출은 읽은 바이트 수를 반환하며 이는 요청된 바이트보다 작을 수 있습니다. 애플리케이션이 진행하기 전에 완전히 채워지는 버퍼에 의존하는 경우 루프에서 읽기를 수행하여 동작을 다시 수행할 수 있습니다.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Read 요청된 바이트 수에 관계없이 적어도 바이트 이상의 데이터를 사용할 수 있거나 스트림이 끝에 도달할 때까지 스트림 또는 ReadAsync 호출이 완료되지 않을 수 있습니다. 애플리케이션이 대기하지 않고 즉시 완료되는 0바이트 읽기에 의존하는 경우 버퍼 길이 자체를 확인하고 호출을 완전히 건너뛸 수 있습니다.

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

영향을 받는 API