1. 首页
  2. 文章列表
  3. 浅谈http断点续传的原理以及.NET代码实现,看似挺高端,其实很简单

最近参与公司的项目,说将来需要支持文件的断点续传,大家都感觉这似乎是比较高端又有点难以实现的功能,其实断点续传仔细研究后发现,其实蛮简单的,趁此机会也给大家科普下吧。

什么是断点续传

断点续传是一种结合本地存储和网络存储的技术,主要应用场景是用来解决在网络条件不佳或网络断开时的文件完全下载失败的问题。你想想当你在下载一个好几个GB的文件时,如果不支持断点续传,而你的网络状况又不是很好,在你下载到99%的时候,突然失败了,必须从头重新下载,你会是什么样的心情?而断点续传则解决了这样的问题,即使你在99%的时候下载失败了,那你还可以点击继续下载来补上这最后的1%,因为断点续传支持从文件上次中断的地方开始传送数据,而并非是从文件开头传送。这就是断点续传的定义。web服务器都默认可以断点续传,但我们很少知道他的原理,而且web服务器本身的文件下载有时候并不满足我们的业务场景,所以这种情况下往往需要我们开发者自己去实现下载功能,这时对我们开发者来讲就很有必要知道断点续传的原理了,下面就来看看小编的介绍吧。

断点续传的原理

其实断点续传的原理很简单,就是在 http 的请求头上和一般的请求有所不同而已。打个比方,浏览器请求服务器上的一个文件时,所发出的请求通过抓包后发现如下:

假设服务器域名为masuit.com,文件名为 down.zip。

GET /down.zip HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/msword, application/vnd.ms-powerpoint, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
Connection: Keep-Alive
服务器收到请求后,按要求寻找

请求的文件,提取文件的信息,然后返回给浏览器,返回信息如下:

200
Content-Length=106786028
Accept-Ranges=bytes
Date=Mon, 30 Apr 2001 12:56:11 GMT
ETag=W/“02ca57e173c11:95b”
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT

而所谓断点续传,也就是要从文件已经下载的地方开始继续下载。所以在客户端浏览器传给 Web 服务器的时候要多加一条信息 -- 从哪里开始。

如果我们在已经暂停的下载再点继续的话,传递请求信息给 Web 服务器,要求从 2000070 字节开始。通过抓包我们其实可以看到这样的请求头:

GET /down.zip HTTP/1.0
User-Agent: NetFox
RANGE: bytes=2000070-
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2

仔细看一下就会发现多了一行 RANGE: bytes=2000070-

这一行的意思就是告诉服务器下载 down.zip 这个文件从 2000070 字节开始传,前面的字节就不用传给我了。

服务器收到这个请求以后,返回的信息如下:

206
Content-Length=106786028
Content-Range=bytes 2000070-106786027/106786028
Date=Mon, 30 Apr 2001 12:55:20 GMT
ETag=W/“02ca57e173c11:95b”
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT

和前面服务器返回的信息比较一下,就会发现增加了一行:

Content-Range=bytes 2000070-106786027/106786028

同时返回的代码也改为 206 了,而不再是 200 了。

知道了以上原理,我们对断点续传的写代码就有点思路了。这个技术在现在的编程技术上我个人觉得也算不得什么,大家也应该都注意到我们平时下载文件时都是断点续传吧,甚至还可以调用第三方的多线程下载器,进行多线程并行传输。

断点续传总结

要实现断点续传的功能,通常都需要客户端记录下当前的下载进度,并在需要续传的时候通知服务端本次需要下载的内容片段。

HTTP1.1协议(RFC2616)中定义了断点续传相关的HTTP头 Range和Content-Range字段,一个最简单的断点续传实现大概如下:

1. 客户端下载一个1024K的文件,已经下载了其中512K

2. 网络中断,客户端请求续传,因此需要在HTTP头中申明本次需要续传的片段:Range:bytes=512000-,这个头通知服务端从文件的512K位置开始传输文件;

3. 服务端收到断点续传请求,根据请求头的Content-Range来决定从文件流的哪个位置上开始读,比如从文件的512K位置开始传输,并且在HTTP头中增加:Content-Range:bytes 512000-/1024000,并且此时服务端返回的HTTP状态码应该是206,而不是200。

但是在实际场景中,会出现一种情况,即在终端发起续传请求时,URL对应的文件内容在服务端已经发生变化,此时续传的数据肯定是错误的。如何解决这个问题了?显然此时我们需要有一个标识文件唯一性的方法。在RFC2616中也有相应的定义,比如实现Last-Modified来标识文件的最后修改时间,这样即可判断出续传文件时是否已经发生过改动。同时RFC2616中还定义有一个ETag的头,可以使用ETag头来放置文件的唯一标识,比如文件的MD5值。

