asp.net web api 下载之断点续传

一、基本思想

利用 HTTP 请求的Range标头值,来向服务端传递请求数据的开始位置和结束位置。服务端获得这两个参数后,将指定范围内的数据传递给客户端。当客户端请求暂停或中断之后,待到客户端再次向服务器发起请求,继续下载数据时,客户端传递给服务端的Range值说明了向服务端请求数据的范围,即从上一次中断传输的位置开始直到最后。

二、示例代码

1 DownloadCore:完成下载任务

public class DownloadCore<T>
    {
        private HttpRequestMessage request;
        private IFileProvider<T> fileProvider;
        private T requestModel;
        public DownloadCore(HttpRequestMessage request,IFileProvider<T> fileProvider,T requestModel)
        {
            this.request = request;
            this.fileProvider = fileProvider;
            this.requestModel = requestModel;
        }
        public HttpResponseMessage Download()
        {
            if (requestModel == null)
            {
                //抛出异常
            }
            if (!fileProvider.Exists(requestModel))
            {
                //抛出异常            }
            long fileLength = fileProvider.GetLength(requestModel);
            if (fileLength == 0)
            {
                //抛出异常 
            }
            ContentInfo contentInfo = GetContentInfoFromRequest(fileLength);
            Stream stream = PartialStream.GetPartialStream(fileProvider.Open(requestModel), contentInfo.From, contentInfo.To);
            if (stream == null)
            {
                //抛出异常 
       }
            HttpContent content = new StreamContent(stream, AppSettings.BufferSize);
            return SetResponse(content, contentInfo, fileLength, fileProvider.GetFileName(requestModel));
        }
        private ContentInfo GetContentInfoFromRequest(long entityLength)
        {
            var result = new ContentInfo
            {
                From = 0,
                To = entityLength - 1,
                IsPartial = false,
                Length = entityLength
            };
            RangeHeaderValue rangeHeader = request.Headers.Range;
            if (rangeHeader != null && rangeHeader.Ranges.Count != 0)
            {
                //仅支持一个range
                if (rangeHeader.Ranges.Count > 1)
                {
                    //抛出异常 
                }
                RangeItemHeaderValue range = rangeHeader.Ranges.First();
                if (range.From.HasValue && range.From < 0 || range.To.HasValue && range.To > entityLength - 1)
                {
                    //抛出异常 
                }

                result.From = range.From ?? 0;
                result.To = range.To ?? entityLength - 1;
                result.IsPartial = true;
                result.Length = entityLength;

                if (range.From.HasValue && range.To.HasValue)
                {
                    result.Length = range.To.Value - range.From.Value + 1;
                }
                else if (range.From.HasValue)
                {
                    result.Length = entityLength - range.From.Value;
                }
                else if (range.To.HasValue)
                {
                    result.Length = range.To.Value + 1;
                }
            }
            return result;
        }

        private HttpResponseMessage SetResponse(HttpContent content, ContentInfo contentInfo, long entityLength, string fileNameWithExtend)
        {
            HttpResponseMessage response = new HttpResponseMessage() ;
            response.Headers.AcceptRanges.Add("bytes");
            response.StatusCode = contentInfo.IsPartial ? HttpStatusCode.PartialContent : HttpStatusCode.OK;
            response.Content = content;
            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
            response.Content.Headers.ContentDisposition.FileName = fileNameWithExtend;
            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, entityLength);
            }
            return response;
        }
}

网络请求参数解析

public static DownloadUriModel GetDownloadParam(HttpRequestMessage request)
        {
            var uriQuery = request.RequestUri.Query;
            //针对中文重新编码
            uriQuery = HttpUtility.UrlDecode(uriQuery);

            var queryChildren = uriQuery.Substring(1, uriQuery.Length - 1).Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries).ToList();
            Dictionary<string, string> queryDict = new Dictionary<string, string>();
            queryChildren.ForEach(item =>
            {
                var child = item.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
                queryDict.Add(child[0], child[1]);
            });          

            string fileName = queryDict.ContainsKey("fileName") ? queryDict["fileName"] : null;
            if (string.IsNullOrWhiteSpace(fileName))
            {
                //抛出异常             
       }
            string extend = Path.GetExtension(fileName);
            if (!string.IsNullOrWhiteSpace(extend))
            {
                fileName = Path.GetFileNameWithoutExtension(fileName);
            }

            string fileType = queryDict.ContainsKey("fileType") ? queryDict["fileType"] : null;
            if (string.IsNullOrWhiteSpace(fileType))
            {
                //抛出异常 
            }

            return new DownloadUriModel
            {
                FileNameNoExtend = fileName,
                FileType = fileType,                
            };
        }

部分流数据

public class PartialStream
    {
        public static Stream GetPartialStream(Stream fileStream, long start, long end)
        {
            if (fileStream == null)
            {
                return null;
            }
            if (start > 0)
            {
                fileStream.Seek(start, SeekOrigin.Begin);
            }

            return fileStream;
        }
}

