断点续传基本原理

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. Hibernate学习1--对象持久化的思想

    前些天开始接触hibernate,想想以前直接用c3p0进行笨拙而繁杂的数据库操作就觉得生无可恋了,也正因如此,在刚刚接触hibernate的我,便强烈地喜欢上这种偷懒而优雅的面向对象式操作数据库的方 ...

  2. OOP 第一章作业总结

    程序设计结构分析 类图分析 第一次作业 由于第一次作业完成的功能比较简单,而且出于对面向对象设计理念不熟悉(其实现在也不是很熟悉,逃),整个程序设计的非常简单.通过类图(见下)可以看出,程序只有两个类 ...

  3. CSS 盒子模型及 float 和 position

    ## CSS和模型 ##CSS盒模型本质上是一个盒子,封装周围的 HTML 元素,包括 外边距(marign),边框(border),填充(padding),内容物(content) 盒子模型的类型: ...

  4. mongoDB (mongoose、增删改查、聚合、索引、连接、备份与恢复、监控等等)

    MongoDB - 简介 官网:https://www.mongodb.com/ MongoDB 是一个基于分布式文件存储的数据库,由 C++ 语言编写,旨在为 WEB 应用提供可扩展的高性能数据存储 ...

  5. C++程序员必需的修养

    原文:http://www.cnblogs.com/ctoroad/archive/2006/03/24/357423.html 我总结了在用C/C++语言(主要是C语言)进行程序写作上的三十二个“修 ...

  6. dom操作排他思想

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  7. ajax 请求调用问题

    http://localhost/dev/list 和  http://127.0.0.1/dev/list 最近在架构整体常规通用系统的解决方案,遭遇AJAX请求不执行的问题,刚开始以为Spring ...

  8. 64位Navicat Premium-11.2.7(64bit)访问64位Oracle服务器

    1 在windows 10 64位操作系统安装Navicat Premium-11.2.7(64bit). 2 在服务器安装64位的Oracle(win64_11gR2_database). 3 在h ...

  9. Apache2.4和IIS7整合共享80端口测试

    言我再重新排版一下 在C:\Windows\System32\drivers\etc\hosts文件中配置2个测试域名用于整合测试 127.0.0.1 www.aaa.com // apache项目 ...

  10. task可声明参数 z

    直接这样写 var task1 = Task.Factory.StartNew(() => DoSomeWork(message1, message2)); 如果是winform,你在另外一个线 ...