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

问题描述

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

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

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

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

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

为此,我将服务器设置为压缩数据库,然后返回 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()) 并返回 GZipStream.ToArray(),或者直接从文件中返回内存流。

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

var dbStream = client.OpenRead(downloadUrl);

这使得下载数据时客户端的内存使用量根本不会激增。

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

我已尽力避免 XY 问题,因此,如果我遗漏了任何内容,请告诉我,感谢您对此提供的帮助。谢谢

解决方法

由于我不知道您如何传输数据(NetworkStream byte[] 等),您也可以将压缩数据库直接作为 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;
  }
}

为了正确管理临时文件的范围,我在这里实现了一个“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(),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 count) => _fileStream.Write(buffer,count);

}

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

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