• ASP.NET Web API编程——文件下载


    断点续传基本原理

    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, 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);
            }

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

    转载与引用请注明出处。

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

  • 相关阅读:
    Encryption (hard) CodeForces
    cf 1163D Mysterious Code (字符串, dp)
    AC日记——大整数的因子 openjudge 1.6 13
    AC日记——计算2的N次方 openjudge 1.6 12
    Ac日记——大整数减法 openjudge 1.6 11
    AC日记——大整数加法 openjudge 1.6 10
    AC日记——组合数问题 落谷 P2822 noip2016day2T1
    AC日记——向量点积计算 openjudge 1.6 09
    AC日记——石头剪刀布 openjudge 1.6 08
    AC日记——有趣的跳跃 openjudge 1.6 07
  • 原文地址:https://www.cnblogs.com/hdwgxz/p/7856409.html
Copyright © 2020-2023  润新知