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

How does asynchronous file hash and disk write actually work?

我正在构建一个 ASP.NET 核心应用程序,它需要处理大文件上传——多达 200GB。我的目标是将这些文件写入磁盘并同时捕获一个 MD5 Hash。

我已经完成并创建了自己的方法来识别来自 HTTP 客户端请求的文件流,如 Uploading large files with streaming 中所述。找到流后,我将使用以下代码写入磁盘并创建 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();

很棒的是上面的工作(文件创建和散列生成)。不太棒的是我不完全理解它是如何工作的:

我很高兴这个工作,但在没有完全理解它的情况下,我担心我引入了一些邪恶的东西。

它是这样工作的:

1) CopyToAsync 分配指定大小的字节缓冲区(或者如果您使用有问题的重载,则使用默认大小)。然后它在源流上调用 ReadAsync 来填充该缓冲区,然后在目标流上调用 WriteAsync 将该缓冲区写入目标流。重复直到写入所有数据。所以这个操作在内存中保存小字节数组(缓冲区)。读写是异步的(如果 source\target 流支持)。

2) CryptoStream 在写入模式下是这样工作的:当你写入它时,它需要你写入的缓冲区(这与上面讨论的缓冲区相同)并将它提供给你传递的 ICryptoTransform 实现到它(在这种情况下 - MD5)。转换可能需要以特定大小的块进行处理(由 ICryptoTransform.InputBlockSize 属性 确定)。在这种情况下,CryptoStream 可能会缓存您写入的数据,直到有特定大小的完整块。这不是问题,因为这些块通常非常小(远小于 CopyAsync 的合理缓冲区大小)。然后它将这些块一个一个地传递给 ICryptoTransform.TransformBlock,并接收输出(另一个字节数组)。这个过程是同步的,因为这里没有任何东西可以异步。

3) 在块被 ICryptoTransform 转换后 - 此块被异步写入输出流(在本例中为 targetStream)(使用 WriteAsync)。所以CryptoStream的内存消耗也很小,和target transform input and output block size有关。

4) MD5 ICryptoTransform 的实现使用传递的块来连续计算哈希值,因为该算法不需要立即存在完整数据来计算哈希值,它可以通过块计算它堵塞。然后它输出与输入时接收到的完全相同的块,因此没有进行任何转换。这意味着 TransformBlock 对于 MD5 只是 returns 输入,同时在内部更新散列。

总结并回答您的问题:

  • crypto stream 只持有小缓冲区来缓冲数据,直到转换输入块大小,它尽快将转换后的数据直接写入输出流。它不包含整个数据的副本。
  • 加密流本身没有 IO 工作发生,它只执行 CPU 绑定工作(转换),这是同步发生的,它应该。但是当你写入加密流时——它写入目标流——并且这确实是异步发生的。

旁注 - 要真正利用异步文件 IO - 您需要使用 "asynchronous" 选项初始化文件流,例如:

new FileStream(pathAndFileName, FileMode.Create, FileAccess.Write, FileShare.None,
               4096, FileOptions.Asynchronous)

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