使用 WCF 通过流返回文件时如何减少内存使用量?

How to reduce memory usage when returning a file over a stream with WCF?

我每天有 1 个大文件和许多小文件发送到服务器。服务器在收到这些时解析并 creates/recreates/updates 一个 sqlite 数据库。客户端机器也需要这个数据库,并且可以请求它或请求更新。一切都通过局域网连接。

客户端机器需要数据库,因为它们没有可靠的互联网访问权限,因此无法使用云数据库。服务器也可能已关闭,因此向服务器询问单个查询是不可靠的。

大文件更新涉及数据库中的每一行,因为增量中可能遗漏了一些信息。因此,我们无法将大增量发送给客户端,我相信在客户端重新创建它们更有意义。

由于客户端机器很差,查询服务器的行并在这些机器上制作大的增量非常耗时,可能需要 2 个多小时。由于这种情况每天都会发生,因此 24 小时中有 2 小时的陈旧数据不是一种选择。

我们决定让客户端请求整个数据库,发生这种情况时,服务器会压缩并发送数据库,这只需要几分钟。

为此,我设置了服务器来压缩数据库,然后 return MemoryStream

var dbCopyPath = ".\db_copy.db";

using (var readFileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    Log("Compressing db copy...");
    using (var writeFileStream = new FileStream(dbCopyPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read))
    {
        using (var gzipStream = new GZipStream(writeFileStream, CompressionLevel.Optimal))
        {
            readFileStream.CopyTo(gzipStream);
        }
    }
}

return new MemoryStream(File.ReadAllBytes(dbCopyPath));

我尝试了一些其他方法,例如将 FileStream 写入 GZipStream(new MemoryStream()) 并 returning GZipStream.ToArray(),或者只是 returning直接来自文件的内存流。

我尝试过的所有选项的问题是它们都保留了大量内存(或者根本不起作用)。当我在压缩后只有 200mb 的文件时,我已经看到进程在 运行 时始终保留 600mb 的内存。如果进来的文件太大,这最终会开始给我内存不足的异常。在客户端,我可以像这样读取流:

var dbStream = client.OpenRead(downloadUrl);

这样一来,客户端在下载数据时内存使用量就不会激增。

我理想的解决方案是通过服务器将数据直接从文件流式传输到客户端。我不确定这是否可能,因为我已经用许多不同的流组合尝试过这个,但是如果有某种方法可以有一个惰性流,比如服务器不会加载部分流,直到客户端需要它们进行写入那将是理想的,尽管我再次不确定这是否可能甚至完全有意义。

我尽力避免 XY 问题,所以如果我遗漏了什么,请告诉我,我很感激任何帮助。谢谢

由于我不知道您如何传输数据(NetworkStream byte[],等等),您也可以 return 将您的压缩数据库直接作为 FileStream 而无需 MemoryStream:

private static Stream GetCompressedDbStream(string path)
{
  var tempFileStream = new TemporaryFileStream();

  try
  {
    using (var readFileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
      using (var gzipStream = new GZipStream(tempFileStream, CompressionLevel.Optimal, true))
      {
        readFileStream.CopyTo(gzipStream);
      }
    }

    tempFileStream.Seek(0, SeekOrigin.Begin);
    return tempFileStream;
  }
  catch (Exception)
  {
    // Log to console or alert user.
    tempFileStream.Dispose();
    throw;
  }
}

为了正确管理临时文件的范围,我在这里实现了 class 'TemporaryFileStream'。这将在处理流后立即删除临时文件:

public class TemporaryFileStream : Stream, IDisposable
{

  private readonly FileStream _fileStream;
  private bool _disposedValue;

  public override bool CanRead => _fileStream.CanRead;

  public override bool CanSeek => _fileStream.CanSeek;

  public override bool CanWrite => _fileStream.CanWrite;

  public override long Length => _fileStream.Length;

  public override long Position
  {
    get => _fileStream.Position;
    set => _fileStream.Position = value;
  }

  public TemporaryFileStream()
  {
    _fileStream = new FileStream(Path.GetTempFileName(), FileMode.Open, FileAccess.ReadWrite);
    new FileInfo(_fileStream.Name).Attributes = FileAttributes.Temporary;
  }

  protected virtual void Dispose(bool disposing)
  {
    if (!_disposedValue)
    {
      if (disposing)
      {
        _fileStream.Dispose();
        File.Delete(_fileStream.Name);
      }

      _disposedValue = true;
    }
  }

  public void Dispose()
  {
    Dispose(disposing: true);
    GC.SuppressFinalize(this);
  }

  public override void Flush() => _fileStream.Flush();
  public override int Read(byte[] buffer, int offset, int count) => _fileStream.Read(buffer, offset, count);
  public override long Seek(long offset, SeekOrigin origin) => _fileStream.Seek(offset, origin);
  public override void SetLength(long value) => _fileStream.SetLength(value);
  public override void Write(byte[] buffer, int offset, int count) => _fileStream.Write(buffer, offset, count);

}

然后您可以使用简单的 CopyTo 或 Read 以有效地传输数据:

using var stream = GetCompressedDbStream(@"DbPath");
// CopyTo ...