DeflateStream、GZipStream 和 CryptoStream 中的部分和零字节读取
DeflateStream、GZipStream 和 CryptoStream 上的 Read()
和 ReadAsync()
方法可能不再根据请求返回任意数量的字节数。
以前,派生自典型 Stream.Read 和 Stream.ReadAsync 的 DeflateStream、GZipStream 和 CryptoStream 有以下两种行为方式,这两种方式都是此更改所解决的:
- 只有传递给读取操作的缓冲区被完全填充或到达流的末尾,它们才能完成读取操作。
- 作为包装流,它们没有将零长度缓冲区功能委托给它们包装的流。
请考虑以下示例,它创建并压缩 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.Read
或 Stream.ReadAsync
时,操作将不会完成,直到:
- 从流中读取了
N
字节,或 - 基础流从对读取的调用中返回 0,表示没有更多可用数据。
另外,当使用长度为 0 的缓冲区调用 Stream.Read
或 Stream.ReadAsync
时,操作将立即成功,有时无需对其包装的流执行零长度读取。
新行为
从 .NET 6 开始,当对缓冲区长度为 N
的受影响流类型之一调用 Stream.Read
或 Stream.ReadAsync
时,操作将在以下情况下完成:
- 至少从流中读取了一个字节,或者
- 基础流从对读取的调用中返回 0,表示没有更多可用数据。
另外,当使用长度为 0 的缓冲区调用 Stream.Read
或 Stream.ReadAsync
时,一旦使用非零缓冲区的调用成功,操作就会成功。
调用受影响的 Read
方法之一时,如果读取操作可以满足请求的至少一个字节,则无论请求了多少字节,它都会返回在那一时刻它可以返回的任意多的次数。
引入的版本
6.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
调用可能没有完成,直到至少有一个字节的数据可供使用(或流到达其末尾)。 如果应用程序依赖于零字节读取立即完成而无需等待,则它可以检查缓冲区长度本身并完全跳过调用:int bytesRead = 0; if (!buffer.IsEmpty) { bytesRead = stream.Read(buffer); }
受影响的 API
- System.IO.Compression.DeflateStream.Read
- System.IO.Compression.DeflateStream.ReadAsync
- System.IO.Compression.DeflateStream.BeginRead(Byte[], Int32, Int32, AsyncCallback, Object)
- System.IO.Compression.GZipStream.Read
- System.IO.Compression.GZipStream.ReadAsync
- System.IO.Compression.GZipStream.BeginRead(Byte[], Int32, Int32, AsyncCallback, Object)
- System.Security.Cryptography.CryptoStream.Read
- System.Security.Cryptography.CryptoStream.ReadAsync
- System.Security.Cryptography.CryptoStream.BeginRead(Byte[], Int32, Int32, AsyncCallback, Object)