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

    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为具体的下载文件名,才可以在客户端知道你下载的文件名的名字,很重要。

    客户端代码

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

    直接下载文件的逻辑
    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);
                }
            }
        }
    }
    

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

    获取文件大小以及名字
    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 自带的头信息,直接获取即可。挺简单的。

    分片下载核心

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

    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;
        }
    }
    

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

    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 就是按照指定的大小分为进行请求,并返回所需的文件大小。

    实际代码
    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

    参考资料地址

    《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
    

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

  • 相关阅读:
    十六、CANdelaStudio深入-CDD与CDDT的差异(新建自定义服务)
    MyBatis-Plus乐观锁插件使用
    TCP/IP_第八章_静态路由_实验案例二
    Spring注解开发_Spring容器创建概述
    笔记wife_assistant
    Java对象内存模型
    【算法】动态规划
    有c语言的基础学习python【python基础详解】
    springboot使用WebSocket
    【Git】安装和常用命令的使用与讲解及项目搭建和团队开发的出现的问题并且给予解决
  • 原文地址:https://blog.csdn.net/kesshei/article/details/139381742