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 条评论
登录 后参与评论

相关文章

来自专栏Kubernetes

runC源码分析——namespace

runc/libcontainer/configs/config.go中定义了container对应的Namespaces。另外对于User Namespace...

2728
来自专栏SDNLAB

源码解读ODL的MAC地址学习(二)

1 简介 上一篇文章(源码解读ODL的MAC地址学习(一))已经分析了MAC地址学习中的ARP请求的部分源码,下面将接着上一篇文章,介绍一下ARP响应和生成流表...

3995
来自专栏码匠的流水账

springboot2增加diskspace指标

spring-boot-actuator-autoconfigure-2.0.1.RELEASE-sources.jar!/org/springframewor...

751
来自专栏张善友的专栏

Blackpearl 的 Impersonate

Blackpearl的Connection方法 Impersonate(string name)。这个就是传说中的后门方法,它可以帮你模拟任何一个帐号(域用户或...

1926
来自专栏IT进修之路

原 spring boot Druid多

1992
来自专栏一个会写诗的程序员的博客

第7章 Spring Boot集成模板引擎小结

因为Spring Boot其实是对Spring生态的封装整合打包,以简化开发中使用Spring框架。所以 Spring Boot在集成模板引擎过程中,其实就是对...

763
来自专栏

后端开源软件集合

缓存系统:memcached(group cache)、redis、mongodb、Couchbase(CouchDB、Membase、CouchOne) ht...

1779
来自专栏Golang语言社区

Golang实现ping

在使用Go语言的net.Dial函数时,发送echo request报文时,不用考虑i前20个字节的ip头;但是在接收到echo response消息时,前20...

702
来自专栏Linyb极客之路

在SpringCloud Zuul中使用WebSockets

近期的项目中需要用到WebSocket,因为使用的是微服务架构,所以又直接使用了Spring Cloud的Zuul。然而,Zuul对WebSocket的支持不是...

932
来自专栏晓晨的专栏

ABP从入门到精通(4):使用基于JWT标准的Token访问WebApi

1413

扫码关注云+社区