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

 

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

转载与引用请注明出处。

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

posted @ 2017-11-18 12:57  甜橙很酸  阅读(1285)  评论(0编辑  收藏  举报