断点续传基本原理

HTTP协议中与断点续传相关的HTTP头为:Range和Content-Range标头,断点续传实现流程: 
1)客户端请求下载一个文件,文件的总长度为n;已经下载了一部分文件,长度为m(单位KB) 
2) 客户端主动暂停下载或网络中断,客户端请求继续下载,HTTP请求标头设置为:

Range:bytes=m-  
3) 服务端收到断点续传请求,从文件的m位置开始传输,HTTP响应头设置为: 
Content-Range:bytes m-n/n,服务端返回的HTTP状态码是206。

HTTP请求与响应实例(使用wireshark抓取HTTP报文):

第一次请求的请求头:

暂停后,再次请求的请求头:

某次暂停后再次发起的请求和返回的响应头:

Web API提供了对上述标头的支持:

HttpRequestMessage.Headers.Range:设置请求头的Range标头,Range的类型是RangeHeaderValue,RangeHeaderValue有一个类型为ICollection<RangeItemHeaderValue>的属性Ranges,RangeItemHeaderValue有两个类型为long的属性From和To,这两个属性分别表达了请求数据的开始和结束位置。

HttpResponseHeaders.AcceptRanges属性设置Accept-Ranges标头,HttpResponseMessage.Content属性的Headers属性设置响应内容标头,q其类型为HttpContentHeaders,HttpContentHeaders.ContentDisposition属性设置Content-Disposition标头值,ContentDisposition属性类型为ContentDispositionHeaderValue,可使用ContentDispositionHeaderValue.FileName设置文件名。HttpContentHeaders.ContentTypes属性设置Content-Type标头。HttpContentHeaders.ContentRangese设置响应的消息体的数据范围。

二、示例

Get请求,调用url:http://localhost/webApi_test/api/download?filecode=KBase[V11.0%2020140828]&filetype=exe

1使用StreamContent向消息体中写数据

使用StreamContent适合将磁盘文件流直接“挂”到响应流,对于那种数据源是另一个服务,或者数据来自本地磁盘,但是无法将文件流直接挂到响应流(可能对文件要进行编码转换或加密解密等操作)的情形不适合使用StreamContent,因为直接将流“挂”到响应流,可以实现对服务器缓存的控制,已实现在服务器和客户端之间建立一个管道,一点一点地,源源不断将数据传送给客户端,而不必一次将数据都读入内存,这样极大的节省了内存,同时也使得传输大文件成为了可能。

控制器及操作:

public class DownloadController : ApiController
{
public HttpResponseMessage Get([FromUri]Input input)
{
string filePath = string.Format(@"D:\工具软件\{0}.{1}", input.FileCode, input.FileType);
string fileName = Path.GetFileName(filePath);
DiskFileProvider fileProvider = new DiskFileProvider(filePath);
long entireLength = fileProvider.GetLength();
ContentInfo contentInfo = GetContentInfoFromRequest(entireLength, this.Request);
Stream partialStream = fileProvider.GetPartialStream(contentInfo.From);
HttpContent content = new StreamContent(partialStream, );
return SetResponse(content, contentInfo, entireLength,fileName);
}
}

获得请求信息,包括:文件的总长度,请求数据的额范围,是否支持多个范围。

        private ContentInfo GetContentInfoFromRequest(long entireLength, HttpRequestMessage request)
{
var contentInfo = new ContentInfo
{
From = ,
To = entireLength - ,
IsPartial = false,
Length = entireLength
};
RangeHeaderValue rangeHeader = request.Headers.Range;
if (rangeHeader != null && rangeHeader.Ranges.Count != )
{
//仅支持一个range
if (rangeHeader.Ranges.Count > )
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
RangeItemHeaderValue range = rangeHeader.Ranges.First();
if (range.From.HasValue && range.From < || range.To.HasValue && range.To > entireLength - )
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
} contentInfo.From = range.From ?? ;
contentInfo.To = range.To ?? entireLength - ;
contentInfo.IsPartial = true;
contentInfo.Length = entireLength; if (range.From.HasValue && range.To.HasValue)
{
contentInfo.Length = range.To.Value - range.From.Value + ;
}
else if (range.From.HasValue)
{
contentInfo.Length = entireLength - range.From.Value;
}
else if (range.To.HasValue)
{
contentInfo.Length = range.To.Value + ;
}
}
return contentInfo;
}

