其实断点续传的原理很简单,就是在Http的请求上和一般的下载有所不同而已。

打个比方,浏览器请求服务器上的一个文时,所发出的请求如下:假设服务器域名为www.yzjlb.net ,文件名为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了。

知道了以上原理,就可以进行断点续传的编程了。


原文地址:http://www.nowamagic.net/librarys/veda/detail/867

什么是断点续传

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

img

断点续传的原理

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

假设服务器域名为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 了。

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

img

断点续传总结

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

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。

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

项目地址:

ldqk/Masuit.Toolsgithub.com图标

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

它支持ETag标头以及Last-Modified标头。 它还支持以下前置条件标头:If-Match,If-None-Match,If-Modified-Since,If-Unmodified-Since,If-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");
}

http://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;
}

原文地址:https://zhuanlan.zhihu.com/p/258335390

发表评论

邮箱地址不会被公开。 必填项已用*标注