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

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

    转载与引用请注明出处。

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

  • 相关阅读:
    RABBITMQ/JAVA 客户端测试(再补:利用文件流)
    在虚拟机中安装CentOS7
    RabbitMQ/JAVA 客户端测试(补:利用线程)
    ffmepg-nginx-nginx-rtmp-module配置脚本
    PDO防注入原理分析以及使用PDO的注意事项
    使用MySQL Proxy解决MySQL主从同步延迟
    好文收藏
    CentOS6.6安装mysql出现的问题
    大型网站技术架构相关文章
    Redis必要的一些配置
  • 原文地址:https://www.cnblogs.com/hdwgxz/p/7856409.html
Copyright © 2020-2023  润新知