终端在发起续传请求时应该在HTTP头中申明If-Match 或者If-Modified-Since 字段,帮助服务端判别文件变化。另外RFC2616中同时定义有一个If-Range头,终端如果在续传是使用If-Range。If-Range中的内容可以为最初收到的ETag头或者是Last-Modfied中的最后修改时候。服务端在收到续传请求时,通过If-Range中的内容进行校验,校验一致时返回206的续传回应,不一致时服务端则返回200回应,回应的内容为新的文件的全部数据。


最后,博主也基于这样的原理来实现了断线续传的FileResult。

基于Asp.Net MVC和Asp.Net Core实现的可断点续传的ResumeFileResult

项目地址:https://github.com/ldqk/Masuit.Tools

允许你在ASP.NET Core中通过MVC/WebAPI应用程序传输文件数据时使用断点续传以及多线程下载。

它允许提供ETag标题以及Last-Modified标题。 它还支持以下前置条件标题:If-MatchIf-None-MatchIf-Modified-SinceIf-Unmodified-SinceIf-Range

.NET Framework使用方式

nuget安装包:

PM>Install-Package Masuit.Tools

在你的控制器中,你可以像在FileResult一样使用它。

using Masuit.Tools.Mvc; 
using Masuit.Tools.Mvc.ResumeFileResult;
private readonly MimeMapper mimeMapper=new MimeMapper(); // 推荐使用依赖注入
public ActionResult ResumeFileResult()
{
    var path = Server.MapPath("~/Content/test.mp4");
    return new ResumeFileResult(path, mimeMapper.GetMimeFromPath(path), Request);
}
public ActionResult ResumeFile()
{
    return this.ResumeFile("~/Content/test.mp4", mimeMapper.GetMimeFromPath(path), "test.mp4");
}
public ActionResult ResumePhysicalFile()
{
    return this.ResumePhysicalFile(@"D:/test.mp4", mimeMapper.GetMimeFromPath(@"D:/test.mp4"), "test.mp4");
}

Asp.NET Core使用方式

nuget安装包:

PM>Install-Package Masuit.Tools.Core

和.NET Framework的方式有所不同,.NET Core在使用ResumeFileResults之前,必须在Startup.cs的ConfigureServices方法调用中配置服务:

using Masuit.Tools.AspNetCore.ResumeFileResults.DependencyInjection;
public void ConfigureServices(IServiceCollection services)
{
    services.AddResumeFileResult();
    ...
}

然后在你的控制器中,就可以像在FileResult一样的方式使用它了。

using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
private const string EntityTag = "\"TestFile\"";

private readonly IHostingEnvironment _hostingEnvironment;

private readonly DateTimeOffset _lastModified = new DateTimeOffset(2016, 1, 1, 0, 0, 0, TimeSpan.Zero);

public TestController(IHostingEnvironment hostingEnvironment)
{
    _hostingEnvironment = hostingEnvironment;
}

