.Net Core 实现 自定义Http的Range输出实现断点续传或者分段下载

发布时间 2023-12-25 08:55:05作者: 码农小白修炼记

一、Http的Range请求头,结合相应头Accept-Ranges、Content-Range 可以实现如下功能:

1.断点续传。用于下载文件被中断后,继续下载。

2.大文件指定区块下载,如视频、音频拖动播放,直接定位到指定位置下载内容。可以避免每次都读取、传输整个文件,从而提升服务端性能。

3.大文件分包批量下载,再合并完整文件。可以提高下载速度。

 

二、Http的Range 相关说明:

1.规则要点

 请求头Range表示请求的数据起始位置。响应头Accept-Ranges:bytes 表示支持续传。响应头Content-Range表示返回的其实位置、总长度

请求头Range的数字,首尾都包含,长度是: end-begin+1

请求头Range的指定的长度,只是意向下载量,服务端不一定返回请求的长度。比如:bytes=0-, 表示希望下载整个文件,但服务端可以返回有限长度的数据块(有利于性能)。但数据其实位置start需按照请求。

2.在Http 响应请求是 200,表示响应结束,响应成功

Http 响应状态:206,表示响应中,响应部分数据,不会单开Socket链接。

 

三、在Asp.Net Core中实现自定义 Range 文件响应

1.封装处理的类:

复制代码
    public class DownloadRange
    {

        public HttpContext context = null;
        public HttpRequest request = null;
        public HttpResponse response = null;
        public DownloadRange(HttpContext ctx)
        {
            this.context = ctx;
            this.request = ctx.Request;
            this.response = ctx.Response;
        }
        private int HttpRangeSize = 1024 * 1024; //最大块长度 1M
        public void WriteFile(string file)
        {
            using (var fs = File.OpenRead(file))
            {
                WriteStream(fs);
            }
        }
        private  void WriteStream(Stream fs)
        {
            string range = request.Headers["Range"];
            range = range ?? "";
            range = range.Trim().ToLower();
            if (fs.CanSeek)
            {
                if (range.StartsWith("bytes=") && range.Contains("-"))
                {
                    //分段输出文件
                    int start = -1, end = -1;
                    var rgs = range.Substring(6).Split('-');
                    int.TryParse(rgs[0], out start);
                    int.TryParse(rgs[1], out end);
                    if (rgs[0] == "")
                    {
                        start = (int)fs.Length - end;
                        end = (int)fs.Length - 1;
                    }
                    if (rgs[1] == "")
                    {
                        end = (int)fs.Length - 1;
                    }
                    WriteRangeStream(fs, start, end);
                }
                else
                {
                    //输出整个文件
                    int l;
                    byte[] buffer = new byte[40960];
                    while ((l = fs.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        response.Body.Write(buffer, 0, l);
                    }
                }
            }
        }
        private  void WriteRangeStream(Stream fs, int start, int end)
        {
            using (fs)
            {
                int rangLen = end - start + 1;
                if (rangLen > 0)
                {
                    if (rangLen > HttpRangeSize)
                    {
                        rangLen = HttpRangeSize;
                        end = start + HttpRangeSize - 1;
                    }
                }
                else
                {
                    throw new Exception("Range error");
                }

                long size = fs.Length;
                //如果是整个文件返回200,否则返回206
                if (start == 0 && end + 1 >= size)
                {
                    response.StatusCode = 200;
                }
                else
                {
                    response.StatusCode = 206;
                }
                // response.Headers.Add("Accept-Ranges", "bytes");
                response.Headers.Add("Content-Range", $"bytes {start}-{end}/{size}");
                response.Headers.Add("Content-Length", rangLen.ToString());

                int readLen, total = 0;
                byte[] buffer = new byte[40960];
                //流定位到指定位置
                try
                {
                    fs.Seek(start, SeekOrigin.Begin);
                    while (total < rangLen && (readLen = fs.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        total += readLen;
                        if (total > rangLen)
                        {
                            readLen -= (total - rangLen);
                            total = rangLen;
                        }
                        response.Body.Write(buffer, 0, readLen);
                    }
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
        }
    }
复制代码

 

2.自定义中间件,处理文件输出

复制代码
    public class OuterImgMiddleware
    {
        public static string RootPath { get; set; } //配置文件读取绝对位置
        private readonly RequestDelegate _next;
        public OuterImgMiddleware(RequestDelegate next, Microsoft.AspNetCore.Hosting.IHostingEnvironment env)
        {
            _next = next;
        }
        public async Task Invoke(HttpContext context)
        {
            var path = context.Request.Path.ToString();
            var headersDictionary = context.Request.Headers;

            if (context.Request.Method == "GET" && !string.IsNullOrEmpty(path))
            {
                if (
                    path.Contains("/upload/logo")
                    || path.Contains("/upload/image")
                    || path.Contains("/upload/ueimage")
                    )
                {
                    var unauthorizedImagePath = RootPath + path;
                    FileInfo file = new FileInfo(unauthorizedImagePath);
                    if (file.Exists)
                    {
                        int length = path.LastIndexOf(".") - path.LastIndexOf("/") - 1;
                        context.Response.Headers["Etag"] = path.Substring(path.LastIndexOf("/") + 1, length);
                        context.Response.Headers["Last-Modified"] = new DateTime(2018).ToString("r");
                        context.Response.Headers["Accept-Ranges"] = "bytes";
                        //context.Response.Headers["Content-Length"] = file.Length.ToString();
                        if (path.EndsWith(".mp4"))
                        {
                            context.Response.ContentType = "video/mp4";
                            //分段读取内容
                            DownloadRange download = new DownloadRange(context);
                            download.WriteFile(unauthorizedImagePath);
                        }
                        else
                        {
                            context.Response.ContentType = "image/jpeg";
                            context.Response.Headers["Cache-Control"] = "public"; //指定客户端,服务器都处理缓存
                            await context.Response.SendFileAsync(unauthorizedImagePath);
                        }
                    }
                    return;
                }
            }

            await _next(context);
        }
    }
    public static class MvcExtensions
    {
        public static IApplicationBuilder UseOutImg(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<OuterImgMiddleware>();
        }
    }
复制代码

 

3. 在服务配置中 ConfigureServices,开启同步读取

          //启用允许同步读取
            services.Configure<KestrelServerOptions>(x => x.AllowSynchronousIO = true)
              .Configure<IISServerOptions>(x => x.AllowSynchronousIO = true);

4.在配置中 Configure,启用中间件