断点续传基本原理

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. Java - 如何进行安全发布

    首先让我简单解释一下所谓"发布". 发布(publish),使对象可以在当前作用域之外的代码中可见,如果该对象被发布,则该对象的非私有域中引用的所有实例同样也会被发布. 不仅仅是作 ...

  2. golang学习之beego增删改查代码实现

    记录下使用beego的增删改查实现,数据库使用mysql,完整代码如下: package main import ( _ "crud_beego/routers" //自动注册路由 ...

  3. mac os下载安装jmeter

    一.简介 jmeter是属于apache的一个开源产品,纯Java应用.最初用来进行功能测试,而后又扩展了更多的测试功能. 二.下载 进入apache的jmeter下载页:http://jmeter. ...

  4. zookeeper【2】集群管理

    Zookeeper 的核心是广播,这个机制保证了各个Server之间的同步.实现这个机制的协议叫做Zab协议. Zab协议有两种模式,它们分别是恢复模式(选主)和广播 模式(同步).当服务启动或者在领 ...

  5. System.arraycopy的测试

    ArrayList的源码中数组的拷贝用到该方法: public static void arraycopy(Object src, --源数组 int srcPos, --源数组要复制的起始位置 Ob ...

  6. 第8章 CSS3中的变形与动画(上)

    变形--旋转 rotate() 旋转rotate()函数通过指定的角度参数使元素相对原点进行旋转.它主要在二维空间内进行操作,设置一个角度值,用来指定旋转的幅度.如果这个值为正值,元素相对原点中心顺时 ...

  7. K2P刷机教程转自恩山磨人的小妖精

    K2P刷机指南说明 K2P MTK版发布之初用的是22.5.7.85, 这个版本官改和高恪K2P固件都可以从斐讯固件基础上直接升级, 是所谓直刷.但好景不长, 之后的版本比如22.5.17.33就改了 ...

  8. mysql load data infile auto increment id

    1. 问题描述 当使用load data infile 向表中插入数据 而主键id是 auto_increment 时 ,执行 load data 不会报错 但插入也不成功 2. 问题解决 2.1 方 ...

  9. 【Android】10.0 UI开发——如何编写程序界面、常见控件的使用

    ************************ 转载请注明出处:https://www.cnblogs.com/xiaofu007/p/10331880.html ***************** ...

  10. 理解JS表达式

    表达式:是由运算元和运算符(可选)构成,并产生运算结果的语法结构. 基本表达式 以下在ES5中被称为基本表达式(Primary Expression) this.null.arguments等内置的关 ...