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

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

相关推荐
Asthenia04122 分钟前
理解词法分析与LEX:编译器的守门人
后端
uhakadotcom3 分钟前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
Asthenia04121 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide2 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04123 小时前
Spring 启动流程:比喻表达
后端