2 数据

下载数据的来源包括本地磁盘,网络,数据库等,这里只列举待下载数据在本地磁盘和网络的情形。

本地磁盘

public class DiskFileProvider : IFileProvider<DownloadRequestModel>
    {
        /// <summary>
        /// android app 所在文件夹路径
        /// </summary>
        private readonly string filesDirectory;

        public DiskFileProvider()
        {
            filesDirectory = AppSettings.AppLocation;
        }

        public bool Exists(DownloadRequestModel model)
        {
            string searchPattern = string.Format("{0}.{1}",model.FileNameNoExtend, model.FileType);
            string file = Directory.GetFiles(filesDirectory, searchPattern, SearchOption.TopDirectoryOnly)
                    .FirstOrDefault();
            return file != null;
        }

        public Stream Open(DownloadRequestModel model)
        {
            return File.Open(GetFilePath(model), FileMode.Open, FileAccess.Read,FileShare.Delete);
        }

        public long GetLength(DownloadRequestModel model)
        {
            return new FileInfo(GetFilePath(model)).Length;
        }

        private string GetFilePath(DownloadRequestModel model)
        {
            string searchPattern = string.Format("{0}.{1}", model.FileNameNoExtend, model.FileType);
            return Path.Combine(filesDirectory, searchPattern);
        }


        public string GetFileName(DownloadRequestModel model)
        {
            return model.FileNameWithExtend;
        }
}

网络

public class NetFileProvider: IFileProvider<DownloadUriModel>
{
public bool Exists(DownloadUriModel model)
{
//具体实现
}


public Stream Open(DownloadUriModel model)
{
//具体实现
}

......
}

统一的接口

public interface IFileProvider<T>
{
        bool Exists(T model);
        Stream Open(T model);
        long GetLength(T model);
        string GetFileName(T model);
}

3 数据模型

public class DownloadRequestModel
{
        /// <summary>
        /// 文件名(不含扩展名)
        /// </summary>
        public string FileNameNoExtend { get; set; }
        /// <summary>
        /// 文件类型(扩展名)
        /// </summary>
        public string FileType { get; set; }
        /// <summary>
        /// 文件名
        /// </summary>
        public string FileNameWithExtend
        {
            get 
            {
                string extend = FileType.Contains(".") ? FileType.Substring(1) : FileType;
                return string.Format("{0}.{1}", FileNameNoExtend, extend);
            }
        }
        
}

//扩展实现
public class DownloadUriModel:DownloadRequestModel
{
......

}

public class ContentInfo
{
        public long From;
        public long To;
        public bool IsPartial;
        public long Length;
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏瓜大三哥

Yaffs_guts(三)

1.垃圾回收 1.static int yaffs_InitialiseBlocks(yaffs_Device *dev,int nBlocks)//块初始化 ...

2025
来自专栏java一日一条

50个常见的 Java 错误及避免方法(第三部分)

当我们尝试调用带有错误参数的Java代码时,通常会产生此Java错误消息(@ghacksnews):

1083
来自专栏Flutter入门

Weex是如何在Android客户端上跑起来的

Weex可以通过自己设计的DSL,书写.we文件或者.vue文件来开发界面,整个页面书写分成了3段,template、style、script,借鉴了成熟的MV...

2444
来自专栏GopherCoder

『Golang 内置模块库 template 』

4244
来自专栏菩提树下的杨过

Replace方法与正则表达式的性能比较

今天做项目时遇到一个小需求:要将字符串中的回车符号替换成其它符号(比如"<br/>")。 考虑到不同的情况下,有些系统中是用\r\n作回车符,有些仅用\n就代表...

1939
来自专栏Java架构师历程

【datatable】Cannot read property ‘style’ of undefined问题解决

遇到这个问题的时候一开始我以为是引入的js有问题,后来研究了源码之后原来是datatable的列的数量和表头列的数量没有对齐,表头我定义的列是这样的

1141
来自专栏码农阿宇

JustMock .NET单元测试利器(三)用JustMock测试你的应用程序

用JustMock测试你的应用程序 本主题将指导您通过几个简单的步骤来使用Telerik®JustMock轻松测试您的应用程序。您将理解一个简单的原理,称为Ar...

3657
来自专栏编码小白

tomcat请求处理分析(六)servlet的处理过程

1.1.1.1  servlet的解析过程 servlet的解析分为两步实现,第一个是匹配到对应的Wrapper,第二个是加载对应的servlet并进行数据,这...

5937
来自专栏大内老A

Enterprise Library深入解析与灵活应用(6):自己动手创建迷你版AOP框架

基于Enterprise Library PIAB的AOP框架已经在公司项目开发中得到广泛的使用,但是最近同事维护一个老的项目,使用到了Enterprise L...

1898
来自专栏大内老A

我的WCF之旅(4):WCF中的序列化[上篇]

SOA 和Message Windows Communication Foundation (WCF) 是基于面向服务架构(Service Orienta...

18810

扫码关注云+社区