异步文件哈希和磁盘写入实际上是如何工作的?

5
我正在构建一个ASP.NET Core应用程序,需要处理大文件上传,最多达200GB。我的目标是将这些文件写入磁盘并同时捕获MD5哈希值。
我已经按照使用流进行大文件上传中的说明创建了自己的方法来识别来自HTTP客户端请求的文件流。一旦我找到了流,我将使用下面的代码来将其写入磁盘并创建MD5哈希值。
// removed the curly brackets from using statements for readability on Stack Overflow
var md5 = MD5.Create();
using (var targetStream = File.OpenWrite(pathAndFileName))
using (var cryptoStream = new CryptoStream(targetStream, md5, CryptoStreamMode.Write))
using (var sourceStream = fileNameAndStream.FileStream)
{
    await sourceStream.CopyToAsync(cryptoStream);
}

var hash = md5.Hash;
md5.Dispose();

很棒的是上述代码可以正常运行(文件已创建且哈希已生成)。但不太好的是我并没有完全理解它是如何工作的:
  • cryptoStream 是被复制到 targetStream 中然后写入吗?
  • cryptoStream 是在内存中保留字节还是按需读取?
  • cryptoStreamtargetStream 都是异步发生的吗?
  • 还是将数据异步复制到 cryptoStream,然后同步写入到 targetStream 中?
我很高兴它能正常工作,但由于我并没有完全理解它,所以我担心可能引入了一些问题。
1个回答

6
它的工作原理如下:
1)CopyToAsync 分配指定大小的字节缓冲区(或者使用问题中的重载来使用默认大小)。然后在源流上调用 ReadAsync 来填充该缓冲区,接着在目标流上调用 WriteAsync 将该缓冲区写入目标流。重复此过程直到所有数据被写入。因此,此操作在内存中保留小的字节数组(缓冲区)。如果源/目标流支持异步,则读取和写入是异步的。
2)以写模式运行的 CryptoStream 的工作方式如下:当您向其写入时,它会获取您写入的缓冲区(这就是上面讨论的相同缓冲区),并将其提供给您传递给它的 ICryptoTransform 实现(在本例中为 MD5)。变换可能需要按特定大小的块进行处理(由 ICryptoTransform.InputBlockSize 属性确定)。在这种情况下,CryptoStream 可能会稍微缓存您写入的数据,直到有特定大小的完整块。这不是问题,因为这些块通常非常小(比 CopyAsync 的合理缓冲区大小小得多)。然后,它将逐个将这些块传递给 ICryptoTransform.TransformBlock,并接收输出(另一个字节数组)。此过程是同步的,因为这里没有任何可以异步执行的内容。
3)通过 ICryptoTransform 转换后的块被异步地写入输出流(在本例中为 targetStream)(使用 WriteAsync)。因此,CryptoStream 的内存消耗也很小,并且与目标转换输入和输出块大小有关。
4)ICryptoTransformMD5 实现使用传递的块持续计算哈希值,因为该算法不要求完整数据同时存在以计算哈希值,它可以逐块计算。然后它会输出与输入相同的块,所以没有进行变换。这意味着对于 MD5,TransformBlock 只是返回输入本身,同时在内部更新哈希。
总之,回答您的问题:
- 加密流只保留了小缓冲区来缓冲数据,以便达到变换输入块大小的要求,并尽快将变换后的数据写入输出流。它不会保存整个数据的副本。 - 加密流本身没有进行 IO 工作,它只执行 CPU 绑定工作(变换),这是同步进行的,正如应该一样。但是当您写入加密流时,它会写入目标流,这是异步进行的。
附注 - 要真正利用异步文件 IO - 您需要使用 "asynchronous" 选项初始化文件流,例如:
```csharp using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true)) { await cryptoStream.CopyToAsync(fileStream); } ```
new FileStream(pathAndFileName, FileMode.Create, FileAccess.Write, FileShare.None,
               4096, FileOptions.Asynchronous)

否则,即使使用了WriteAsync,您对目标流的写入仍将是同步的。

非常感谢您提供如此详尽的答案。加密流是否需要相同的异步初始化呢? - ahsteele
1
@ahsteele 不,这种初始化方式只适用于 FileStream(因为底层的 Windows API 就是这样工作的)。此外,CryptoStream 并不执行异步 IO(甚至任何 IO),它只是将异步调用代理到目标流。 - Evk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接