设置响应,对上述介绍的响应内容标头字段进行合理的设置。

       private HttpResponseMessage SetResponse(HttpContent content, ContentInfo contentInfo, long entireLength,string fileName)
{
HttpResponseMessage response = new HttpResponseMessage();
//设置Accept-Ranges:bytes
response.Headers.AcceptRanges.Add("bytes");
//设置传输部分数据时,如果成功,那么状态码为206
response.StatusCode = contentInfo.IsPartial ? HttpStatusCode.PartialContent : HttpStatusCode.OK;
//设置响应内容
response.Content = content;
//Content-Disposition设置为attachment,指示浏览器客户端弹出下载框。
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
//设置下载文件的文件名
response.Content.Headers.ContentDisposition.FileName = fileName;
//设置Content-Type:application/octet-stream
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
//设置响应消息内容长度
response.Content.Headers.ContentLength = contentInfo.Length;
if (contentInfo.IsPartial)
{
//设置响应内容的起始位置
response.Content.Headers.ContentRange = new ContentRangeHeaderValue(contentInfo.From, contentInfo.To, entireLength);
}
return response;
}

数据源访问接口:

public interface IFileProvider
{
bool Exists();
Stream GetPartialStream(long offset);
long GetLength();
}

数据源接口实现

public class DiskFileProvider : IFileProvider,IDisposable
{
private Stream fileStream;
private string filePath;
public DiskFileProvider(string filePath)
{
try
{
this.filePath = filePath;
this.fileStream = new FileStream(filePath, FileMode.Open,FileAccess.Read,FileShare.Read);
}
catch (Exception ex)
{ } } public bool Exists()
{
return File.Exists(filePath);
} public Stream GetPartialStream(long offset)
{
if (offset > )
{
fileStream.Seek(offset, SeekOrigin.Begin);
} return fileStream;
} public long GetLength()
{
return fileStream.Length;
} public void Dispose()
{
if(fileStream!=null)fileStream.Close();
}
}

数据模型:请求参数模型和请求数据信息模型

    public class Input
{
public string FileCode { set; get; }
public string FileType { set; get; }
}
public class ContentInfo
{
public long From {set;get;}
public long To { set; get; }
public bool IsPartial { set; get; }
public long Length { set; get; }
}

2使用PushStreamContent

为了使用PushStreamContent需要对IFileProvider进行改造,如下:

public interface IFileProvider
{
long Offset{set;get;}
bool Exists();
Stream GetPartialStream(long offset);
Task WriteToStream(Stream outputStream, HttpContent content, TransportContext context);
long GetLength();
}

可以发现与原来的接口相比较多了Offset属性和WriteToStream方法。

下面是IFileProvider接口的实现,为了使用PushStreamContent,实现接口的WriteToStream方法,这里需要注意:

PushStreamContent构造函数有几个重载的方法,他们的共同特点是含有委托类型的参数。而本文采用了有返回值的参数,经实践发现采用无返回值的参数,会随机地生成一条windows警告日志。另外调用FileStream.Read函数时,其参数都是int类型的,但是FileStream.Length却是long类型的,在使用时就需要转型,不要将FileStream.Length,而应在(int)Math.Min(length, (long)buffer.Length)这部分执行转型,这样如果FileStream.Length真的比int类型的最大值还大,那么也不会因为转型而出现错误。

public class ByteToStream : IFileProvider
{
private string filePath;
public long Offset{set;get;}
public ByteToStream(string filePath)
{
try
{
this.filePath = filePath;
}
catch (Exception ex)
{ }
} public bool Exists()
{
return File.Exists(filePath);
} public Stream GetPartialStream(long offset)
{
throw new NotImplementedException();
} public long GetLength()
{
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
return fileStream.Length;
}
} public async Task WriteToStream(Stream outputStream, HttpContent content, TransportContext context)
{
try
{
var buffer = new byte[]; using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
fileStream.Seek(Offset, SeekOrigin.Begin);
long length = fileStream.Length;
var bytesRead = ; while (length > && bytesRead > )
{
bytesRead = fileStream.Read(buffer, , (int)Math.Min(length, (long)buffer.Length));
await outputStream.WriteAsync(buffer, , bytesRead);
length -= bytesRead;
}
}
}
catch (HttpException ex)
{
return;
}
finally
{
outputStream.Close();
}
}
}

控制器操作相应地变为:

        public HttpResponseMessage Get([FromUri]Input input)
{
string filePath = string.Format(@"D:\工具软件\{0}.{1}", input.FileCode, input.FileType);
string fileName = Path.GetFileName(filePath); IFileProvider fileProvider = new ByteToStream(filePath);
long entireLength = fileProvider.GetLength();
ContentInfo contentInfo = GetContentInfoFromRequest(entireLength, this.Request);
Func<Stream, HttpContent, TransportContext, Task> onStreamAvailable = fileProvider.WriteToStream;
HttpContent content = new PushStreamContent(onStreamAvailable); return SetResponse(content, contentInfo, entireLength,fileName);
}

---------------------------------------------------------------------

转载与引用请注明出处。

时间仓促,水平有限,如有不当之处,欢迎指正。