[HttpGet("content/{fileName}/{etag}")]
public IActionResult FileContent(bool fileName, bool etag)
{
    string webRoot = _hostingEnvironment.WebRootPath;
    var content = System.IO.File.ReadAllBytes(Path.Combine(webRoot, "TestFile.txt"));
    ResumeFileContentResult result = this.ResumeFile(content, "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
    result.LastModified = _lastModified;
    return result;
}

[HttpGet("content/{fileName}")]
public IActionResult FileContent(bool fileName)
{
    string webRoot = _hostingEnvironment.WebRootPath;
    var content = System.IO.File.ReadAllBytes(Path.Combine(webRoot, "TestFile.txt"));
    var result = new ResumeFileContentResult(content, "text/plain")
    {
        FileInlineName = "TestFile.txt",
        LastModified = _lastModified
    };
    return result;
}

[HttpHead("file")]
public IActionResult FileHead()
{
    ResumeVirtualFileResult result = this.ResumeFile("TestFile.txt", "text/plain", "TestFile.txt", EntityTag);
    result.LastModified = _lastModified;
    return result;
}

[HttpPut("file")]
public IActionResult FilePut()
{
    ResumeVirtualFileResult result = this.ResumeFile("TestFile.txt", "text/plain", "TestFile.txt", EntityTag);
    result.LastModified = _lastModified;
    return result;
}

[HttpGet("stream/{fileName}/{etag}")]
public IActionResult FileStream(bool fileName, bool etag)
{
    string webRoot = _hostingEnvironment.WebRootPath;
    FileStream stream = System.IO.File.OpenRead(Path.Combine(webRoot, "TestFile.txt"));

    ResumeFileStreamResult result = this.ResumeFile(stream, "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
    result.LastModified = _lastModified;
    return result;
}

[HttpGet("stream/{fileName}")]
public IActionResult FileStream(bool fileName)
{
    string webRoot = _hostingEnvironment.WebRootPath;
    FileStream stream = System.IO.File.OpenRead(Path.Combine(webRoot, "TestFile.txt"));

    var result = new ResumeFileStreamResult(stream, "text/plain")
    {
        FileInlineName = "TestFile.txt",
        LastModified = _lastModified
    };

    return result;
}

[HttpGet("physical/{fileName}/{etag}")]
public IActionResult PhysicalFile(bool fileName, bool etag)
{
    string webRoot = _hostingEnvironment.WebRootPath;

    ResumePhysicalFileResult result = this.ResumePhysicalFile(Path.Combine(webRoot, "TestFile.txt"), "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
    result.LastModified = _lastModified;
    return result;
}

[HttpGet("physical/{fileName}")]
public IActionResult PhysicalFile(bool fileName)
{
    string webRoot = _hostingEnvironment.WebRootPath;

    var result = new ResumePhysicalFileResult(Path.Combine(webRoot, "TestFile.txt"), "text/plain")
    {
        FileInlineName = "TestFile.txt",
        LastModified = _lastModified
    };

    return result;
}

[HttpGet("virtual/{fileName}/{etag}")]
public IActionResult VirtualFile(bool fileName, bool etag)
{
    ResumeVirtualFileResult result = this.ResumeFile("TestFile.txt", "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
    result.LastModified = _lastModified;
    return result;
}

以上示例将为您的数据提供“Content-Disposition:attachment”。 当没有提供fileName时,数据会作为“Content-Disposition:inline”提供。

另外,它还可以提供ETag和LastModified标题。

[HttpGet("virtual/{fileName}")]
public IActionResult VirtualFile(bool fileName)
{
    var result = new ResumeVirtualFileResult("TestFile.txt", "text/plain")
    {
        FileInlineName = "TestFile.txt",
        LastModified = _lastModified
    };
    return result;
}

以上示例和代码都可以在github仓库找到。

https://github.com/ldqk/Masuit.Tools

分享到:

版权声明:

本文仅用于学习、研究和交流目的,欢迎非商业性质转载。本文链接:https://masuit.com/1416

l  博主在此发文(包括但不限于汉字、拼音、拉丁字母)均为随意敲击键盘所出,用于检验本人电脑键盘录入、屏幕显示的机械、光电性能,并不代表本人局部或全部同意、支持或者反对观点。如需要详查请直接与键盘生产厂商法人代表联系。挖井挑水无水表,不会网购无快递。

l  博主的文章没有高度、深度和广度,只是凑字数。由于博主的水平不高(其实是个菜B),不足和错误之处在所难免,希望大家能够批评指出。

l  博主是利用读书、参考、引用、抄袭、复制和粘贴等多种方式打造成自己的纯镀 24k 文章,请原谅博主成为一个无耻的文档搬运工!

l  文章内容部分来源于互联网,不代表本人的任何立场;涉及到的软件来源于互联网,仅供个人下载使用,请勿用于商业用途,版权归软件开发者所有,下载后请于24小时内删除,如有真实需要请支持正版!因下载本站任何资源造成的损失,全部责任由使用者本人承担!如果你是版权方,认为本文内容对您的权益有所侵犯,请联系博主,并参照侵删联系的说明提交相应的证明材料,待博主进行严格地审查和背景调查后,情况属实的将在三天内将本文删除或修正。

相关推荐:

IIS通过URL重写将www的域名301到不带www的域名和强制使用https访问网站 谈一谈单例模式、静态类和线程内唯一对象有什么区别
用C#实现求有向图的最长路径和最短路径 深入理解C#中的IDisposable接口
谈一谈.NET中的并行编程(TPL)——多线程、异步、任务和并行计算 博主的又一开源项目——基于EntityFrameworkCore和Lucene.NET实现的全文搜索引擎库
.NET/java Office组件神器——Aspose.Total 17.x/18.x/19.x破解版+破解补丁下载 Autofac在.NET Core中的属性注入
浅谈MVC的Attribute路由,教你一步一步设计出漂亮的路由 由double类型判等引发的一点小思考

评论区: