[C#] ASP.NET 档案下载(3) - 档案续传

相信大家都遇过,下载档案最后剩一点点时出错失败,然后要重新下载整个档案,体验一定非常差吧,不过如果程式有支援档案续传功能,可以从断线的地方继续,就不用浪费时间重新下载,今天要和大家介绍档案续传的方式。

档案续传需要的 Request Header:

Range: 告知伺服器要下载的档案範围,等号前为範围的单位,通常是bytes,等号后为範围的开始到结束,範围从 0 开始计算,四种格式如下:
Range: bytes=500-1000: 从 500 byte 开始,到 1000 byte 结束。
Range: bytes=500-: 从 500 byte 开始,到档案的最后结束。
Range: bytes=-500: 传回倒数 500 个 byte 的内容,这里和上面两种比较不同。
Range: bytes=500-1000, 1500-2000: 可以指定多个範围。

If-Range: 确保续传下载的过程中,这次下载的部分和上次下载的,这之间档案没有被变更过。
为非必要可以不加,但如果有 If-Range 就一定要配合 Range 使用,否则忽略 If-Range
可以使用 Last-Modified 时间验证或 ETag 标记验证,两者选其一,但不可以两者同时使用。
If-Range: Wed, 18 Oct 2017 07:30:00 GMT

档案续传需要的 Reponse Header:

Accept-Ranges: 请求範围的单位。 Accept-Ranges: bytesContent-Length: 请求的内容长度,不是整个档案的大小。 Content-Length: 1000Content-Range: 请求範围在整个档案中的位置。
Content-Range: bytes 500-1000/3000: 请求範围从 500 byte 开始,到 1000 byte 结束,整个档案大小 3000 byte。Last-Modified: 档案的最后修改时间,使用国际标準时间 GMT。
Last-Modified: Wed, 18 Oct 2017 07:30:00 GMTETag: 档案的唯一标记,用来验证档案是否变更,类似 MD5 的作用。
ETag: "33a64df5514abcd55bsb2a148795d9f6b989d4"

档案续传流程如下:

第一次下载档案,没有传送 Range 返回状态码 200,一般档案下载。第二次发现档案存在,所以会传送 Range 从断掉的地方继续,返回状态码 206,档案续传下载。如果 If-Range 验证发现档案有变动,返回状态码 200,重新下载档案。如果 Range 範围错误,返回状态码 416

程式码:

public void ProcessRequest(HttpContext context){    var index = context.Request.Params["index"];    var fileName = "test.txt";    var filePath = context.Server.MapPath("~/File/" + fileName);    //档案最后修改时间,格式 RFC1123    var lastModified = File.GetLastWriteTime(filePath).ToString("r");    using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))    {        var bufferSize = 102400;             //缓冲区大小 100KB        var buffer = new byte[bufferSize];   //缓冲区        var outputLength = fs.Length;        //档案大小        var readLength = 0;                  //每次读取大小        var sIndex = (long)0;                //开始读取位置        var eIndex = outputLength - 1;       //结束读取位置        var isPartialContent = false;        //是否为续传        if (context.Request.Headers["Range"] != null)        {            //判断档案最后修改时间是否和 If-Range 相同,相同代表档案没有被修改过            if (context.Request.Headers["If-Range"] == null ||                context.Request.Headers["If-Range"] == lastModified)            {                //取得要续传的範围                var range = context.Request.Headers["Range"];                var sRange = "";                var eRange = "";                                //验证续传的範围格式是否正确                var regex = new Regex(@"^[\s]*bytes=(([0-9]*)-([0-9]*))$");                if (regex.IsMatch(range))                {                    var match = regex.Match(range);                    sRange = match.Groups[2].Value;                    eRange = match.Groups[3].Value;                }                //50-100 : 从第 50 个 byte 开始到第 100 个 byte                if (!string.IsNullOrEmpty(sRange) &&                     !string.IsNullOrEmpty(eRange))                {                    sIndex = long.Parse(sRange);                    eIndex = long.Parse(eRange);                }                //50- : 从第 50 个 byte 开始到最后                if (!string.IsNullOrEmpty(sRange) &&                      string.IsNullOrEmpty(eRange))                {                    sIndex = long.Parse(sRange);                }                //-50 : 倒数 50 个 byte                if (string.IsNullOrEmpty(sRange) && !                    string.IsNullOrEmpty(eRange))                {                    sIndex = eIndex + 1 - long.Parse(sRange);                }                if (eIndex < 0 || sIndex > outputLength - 1 ||                     sIndex > eIndex)                {                    //Range 範围不符                    context.Response.StatusCode = 416;                    return;                }                //是否为档案续传                isPartialContent = true;            }        }        context.Response.Clear();        context.Response.AddHeader("Accept-Ranges", "bytes");        context.Response.AppendHeader("Last-Modified", lastModified);        context.Response.AddHeader(            "Content-Length", $"{eIndex - sIndex + 1}");        context.Response.AddHeader(            "Content-Range", $" bytes {sIndex}-{eIndex}/{outputLength}");        context.Response.ContentType = "application/octet-stream";        context.Response.AddHeader(            "content-disposition", "attachment; filename=" + fileName);        if (isPartialContent) context.Response.StatusCode = 206;        try        {            var currentIndex = sIndex;            fs.Seek(currentIndex, SeekOrigin.Begin);            while (currentIndex <= eIndex &&                    context.Response.IsClientConnected)            {                readLength =                     (int)Math.Min(eIndex - currentIndex + 1, bufferSize);                fs.Read(buffer, 0, readLength);                context.Response.OutputStream.Write(buffer, 0, readLength);                context.Response.Flush();                currentIndex = currentIndex + readLength;            }        }        catch (Exception)        {            //传输过程中如果客户端关闭连接,会抛出例外不处理        }        context.Response.End();    }}

结果:

使用续传软体(IDM)的测试结果,支援多点续传,中断后也可以恢复下载。
http://img2.58codes.com/2024/20106865to55bi8RsG.jpg

下载过程中,我按了暂停然后去修改档案,再恢复下载时有被 IDM 判断出来档案已经被变动,要求重新下载。
http://img2.58codes.com/2024/20106865HmkoOJ7Puu.jpg

结语:
我没有实作 Range 的第四种格式,指定多个範围,因为不常用到,而且会增加程式阅读的困难度,If-Range 的部分,我选用 Last-Modified 验证,因为不想去弄 MD5 偷懒一下 XD。

参考文章:
在ASP.NET中支持断点续传下载大文件(ZT) (转)
HTTP 断点下载功能实现
HTTP断点续传(分块传输)
MDN Web Docs

相关文章:
[C#] ASP.NET 档案下载(1) - POST 和 GET 触发档案下载
[C#] ASP.NET 档案下载(2) - 大型档案下载
[C#] ASP.NET 档案下载(3) - 档案续传


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章