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

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

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 里增加以下代码

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

里面有几个点

  1. 设置文件的类型可以用 FileExtensionContentTypeProvider 这个类来的,不用自己写(特殊类型它不支持)
  2. 设置EnableRangeProcessing 为 true ,才能实现分片下载
  3. 设置FileDownloadName为具体的下载文件名,才可以在客户端知道你下载的文件名的名字,很重要。

客户端代码

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

直接下载文件的逻辑
csharp 复制代码
public static async 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);
            }
        }
    }
}

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

获取文件大小以及名字
csharp 复制代码
public static async 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 (null, null);
}

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

分片下载核心

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

csharp 复制代码
public struct BytesRange
{
    public long Start { get; set; }
    public long End { get; set; }
    public long Length { get { return End - Start + 1; } }
    public override string ToString()
    {
        return $"{Start} {End} : {Length}";
    }
    public static 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;
    }
}

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

csharp 复制代码
public static async 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)
            {
                throw new Exception("数据不匹配!");
            }
            byte[] bytes = new byte[stream.Length];
            stream.Read(bytes, 0, bytes.Length);
            return bytes;
        }
    }
}

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

实际代码
csharp 复制代码
static HttpClient client = new HttpClient();
static object lockObj = new object();
static async 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

参考资料地址

csharp 复制代码
《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

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

相关推荐
颜淡慕潇30 分钟前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生1 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料2 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
monkey_meng3 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马3 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng3 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
paopaokaka_luck7 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风9 小时前
详解K8S--声明式API
后端
Peter_chq9 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml49 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