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

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

相关推荐
一弓虽7 分钟前
SpringBoot 学习
java·spring boot·后端·学习
姑苏洛言16 分钟前
扫码小程序实现仓库进销存管理中遇到的问题 setStorageSync 存储大小限制错误解决方案
前端·后端
光而不耀@lgy31 分钟前
C++初登门槛
linux·开发语言·网络·c++·后端
方圆想当图灵1 小时前
由 Mybatis 源码畅谈软件设计(七):SQL “染色” 拦截器实战
后端·mybatis·代码规范
毅航1 小时前
MyBatis 事务管理:一文掌握Mybatis事务管理核心逻辑
java·后端·mybatis
我的golang之路果然有问题1 小时前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
柏油2 小时前
MySql InnoDB 事务实现之 undo log 日志
数据库·后端·mysql
写bug写bug3 小时前
Java Streams 中的7个常见错误
java·后端
Luck小吕4 小时前
两天两夜!这个 GB28181 的坑让我差点卸载 VSCode
后端·网络协议
M1A14 小时前
全栈开发必备:Windows安装VS Code全流程
前端·后端·全栈