当前位置: 欣欣网 > 码农

Asp.Net Core 实现分片下载的最简单方式

2024-06-02码农

技术群里的朋友遇到了这个问题,起初的原因是他对文件增加了一个属性配置

fileResult.EnableRangeProcessing = true;

这个属性我从未遇到过,然后,去F1查看这个属性的描述信息也依然少的可怜,只有简单的描述为(获取或设置为 启用范围处理 FileResult的值)。

范围处理,很容易理解,可能就是实现分片下载的关键,但是,是不是就很简单配置就可以了呢,我对此十分感兴趣,就开始查询它的实际信息。

EnableRangeProcessing 属性的含义

经查看Asp.net Core 源码可以看到,它实际上只是配置了一个头信息。

具体的方法内部,只是一个赋值

赋值的是什么呢,其实就是一个HTTP 头标记

所以,实际上,它只做了一件事情,那就是把请求的头变成以下的样子。

是的,它只是做了这一件事情,那就是把请求头 Accept-Ranges 的值设置为了 bytes 。

HTTP 协议头之 Accept-Ranges

经查寻,此头就是表示着 (Accept-Ranges HTTP 响应标头是服务器使用的一个标记,用于向客户端宣传其对文件下载的部分请求的支持。此字段的值表示可用于定义范围的单位。 )

当存在 Accept-Ranges 标头时,浏览器可能会尝试恢复中断的下载,而不是尝试重新启动下载。

所以,设置它就相当于设置了Asp.net Core 支持分片下载。

Asp.net Core 分片下载实际案例

主要分为服务端和客户端,服务端设置允许分片下载,客户端则需要按照分片进行多线程下载,我这里实际通过并行下载实现。 最后的验证,能运行它或者打开它即可(比如zip格式等)。

服务端代码

服务端异常的简单,新建一个Asp.net Core 项目即可,其他都默认。

我们要做的事情只是在 HomeController 里增加以下代码

private FileExtensionContentTypeProvider provider = new FileExtensionContentTypeProvider();
public IActionResult Down()
{
var file = @"H:\百度网盘\ubuntu.zip";
provider.TryGetContentType(file, outvar contentType);
var result = PhysicalFile(file, contentType);
result.EnableRangeProcessing = true;
result.FileDownloadName = Path.GetFileName(file);
return result;
}

里面有几个点

  1. 1. 设置文件的类型可以用 FileExtensionContentTypeProvider 这个类来的,不用自己写(特殊类型它不支持)

  2. 2. 设置EnableRangeProcessing 为 true ,才能实现分片下载

  3. 3. 设置FileDownloadName为具体的下载文件名,才可以在客户端知道你下载的文件名的名字,很重要。

客户端代码

客户端稍微复杂一些,得获取文件的大小和名字,然后,进行多线程下载,我这边会进行一个简单的模拟。

直接下载文件的逻辑

publicstaticasync Task DownUrl(string url)
{
var result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, url), HttpCompletionOption.ResponseHeadersRead);
if (result.IsSuccessStatusCode)
{
var filename = "temp.zip";
using (var contentStream = await result.Content.ReadAsStreamAsync())
{
using (var file = File.OpenWrite(filename))
{
await contentStream.CopyToAsync(file);
}
}
}
}

当然,如果不想分片下载,可以直接下载即可。

获取文件大小以及名字

publicstaticasync Task<(long? length, string filename)> GetFileLengthandName(string url)
{
var result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, url), HttpCompletionOption.ResponseHeadersRead);
if (result.IsSuccessStatusCode)
{
return (result.Content.Headers.ContentLength, result.Content.Headers.ContentDisposition.FileName);
}
return (nullnull);
}

主要就是通过httpclient 自带的头信息,直接获取即可。挺简单的。

分片下载核心

首先是对获取到的文件大小进行一个范围的分割

