ASP.NET Core 中 HttpResponse.TransmitFile 的替代方法

Alternative method for HttpResponse.TransmitFile in ASP.NET Core

提问人:wangwang 提问时间:10/16/2023 最后编辑:marc_swangwang 更新时间:10/18/2023 访问量:55

问:

由于我想实现从我的 ASP.NET Core 后端下载大文件(> 4GB),因此许多文章指出,在 .NET Framework 中可以实现我的目标。HttpResponse.TransmitFile

但是,这似乎在 .NET Core 中不再可用。HttpResponse.TransmitFile

有谁知道 .NET Core 中的替代方案是什么?我无法告诉你我有多欣赏你的相关答案。HttpResponse.TransmitFile

asp.net asp.net-core .net-core httpresponse downloadFileAsync

评论

0赞 Panagiotis Kanavos 10/16/2023
它不可用,因为它不需要 - 您可以将文件作为流返回,而无需缓存。 在 WebForms 应用程序中用于将文件作为流发送,因为 WebForms 堆栈旨在重绘整个页面(或其中的一部分),而不是向调用方发送原始响应return File(myStream);TransmitFile

答:

-1赞 Jason Pan 10/16/2023 #1

您可以使用以下示例来实现该要求。有关更多详细信息,您可以查看博客 Streaming Zip on ASP.NET Core

private static HttpClient Client { get; } = new HttpClient();
[HttpGet]
public async Task<FileStreamResult> Get()
{
    // get your stream
    var stream = await Client.GetStreamAsync("https://raw.githubusercontent.com/StephenClearyExamples/AsyncDynamicZip/master/README.md");

    return new FileStreamResult(stream, new MediaTypeHeaderValue("text/plain"))
    {
        FileDownloadName = "README.md"
    };
}

对于 zip:

private static HttpClient Client { get; } = new HttpClient();
[HttpGet]
public IActionResult Get()
{
    var filenamesAndUrls = new Dictionary<string, string>
    {
        { "README.md", "https://raw.githubusercontent.com/StephenClearyExamples/AsyncDynamicZip/master/README.md" },
        { ".gitignore", "https://raw.githubusercontent.com/StephenClearyExamples/AsyncDynamicZip/master/.gitignore" },
    };

    return new FileCallbackResult(new MediaTypeHeaderValue("application/octet-stream"), async (outputStream, _) =>
    {
        using (var zipArchive = new ZipArchive(new WriteOnlyStreamWrapper(outputStream), ZipArchiveMode.Create))
        {
            foreach (var kvp in filenamesAndUrls)
            {
                var zipEntry = zipArchive.CreateEntry(kvp.Key);
                using (var zipStream = zipEntry.Open())
                using (var stream = await Client.GetStreamAsync(kvp.Value))
                    await stream.CopyToAsync(zipStream);
            }
        }
    })
    {
        FileDownloadName = "MyZipfile.zip"
    };
}

该解决方案具有我们以前的非核心解决方案的所有相同优势:

  1. 所有 I/O 都是异步的。任何时候都不会在 I/O 上阻塞任何线程。
  2. zip 文件未保存在内存中。它直接流式传输到客户端,即时压缩。
  3. 对于大文件,甚至不会将单个文件完全读入内存。每个文件都是动态单独压缩的。

评论

0赞 Panagiotis Kanavos 10/16/2023
问题不在于如何流式传输 ZIP,而在于如何返回流。做到这一点就足够了。return File(...)
0赞 Panagiotis Kanavos 10/16/2023
答案是 Stephen Cleary 文章中不连贯部分的精确副本,它没有解释发生了什么,使用缺失的类,例如,甚至包括:以前的解决方案是什么?WriteOnlyStreamWrapperThis solution has all the same advantages of our previous non-Core solution
0赞 wangwang 10/16/2023
Jason Pan,非常感谢您的回答,我在这里问一下是否像 Panagiotis Kanavos 所说的那样缺少一些课程?我不能用它来解决我的问题
0赞 wangwang 10/16/2023
还有Kanavos,谢谢你的评论,你说返回File(...)就足够了,但是我要传输的文件这么大,通常都在10GB以上,使用这种方式可能因为前端内存大小而不起作用,我该如何处理,提前致谢!
0赞 Jason Pan 10/18/2023
嗨,@PanagiotisKanavos,您能否分享有关该问题的更多详细信息。我们很荣幸能从您那里学到更多。谢谢^-^
0赞 Panagiotis Kanavos 10/18/2023 #2

我怀疑真正的问题不是找到替代方案(它是或而是处理请求范围,以便客户端可以下载大文件,如果中断,可以重试。TransmitFilereturn File(path)return File(stream)

幸运的是,自 ASP.NET Core 2.1 以来可用的 ControllerBase.File 方法和 Minimal API(以及其他方法)中使用的 Results.File 方法都已经支持这一点。默认情况下,范围处理处于关闭状态,但可以通过传递给参数来启用,例如:trueenableRangeProcessing

public class VideoController : Controller
{
    [HttpGet, Route("videos/video.mp4")]
    public IActionResult Index()
    {
        return File("d:\Videos\video.mp4", "video/mp4", true);
    }
}

更好的是,静态文件提供程序还支持开箱即用的范围(和响应压缩)。如果大文件位于特定文件夹中,则可以使用以下命令为它们提供服务:

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider("path\to\large\files"),  
    RequestPath = "/Videos"
});

如果要在自己的操作中使用响应压缩,则必须在 Web 服务器上或通过响应压缩中间件显式启用它:

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
});

var app = builder.Build();

app.UseResponseCompression();

从那时起,由客户端来检索特定块并重试它们。下载实用程序通常以块形式下载大文件,并自动重试失败的部分。Khalid Abuhakmeh 在一篇简短的博客文章中描述了该过程以及它如何与 ASP.NET Core 配合使用。

在 C# 中,HttpClient 可以请求文件的特定块,甚至可以使用标头同时下载它们,例如:Range

var req = new HttpRequestMessage 
{ 
    RequestUri = new Uri( url ) 
};
req.Headers.Range = new RangeHeaderValue( 0, 999 );

var resp = await client.SendAsync(req);

if (resp.IsSuccessStatusCode)
{
    using var tempFile=File.Create("chunk.001");
    await resp.Content.CopyToAsync(tempFile);
}

如果您有范围列表,则可以使用它来并行下载远程文件,并在以后合并块:

record MyRange(long Start,long End);

async Task DownloadChunkAsync(HttpClient client,Uri uri,MyRange range, CancellationToken ct)
{
    var req = new HttpRequestMessage 
    { 
        RequestUri = uri
    };
    req.Headers.Range = new RangeHeaderValue( range.Start, range.End);
    var resp = await client.SendAsync(req,ct);
    if (resp.IsSuccessStatusCode)
    {
        using var tempFile=File.Create($"chunk.{range.Start,5}");
        await resp.Content.CopyToAsync(tempFile);
    }
}

var ranges=CalculateRanges(...);
var uri=new Uri( url ) ;

//Concurrent downloads
await Parallel.ForEachAsync(ranges,(range,ct)=>{
    await DownloadChunkAsync(client,uri,range,ct);
}
// Combine the chunks
using(var finalStream=File.Create("finalFile.mp4"))
{
    foreach(var range in ranges)
    {
        using var chunkStream=File.OpenRead($"chunk.{range.Start,5}");
        chunkStream.CopyToAsync(filalStream);
    }
}