龙空技术网

熟练掌握ASP.NET Web API:文件下载之断点续传

甜橙很酸 208

前言:

今天各位老铁们对“aspneturl下载图片不显示”大致比较重视,大家都想要学习一些“aspneturl下载图片不显示”的相关内容。那么小编同时在网络上搜集了一些对于“aspneturl下载图片不显示””的相关知识,希望姐妹们能喜欢,看官们一起来学习一下吧!

断点续传基本原理

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:[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, 1024); return SetResponse(content, contentInfo, entireLength,fileName); }}

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

private ContentInfo GetContentInfoFromRequest(long entireLength, HttpRequestMessage request) { var contentInfo = new ContentInfo { From = 0, To = entireLength - 1, IsPartial = false, Length = entireLength }; RangeHeaderValue rangeHeader = request.Headers.Range; if (rangeHeader != null && rangeHeader.Ranges.Count != 0) { //仅支持一个range if (rangeHeader.Ranges.Count > 1) { throw new HttpResponseException(HttpStatusCode.BadRequest); } RangeItemHeaderValue range = rangeHeader.Ranges.First(); if (range.From.HasValue && range.From < 0 || range.To.HasValue && range.To > entireLength - 1) { throw new HttpResponseException(HttpStatusCode.BadRequest); } contentInfo.From = range.From ?? 0; contentInfo.To = range.To ?? entireLength - 1; contentInfo.IsPartial = true; contentInfo.Length = entireLength; if (range.From.HasValue && range.To.HasValue) { contentInfo.Length = range.To.Value - range.From.Value + 1; } else if (range.From.HasValue) { contentInfo.Length = entireLength - range.From.Value; } else if (range.To.HasValue) { contentInfo.Length = range.To.Value + 1; } } 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 > 0) { 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[1024000]; using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { fileStream.Seek(Offset, SeekOrigin.Begin); long length = fileStream.Length; var bytesRead = 1; while (length > 0 && bytesRead > 0) { bytesRead = fileStream.Read(buffer, 0, (int)Math.Min(length, (long)buffer.Length)); await outputStream.WriteAsync(buffer, 0, 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); }

标签: #aspneturl下载图片不显示