publicstruct BytesRange
{
publiclong Start { getset; }
publiclong End { getset; }
publiclong Length { get { return End - Start + 1; } }
publicoverridestringToString()
{
return$"{Start}{End} : {Length}";
}
publicstatic List<BytesRange> GetRanges(long length, long BufferSize = 1 * 1024 * 1024)
{
List<BytesRange> list = new List<BytesRange>();
long count = length / BufferSize;
long Lost = length - BufferSize * count;
if (Lost > 0)
{
list.Add(new BytesRange() { Start = count * BufferSize, End = count * BufferSize + Lost - 1 });
}
if (count > 0)
{
for (long i = 0; i < count; i++)
{
list.Add(new BytesRange() { Start = i * BufferSize, End = (i + 1) * BufferSize - 1 });
}
}
else
{
list.Add(new BytesRange() { Start = 0, End = Lost - 1 });
}
list.OrderByDescending(t => t.Start);
return list;
}
}

这样就可以获取具体的分片信息,具体每片的大小。

publicstaticasync Task<byte[]> GetBytesAsync(string url, BytesRange range)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Range"$"bytes={range.Start}-{range.End}");
using (HttpResponseMessage response = await client.SendAsync(request))
{
using (Stream stream = await response.Content.ReadAsStreamAsync())
{
if (range.Length != stream.Length)
{
thrownew Exception("数据不匹配!");
}
byte[] bytes = newbyte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
return bytes;
}
}
}

GetBytesAsync 就是按照指定的大小分为进行请求,并返回所需的文件大小。

实际代码

static HttpClient client = new HttpClient();
staticobject lockObj = newobject();
staticasync Task Main(string[] args)
{
var url = "http://localhost:5034/home/down";
Stopwatch stopwatch = Stopwatch.StartNew();
await DownUrl(url);
stopwatch.Stop();
Console.WriteLine($"单线程 直接下载耗时:{stopwatch.Elapsed.TotalSeconds}");
stopwatch.Restart();
(long? length, string filename) = await GetFileLengthandName(url);
if (length.HasValue)
{
var number = 10;
//获取分片大小,默认1M 缓存区,太小又太慢 设置成5M。
var list = BytesRange.GetRanges(length.Value, 5 * 1024 * 1024);
Console.WriteLine($"分片数:{list.Count} 每片大小:5MB 并发数:{number}");
var path = Path.Combine(AppContext.BaseDirectory, filename);
using (var write = File.OpenWrite(path))
{
write.SetLength(length.Value);
await write.FlushAsync();
// 并行下载,每秒默认10并发
Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = number }, range =>
{
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {range}");
var bytes = GetBytesAsync(url, range).Result;
lock (lockObj)
{
write.Seek(range.Start, SeekOrigin.Begin);
write.Write(bytes);
}
});
}
Console.WriteLine("下载完毕,请验证!");
}
else
{
Console.WriteLine("没有获取到下载文件的信息!");
}
stopwatch.Stop();
Console.WriteLine($"并发下载 耗时:{stopwatch.Elapsed.TotalSeconds}秒");

Console.ReadLine();
}

验证结果

先运行服务端

再运行下载客户端

看到结果后,有点差异

从结果上来看,直接下载是最快的,应该是少了分片的开销,而且服务都是在本机上,各种IO的限制基本上只有文件IO,带宽IO影响最小。

总结

虽然直接下载是最快的,但是,如果网络中断的话,基本得重新下载,所以,它的风险反而是最高的,而分片下载虽然有了分片的开销的,但是可以从断点处继续下载,风险反而最低,各有优势。

代码地址

https://github.com/kesshei/WebDown.git

https://gitee.com/kesshei/WebDown.git

参考资料地址

【enableRangeProcessing 的代码地址】
https://github.com/dotnet/aspnetcore/blob/53db4d97d7c77d13e20e58a98f104e88d6af6040/src/Shared/ResultsHelpers/FileResultHelper.cs#L141
【Accept-Ranges 】
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Ranges

一键三连呦!,感谢大佬的支持,您的支持就是我的动力!