断点续传基本原理

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. 灵感一:搜索型APP,帮助读书爱好者,搜索某本书的关键字

    灵感来自生活,我在查询 javascript 高级程序设计 3的时候,由于我记不清楚,该关键字在书的某个地方,我就想,有没有这么一款APP,可以通过扫描一本书的二维码,自动下载该书的内容,然后再通过搜 ...

  2. EasyPusher推流类库的.NET调用说明

    EasyPusher推流类库的.NET调用说明 以下内容基于在使用EasyPusher过程中遇到的问题,以及相应的注意事项.本文主要是基于对C++类库的二次封装(便于调试发现问题)以供C#调用以及对一 ...

  3. 三、thymeleaf的使用

    1.简介 thymleaf是一个基于html的页面模板,springboot极力推荐使用它,代替jsp. API地址:https://www.thymeleaf.org/doc/tutorials/3 ...

  4. input的属性用法介绍

    Input表示Form表单中的一种输入对象,其又随Type类型的不同而分文本输入框,密码输入框,单选/复选框,提交/重置按钮等,下面一一介绍. 1,type=text 输入类型是text,这是我们见的 ...

  5. flask之flask-login登陆验证(一)

    这个模块能帮助我们做很多事,最常用到的是,登陆验证(验证当前用户是否已经登陆).记住我功能 一 安装 pip install flask-login 二 导入相关模块及对象并初始化 from flas ...

  6. Python中@修饰符的作用。

    '@'符号用作函数修饰符是python2.4新增加的功能,修饰符必须出现在函数定义前一行,不允许和函数定义在同一行.也就是说@A def f(): 是非法的. 只可以在模块或类定义层内对函数进行修饰, ...

  7. 第10章 布局样式相关-伸缩布局(Flexible Box)

    伸缩布局(一) CSS3引入了一种新的布局模式--Flexbox布局,即伸缩布局盒模型(Flexible Box),用来提供一个更加有效的方式制定.调整和分布一个容器里项目布局,即使它们的大小是未知或 ...

  8. 排序算法lowb三人组-插入排序

    def insert_sort(li): for i in range(1, len(li)): # i表示摸到的牌的下标 tmp = li[i] # 摸到的牌 j = i - 1 while j & ...

  9. 关于 class 的命名

    class名称中只出现小写字符和破折号 使用有组织或目的明确的名称,不使用表现形式 基于最近的的父class 作为新class的前缀 使用 .js-* 来标识行为,并且不要将这些class包含到css ...

  10. ERP与电子商务的集成

    目前现状: 一般来说,企业中存在三种流:物资流.资金流和信息流,其中,信息流不是孤立存在的,它与物资流和资金流密切相关,反映了物资和资金流动前.流动中和流动后的状况. 电子商务与ERP被分裂开来,没有 ...