通过rest端点将压缩后的文件流式传输到客户端



我正在尝试动态压缩文件,但内存消耗很高。例如,压缩2.8 GB的总文件大小需要占用近5 GB的处理器内存。

[Route("zip")]    
public class ZipController : ControllerBase
{
private readonly HttpClient _httpClient;
public ZipController()
{
_httpClient = new HttpClient();
}
[HttpPost]
public async Task Zip([FromBody] JsonToZipInput input)
{        
Response.ContentType = "application/octet-stream";
Response.Headers.Add($"Content-Disposition", $"attachment; filename="{input.FileName}"");

using var zipArchive =
new ZipArchive(Response.BodyWriter.AsStream(), ZipArchiveMode.Create);
foreach (var (key, value) in input.FilePathsToUrls)
{
var zipEntry = zipArchive.CreateEntry(key, CompressionLevel.Optimal);
await using var zipStream = zipEntry.Open();
await using var stream = await _httpClient.GetStreamAsync(value);
await stream.CopyToAsync(zipStream);
}
}
}

我相信您应该能够调用Response.StartAsync:

[HttpPost]
public async Task Zip([FromBody] JsonToZipInput input)
{        
Response.ContentType = "application/octet-stream";
Response.Headers.Add($"Content-Disposition", $"attachment; filename="{input.FileName}"");
await Response.StartAsync();

using var zipArchive = new ZipArchive(Response.BodyWriter.AsStream(), ZipArchiveMode.Create);
foreach (var (key, value) in input.FilePathsToUrls)
{
var zipEntry = zipArchive.CreateEntry(key, CompressionLevel.Optimal);
await using var zipStream = zipEntry.Open();
await using var stream = await _httpClient.GetStreamAsync(value);
await stream.CopyToAsync(zipStream);
}
}

StartAsync应该启动正在发送的响应。请注意,一旦调用StartAsync,就不能修改响应标头状态代码

特别是,这意味着您的异常处理将有所不同。以前,异常(例如,来自请求中的错误URL(将导致异常状态代码(即500(。对于流式响应,StartAsync之后的任何异常都不能更改状态代码;已经发送了。相反,在客户端看来,连接会在没有完全关闭的情况下终止。更为复杂的是,在成功的情况下,这种行为对于web服务器来说并不罕见,因此客户端可能不会抱怨——他们最终只会得到截断(无效(的zip文件。(在流式zip的情况下,zip中的"文件表"是最后发送的,而不是第一个(。

所以,这应该有效,但我也建议:

  1. 确保您的异常日志记录适用于StartAsync之后的异常。无法向客户端返回错误详细信息,因此必须依赖日志记录
  2. 如果你控制了客户端,测试这个新的错误情况,看看你是否能检测到它。如果使用该客户端无法检测到它,那么确保你的代码验证zip

zip文件格式的任何内容都不需要为这个用例占用大量内存。所有文件都必须按顺序排列,最后有一个表描述zip结构和文件偏移量。这使得可以非常有效地流式传输,而根本不需要使用太多内存。

您可能不需要自己写这篇文章,ZipStreamer是您托管的一个微型服务,它正是这样做的(公开,我是作者(。它旨在解决您遇到的确切问题,方法是在字节进入时立即将其流式传输出去,并具有固定的缓冲区大小,以防止内存耗尽。它只需几MB的内存就可以并行传输数百个zip文件。

如果你需要将其作为申请的一部分,这里有一些建议。

  • 禁用压缩将节省CPU和一点内存。根据您的文件,压缩可能不是一个主要的好处(zip压缩后,jpegs实际上会变大(。如果你只是想将多个文件合并为一个文件,这将非常有帮助。但这并不能解释使用GB内存的原因
  • 确保你持有流内容的时间不会超过你需要的时间,看起来你是这样的。按照@Stephen对StartAsync的建议,尽快开始流式传输

最新更新