ASP.NET Web API编程——文件下载的更多相关文章

  1. ASP.NET Web API编程——路由

    路由过程大致分为三个阶段: 1)请求URI匹配已存在路由模板 2)选择控制器 3)选择操作 1匹配已存在的路由模板 路由模板 在WebApiConfig.Register方法中定义路由,例如模板默认生 ...

  2. ASP.NET Web API编程——序列化与内容协商

    1 多媒体格式化器 多媒体类型又叫MIME类型,指示了数据的格式.在HTTP协议中多媒体类型描述了消息体的格式.一个多媒体类型包括两个字符串:类型和子类型. 例如: text/html.image/p ...

  3. ASP.NET Web API编程——构建api帮助文档

    1 概要 创建ASP.NET Web Api 时模板自带Help Pages框架. 2 问题 1)使用VS创建Web Api项目时,模板将Help Pages框架自动集成到其中,使得Web Api项目 ...

  4. ASP.NET Web API编程——模型验证与绑定

    1.模型验证 使用特性约束模型属性 可以使用System.ComponentModel.DataAnnotations提供的特性来限制模型. 例如,Required特性表示字段值不能为空,Range特 ...

  5. ASP.NET Web API编程——版本控制

    版本控制   版本控制的方法有很多,这里提供一种将Odata与普通web api版本控制机制统一的方法,但也可以单独控制,整合控制与单独控制主要的不同是:整合控制通过VersionController ...

  6. ASP.NET Web API编程——文件上传

    首先分别介绍正确的做法和错误的做法,然后分析他们的不同和错误之处,以便读者在实现此功能时可避开误区 1正确的做法 public class AvaterController : BaseApiCont ...

  7. ASP.NET Web API编程——异常捕获

    1 向客户端发送错误消息 使用throw new HttpResponseException()向客户端抛出错误信息. HttpResponseException包含两个重载的构造函数,其中一个是构造 ...

  8. ASP.NET Web API编程——客户端调用

    可以使用HttpClient这个调用Web API,下面是HttpClient的定义,列举了一些常用的方法,其中还有一些没有列举,包括重载的方法. public class HttpClient : ...

  9. ASP.NET Web API编程——接口安全与角色控制

    1 API接口验证与授权 JWT JWT定义,它包含三部分:header,payload,signature:每一部分都是使用Base64编码的JSON字符串.之间以句号分隔.signature是”h ...

随机推荐

  1. RabbitMQ - 任务队列

    这次我们试着实现这样一个小程序: 嗯,就是任务队列(task queue).不是将任务集中在一堆并一直等到所有任务一并完成为止,而是将每一个任务封装为一个消息,并将其发送到队列,后台的workers就 ...

  2. nodejs封装mssql

    对mssql操作Sqlserver数据库的基本封装: 记录一下: /** * Created by chaozhou on 2015/9/18. */ var mssql = require('mss ...

  3. 网站部署中遇到的问题-未能加载文件或程序集“System.Data.SQLite”或它的某一个依赖项

    问题描述: 运行站点抛出错误:未能加载文件或程序集“System.Data.SQLite”或它的某一个依赖项 原因: 应用程序池没有启用32位程序. 解决方法: 找到站点对应的应用程序池,设置启用32 ...

  4. 在C++中实现类似Java的“synchronized”

    我只是代码的搬运工,原文参见A "synchronized" statement for C++ like in Java.其实现是通过区域锁(Scoped locking)和宏定 ...

  5. jmeter单一接口测试

    在使用jmeter进行单一接口测试之前,我们先来了解一下一个用户请求的过程,如下: 1)用户通过浏览器发起一个请求: 2)用户收到服务器返回的响应数据. 如上,就是一个WEB的请求/响应模型 jmet ...

  6. Spring Cloud实战之初级入门(六)— 服务网关zuul

    目录 1.环境介绍 2.api网关服务 2.1 创建工程 2.3 api网关中使用token机制 2.4 测试 2.5 小结 3.一点点重要的事情 1.环境介绍 好了,不知不觉中我们已经来到了最后一篇 ...

  7. java swing画图片爱心

    第一次用swing做一个可视化程序,写第一篇随笔,有写的不好的地方请多多见谅.上个星期三在网上看到一个画爱心的软件,就想着自己用java也实现一个程序,画爱心用到的数学函数知识在网上百度的,不是本人原 ...

  8. 使用js获取URL地址栏里面的参数, 获取请求链接参数,函数定义如下

    function getUrlRequestParam(name) { var paramUrl = window.location.search.substr(1); var paramStrs = ...

  9. python中字符串(str)常用操作总结

    # 字符串的常用操作方法 (都是形成新的字符串,与原字符串没有关系.) 1.字符串的基本操作之切片 s = 'python hello word' # 取首不取尾,取尾要+1 # 切片取出来的字符串与 ...

  10. ccf-201809-2 买菜

    问题描述 小H和小W来到了一条街上,两人分开买菜,他们买菜的过程可以描述为,去店里买一些菜然后去旁边的一个广场把菜装上车,两人都要买n种菜,所以也都要装n次车.具体的,对于小H来说有n个不相交的时间段 ...