相信大家都遇过,下载档案最后剩一点点时出错失败,然后要重新下载整个档案,体验一定非常差吧,不过如果程式有支援档案续传功能,可以从断线的地方继续,就不用浪费时间重新下载,今天要和大家介绍档案续传的方式。
档案续传需要的 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: bytes
Content-Length
: 请求的内容长度,不是整个档案的大小。 Content-Length: 1000
Content-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 GMT
ETag
: 档案的唯一标记,用来验证档案是否变更,类似 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)的测试结果,支援多点续传,中断后也可以恢复下载。
下载过程中,我按了暂停然后去修改档案,再恢复下载时有被 IDM 判断出来档案已经被变动,要求重新下载。
结语:
我没有实作 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) - 档案续传