Pembacaan parsial dan zero-byte dalam DeflateStream, GZipStream, dan CryptoStream

Metode Read() dan ReadAsync() pada DeflateStream, GZipStream, dan CryptoStream mungkin tidak lagi mengembalikan byte sebanyak yang diminta.

Sebelumnya, DeflateStream, , dan CryptoStream berbeda dari khas Stream.Read dan Stream.ReadAsync perilaku dengan dua cara berikut, yang keduanya ditangani GZipStreamoleh perubahan ini:

  • Mereka tidak menyelesaikan operasi baca sampai buffer yang diteruskan ke operasi baca benar-benar terisi atau akhir aliran tercapai.
  • Sebagai aliran pembungkus, mereka tidak mendelegasikan fungsionalitas buffer panjang nol ke aliran yang mereka bungkus.

Pertimbangkan contoh ini yang membuat dan memadatkan 150 byte acak. Kemudian mengirim data terkompresi satu byte sekaligus dari klien ke server, dan server mendekompresi data dengan memanggil Read dan meminta semua 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;
            }
        }
    }
}

Dalam versi .NET dan .NET Framework sebelumnya, output berikut menunjukkan bahwa Read hanya dipanggil sekali. Meskipun data tersedia untuk GZipStream dikembalikan, Read terpaksa menunggu hingga jumlah byte yang diminta tersedia.

Read: 150 bytes
Total received: 150 bytes

Di .NET 6 dan versi yang lebih baru, output berikut menunjukkan bahwa Read dipanggil beberapa kali hingga semua data yang diminta diterima. Meskipun panggilan untuk Read meminta 150 byte, setiap panggilan ke Read berhasil mendekompresi beberapa byte (yaitu, semua byte yang telah diterima pada saat itu) untuk kembali, dan itu dilakukan:

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

Perilaku yang lama

Ketika Stream.Read atau Stream.ReadAsync dipanggil pada salah satu jenis aliran yang terpengaruh dengan buffer panjang N, operasi tidak akan selesai sampai:

  • N byte telah dibaca dari aliran, atau
  • Aliran yang mendasar mengembalikan 0 dari panggilan ke bacaannya, menunjukkan tidak ada lagi data yang tersedia.

Selain itu, ketika Stream.Read atau Stream.ReadAsync dipanggil dengan buffer panjang 0, operasi akan segera berhasil, kadang-kadang tanpa melakukan pembacaan panjang nol pada aliran yang dibungkusnya.

Perilaku yang baru

Mulai dari .NET 6, ketika Stream.Read atau Stream.ReadAsync dipanggil pada salah satu jenis aliran yang terpengaruh dengan buffer panjang N, operasi selesai ketika:

  • Setidaknya 1 byte telah dibaca dari aliran, atau
  • Aliran yang mendasar mengembalikan 0 dari panggilan ke bacaannya, menunjukkan tidak ada lagi data yang tersedia.

Selain itu, ketika Stream.Read atau Stream.ReadAsync dipanggil dengan buffer panjang 0, operasi berhasil setelah panggilan dengan buffer nonzero akan berhasil.

Ketika Anda memanggil salah satu metode yang Read terpengaruh, jika bacaan dapat memenuhi setidaknya satu byte permintaan, terlepas dari berapa banyak yang diminta, itu mengembalikan sebanyak mungkin pada saat itu.

Versi yang diperkenalkan

6.0

Alasan untuk berubah

Aliran mungkin belum kembali dari operasi baca meskipun data telah berhasil dibaca. Ini berarti mereka tidak dapat dengan mudah digunakan dalam situasi komunikasi dua arah di mana pesan yang lebih kecil dari ukuran buffer yang digunakan. Ini dapat menyebabkan kebuntuan: aplikasi tidak dapat membaca data dari aliran yang diperlukan untuk melanjutkan operasi. Ini juga dapat menyebabkan perlambatan arbitrer, dengan konsumen tidak dapat memproses data yang tersedia sambil menunggu lebih banyak data tiba.

Selain itu, dalam aplikasi yang sangat dapat diskalakan, umum untuk menggunakan pembacaan nol byte sebagai cara menunda alokasi buffer hingga buffer diperlukan. Aplikasi dapat mengeluarkan baca dengan buffer kosong, dan ketika pembacaan selesai, data harus segera tersedia untuk digunakan. Aplikasi kemudian dapat mengeluarkan baca lagi, kali ini dengan buffer untuk menerima data. Dengan mendelegasikan ke aliran yang dibungkus jika tidak ada data yang sudah didekompresi atau diubah yang tersedia, aliran ini sekarang mewarisi perilaku aliran yang mereka bungkus.

Secara umum, kode harus:

  • Tidak membuat asumsi tentang aliran Read atau ReadAsync pembacaan operasi sebanyak yang diminta. Panggilan mengembalikan jumlah byte yang dibaca, yang mungkin kurang dari apa yang diminta. Jika aplikasi tergantung pada buffer yang sepenuhnya diisi sebelum berkembang, aplikasi dapat melakukan bacaan dalam perulangan untuk mendapatkan kembali perilaku.

    int totalRead = 0;
    while (totalRead < buffer.Length)
    {
        int bytesRead = stream.Read(buffer.AsSpan(totalRead));
        if (bytesRead == 0) break;
        totalRead += bytesRead;
    }
    
  • Harapkan bahwa aliran Read atau ReadAsync panggilan mungkin tidak selesai sampai setidaknya byte data tersedia untuk dikonsumsi (atau aliran mencapai akhir), terlepas dari berapa banyak byte yang diminta. Jika aplikasi bergantung pada pembacaan nol byte yang segera selesai tanpa menunggu, aplikasi dapat memeriksa panjang buffer itu sendiri dan melewati panggilan sepenuhnya:

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

API yang Terpengaruh