ASP.NET WebAPi之断点续传下载(中)

简介:

前言

前情回顾:上一篇我们遗留了两个问题,一个是未完全实现断点续传,另外则是在响应时是返回StreamContent还是PushStreamContent呢?这一节我们重点来解决这两个问题,同时就在此过程中需要注意的地方一并指出,若有错误之处,请指出。

StreamContent compare to PushStreamContent

我们来看看StreamContent代码,如下:

复制代码
public class StreamContent : HttpContent
{
    // Fields
    private int bufferSize;
    private Stream content;
    private bool contentConsumed;
    private const int defaultBufferSize = 0x1000;
    private long start;

    // Methods
    public StreamContent(Stream content);
]
    public StreamContent(Stream content, int bufferSize);
 
    protected override Task<Stream> CreateContentReadStreamAsync();

    protected override void Dispose(bool disposing);
    private void PrepareContent();

    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context);

    protected internal override bool TryComputeLength(out long length);

    // Nested Types
    private class ReadOnlyStream : DelegatingStream
    {......}
}
复制代码

似乎没有什么可看的,但是有一句话我们需要注意,如下:

 private const int defaultBufferSize = 0x1000;

在StreamContent的第二个构造函数为

 public StreamContent(Stream content, int bufferSize);

上述给定的默认一次性输入到缓冲区大小为4k,这对我们有何意义呢?当我们写入到响应中时,一般我们直接利用的是第一个构造函数,如下:

  var response = new HttpResponseMessage();
  response.Content = new StreamContent(fileStream);

到这里我们明白了这么做是有问题的,当下载时默认读取的是4k,如果文件比较大下载的时间则有延长,所以我们在返回时一定要给定缓冲大小,那么给定多少呢?为达到更好的性能最多是80k,如下:

 private const int BufferSize = 80 * 1024;
 response.Content = new StreamContent(fileStream, BufferSize);

此时下载的速度则有很大的改善,有人就说了为何是80k呢?这个问题我也不知道,老外验证过的,这是链接【.NET Asynchronous stream read/write】。

好了说完StreamContent,接下来我们来看看PushStreamContent,从字面意思来为推送流内容,难道是充分利用了缓冲区吗,猜测可以有,就怕没有任何想法,我们用源码来证明看看。

我们只需看看WebHost模式下对于缓冲策略是怎么选择的,我们看看此类 WebHostBufferPolicySelector  实现,代码如下:

复制代码
     /// <summary>
    /// Provides an implementation of <see cref="IHostBufferPolicySelector"/> suited for use
    /// in an ASP.NET environment which provides direct support for input and output buffering.
    /// </summary>
    public class WebHostBufferPolicySelector : IHostBufferPolicySelector
    {
        ....../// <summary>
        /// Determines whether the host should buffer the <see cref="HttpResponseMessage"/> entity body.
        /// </summary>
        /// <param name="response">The <see cref="HttpResponseMessage"/>response for which to determine
        /// whether host output buffering should be used for the response entity body.</param>
        /// <returns><c>true</c> if buffering should be used; otherwise a streamed response should be used.</returns>
        public virtual bool UseBufferedOutputStream(HttpResponseMessage response)
        {
            if (response == null)
            {
                throw Error.ArgumentNull("response");
            }

            // Any HttpContent that knows its length is presumably already buffered internally.
            HttpContent content = response.Content;
            if (content != null)
            {
                long? contentLength = content.Headers.ContentLength;
                if (contentLength.HasValue && contentLength.Value >= 0)
                {
                    return false;
                }

                // Content length is null or -1 (meaning not known).  
                // Buffer any HttpContent except StreamContent and PushStreamContent
                return !(content is StreamContent || content is PushStreamContent);
            }

            return false;
        }
    }
复制代码

从上述如下一句可以很明显的知道:

 return !(content is StreamContent || content is PushStreamContent);

除了StreamContent和PushStreamContent的HttpContent之外,其余都进行缓冲,所以二者的区别不在于缓冲,那到底是什么呢?好了我们还未查看PushStreamContent的源码,我们继续往下走,查看其源代码如下,我们仅仅只看关于这个类的描述以及第一个构造函数即可,如下:

复制代码
  /// <summary>
    /// Provides an <see cref="HttpContent"/> implementation that exposes an output <see cref="Stream"/>
    /// which can be written to directly. The ability to push data to the output stream differs from the 
    /// <see cref="StreamContent"/> where data is pulled and not pushed.
    /// </summary>
    public class PushStreamContent : HttpContent
    {
        private readonly Func<Stream, HttpContent, TransportContext, Task> _onStreamAvailable;

        /// <summary>
        /// Initializes a new instance of the <see cref="PushStreamContent"/> class. The
        /// <paramref name="onStreamAvailable"/> action is called when an output stream
        /// has become available allowing the action to write to it directly. When the 
        /// stream is closed, it will signal to the content that is has completed and the 
        /// HTTP request or response will be completed.
        /// </summary>
        /// <param name="onStreamAvailable">The action to call when an output stream is available.</param>
        public PushStreamContent(Action<Stream, HttpContent, TransportContext> onStreamAvailable)
            : this(Taskify(onStreamAvailable), (MediaTypeHeaderValue)null)
        {
        }
...... }
复制代码

对于此类的描述大意是:PushStreamContent与StreamContent的不同在于,PushStreamContent在于将数据push【推送】到输出流中,而StreamContent则是将数据从流中【拉取】。 

貌似有点晦涩,我们来举个例子,在webapi中我们常常这样做,读取文件流并返回到响应流中,若是StreamContent,我们会如下这样做:

response.Content = new StreamContent(File.OpenRead(filePath));

上面的释义我用大括号着重括起,StreamContent着重于【拉取】,当响应时此时将从文件流写到输出流,通俗一点说则是我们需要从文件流中去获取数据并写入到输出流中。我们再来看看PushStreamContent的用法,如下:

复制代码
XDocument xDoc = XDocument.Load("cnblogs_backup.xml", LoadOptions.None);
PushStreamContent xDocContent = new PushStreamContent(
(stream, content, context) =>
{

     xDoc.Save(stream);
     stream.Close();
},
"application/xml");
复制代码

PushStreamContent着重于【推送】,当我们加载xml文件时,当我们一旦进行保存时此时则会将数据推送到输出流中。

二者区别在于:StreamContent从流中【拉取】数据,而PushStreamContent则是将数据【推送】到流中。

那么此二者应用的场景是什么呢?

(1)对于下载文件我们则可以通过StreamContent来实现直接从流中拉取,若下载视频流此时则应该利用PushStreamContent来实现,因为未知服务器视频资源的长度,此视频资源来源于别的地方。

(2)数据量巨大,发送请求到webapi时利用PushStreamContent。

当发送请求时,常常序列化数据并请求webapi,我们可能这样做:

    var client = new HttpClient();
    string json = JsonConvert.SerializeObject(data);
    var response = await client.PostAsync(uri, new StringContent(json));

当数据量比较小时没问题,若数据比较大时进行序列化此时则将序列化的字符串加载到内存中,鉴于此这么做不可行,此时我们应该利用PushStreamContent来实现。

复制代码
    var client = new HttpClient();
    var content = new PushStreamContent((stream, httpContent, transportContext) =>
    {
        var serializer = new JsonSerializer();
        using (var writer = new StreamWriter(stream))
        {
            serializer.Serialize(writer, data);
        }
    });
    var response = await client.PostAsync(uri, content);
复制代码

为什么要这样做呢?我们再来看看源码,里面存在这样一个方法。

  protected override Task SerializeToStreamAsync(Stream stream, TransportContext context);

其内部实现利用异步状态机实现,所以当数据量巨大时利用PushStreamContent来返回将会有很大的改善,至此,关于二者的区别以及常见的应用场景已经叙述完毕,接下来我们继续断点续传问题。

断点续传改进 

上一篇我们讲过获取Range属性中的集合通过如下:

request.Headers.Range

我们只取该集合中的第一个范围元素,通过如下

 RangeItemHeaderValue range = rangeHeader.Ranges.First();

此时我们忽略了返回的该范围对象中有当前下载的进度

range.From.HasValue  
range.To.HasValue

我们获取二者的值然后进行重写Stream实时读取剩余部分,下面我们一步一步来看。

定义文件操作接口

复制代码
    public interface IFileProvider
    {
        bool Exists(string name);
        FileStream Open(string name);
        long GetLength(string name);
    }
复制代码

实现该操作文件接口

复制代码
    public class FileProvider : IFileProvider
    {
        private readonly string _filesDirectory;
        private const string AppSettingsKey = "DownloadDir";

        public FileProvider()
        {
            var fileLocation = ConfigurationManager.AppSettings[AppSettingsKey];
            if (!String.IsNullOrWhiteSpace(fileLocation))
            {
                _filesDirectory = fileLocation;
            }
        }

        /// <summary>
        /// 判断文件是否存在
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public bool Exists(string name)
        {
            string file = Directory.GetFiles(_filesDirectory, name, SearchOption.TopDirectoryOnly)
                    .FirstOrDefault();
            return true;
        }


        /// <summary>
        /// 打开文件
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public FileStream Open(string name)
        {
            var fullFilePath = Path.Combine(_filesDirectory, name);
            return File.Open(fullFilePath,
                FileMode.Open, FileAccess.Read, FileShare.Read);
        }

        /// <summary>
        /// 获取文件长度
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public long GetLength(string name)
        {
            var fullFilePath = Path.Combine(_filesDirectory, name);
            return new FileInfo(fullFilePath).Length;
        }
    }
复制代码

获取范围对象中的值进行赋值给封装的对象

复制代码
    public class FileInfo
    {
        public long From;
        public long To;
        public bool IsPartial;
        public long Length;
    }
复制代码

下载控制器,对文件操作进行初始化

复制代码
    public class FileDownloadController : ApiController
    {
        private const int BufferSize = 80 * 1024;
        private const string MimeType = "application/octet-stream";
        public IFileProvider FileProvider { get; set; }
        public FileDownloadController()
        {
            FileProvider = new FileProvider();
        }
        ......
    }
复制代码

接下来则是文件下载的逻辑,首先判断请求文件是否存在,然后获取文件的长度

            if (!FileProvider.Exists(fileName))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            long fileLength = FileProvider.GetLength(fileName);

将请求中的范围对象From和To的值并判断当前已经下载进度以及剩余进度

复制代码
        private FileInfo GetFileInfoFromRequest(HttpRequestMessage request, long entityLength)
        {
            var fileInfo = new FileInfo
            {
                From = 0,
                To = entityLength - 1,
                IsPartial = false,
                Length = entityLength
            };
            var rangeHeader = request.Headers.Range;
            if (rangeHeader != null && rangeHeader.Ranges.Count != 0)
            {
                if (rangeHeader.Ranges.Count > 1)
                {
                    throw new HttpResponseException(HttpStatusCode.RequestedRangeNotSatisfiable);
                }
                RangeItemHeaderValue range = rangeHeader.Ranges.First();
                if (range.From.HasValue && range.From < 0 || range.To.HasValue && range.To > entityLength - 1)
                {
                    throw new HttpResponseException(HttpStatusCode.RequestedRangeNotSatisfiable);
                }

                fileInfo.From = range.From ?? 0;
                fileInfo.To = range.To ?? entityLength - 1;
                fileInfo.IsPartial = true;
                fileInfo.Length = entityLength;
                if (range.From.HasValue && range.To.HasValue)
                {
                    fileInfo.Length = range.To.Value - range.From.Value + 1;
                }
                else if (range.From.HasValue)
                {
                    fileInfo.Length = entityLength - range.From.Value + 1;
                }
                else if (range.To.HasValue)
                {
                    fileInfo.Length = range.To.Value + 1;
                }
            }

            return fileInfo;
        }
复制代码

在响应头信息中的对象ContentRangeHeaderValue设置当前下载进度以及其他响应信息

复制代码
         private void SetResponseHeaders(HttpResponseMessage response, FileInfo fileInfo,
                                      long fileLength, string fileName)
        {
            response.Headers.AcceptRanges.Add("bytes");
            response.StatusCode = fileInfo.IsPartial ? HttpStatusCode.PartialContent
                                      : HttpStatusCode.OK;
            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
            response.Content.Headers.ContentDisposition.FileName = fileName;
            response.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeType);
            response.Content.Headers.ContentLength = fileInfo.Length;
            if (fileInfo.IsPartial)
            {
                response.Content.Headers.ContentRange
                    = new ContentRangeHeaderValue(fileInfo.From, fileInfo.To, fileLength);
            }
        }
复制代码

最重要的一步则是将FileInfo对象的值传递给我们自定义实现的流监控当前下载进度。

复制代码
    public class PartialContentFileStream : Stream
    {
        private readonly long _start;
        private readonly long _end;
        private long _position;
        private FileStream _fileStream;
        public PartialContentFileStream(FileStream fileStream, long start, long end)
        {
            _start = start;
            _position = start;
            _end = end;
            _fileStream = fileStream;

            if (start > 0)
            {
                _fileStream.Seek(start, SeekOrigin.Begin);
            }
        }

        /// <summary>
        /// 将缓冲区数据写到文件
        /// </summary>
        public override void Flush()
        {
            _fileStream.Flush();
        }

        /// <summary>
        /// 设置当前下载位置
        /// </summary>
        /// <param name="offset"></param>
        /// <param name="origin"></param>
        /// <returns></returns>
        public override long Seek(long offset, SeekOrigin origin)
        {
            if (origin == SeekOrigin.Begin)
            {
                _position = _start + offset;
                return _fileStream.Seek(_start + offset, origin);
            }
            else if (origin == SeekOrigin.Current)
            {
                _position += offset;
                return _fileStream.Seek(_position + offset, origin);
            }
            else
            {
                throw new NotImplementedException("SeekOrigin.End未实现");
            }
        }

        /// <summary>
        /// 依据偏离位置读取
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="offset"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            int byteCountToRead = count;
            if (_position + count > _end)
            {
                byteCountToRead = (int)(_end - _position) + 1;
            }
            var result = _fileStream.Read(buffer, offset, byteCountToRead);
            _position += byteCountToRead;
            return result;
        }

        public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
        {
            int byteCountToRead = count;
            if (_position + count > _end)
            {
                byteCountToRead = (int)(_end - _position);
            }
            var result = _fileStream.BeginRead(buffer, offset,
                                               count, (s) =>
                                               {
                                                   _position += byteCountToRead;
                                                   callback(s);
                                               }, state);
            return result;
        }
        ......
    }
复制代码

更新上述下载的完整逻辑 

复制代码
        public HttpResponseMessage GetFile(string fileName)
        {
            fileName = "HBuilder.windows.5.2.6.zip";
            if (!FileProvider.Exists(fileName))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            long fileLength = FileProvider.GetLength(fileName);
            var fileInfo = GetFileInfoFromRequest(this.Request, fileLength);

            var stream = new PartialContentFileStream(FileProvider.Open(fileName),
                                                 fileInfo.From, fileInfo.To);
            var response = new HttpResponseMessage();
            response.Content = new StreamContent(stream, BufferSize);
            SetResponseHeaders(response, fileInfo, fileLength, fileName);
            return response;
        }
复制代码

下面我们来看看演示结果:

好了,到了这里我们也得到了我们想要的结果。

总结 

本节我们将上节遗留的问题一一进行比较详细的叙述并最终解决,是不是就这么完全结束了呢?那本节定义为中篇岂不是不对头了,本节是在web端进行下载,下节我们利用webclient来进行断点续传。想了想无论是mvc上传下载,还是利用webapi来上传下载又或者是将mvc和webapi结合来上传下载基本都已经囊括,这都算是在项目中比较常用的吧,所以也就花了很多时间去研究。对于webapi的断点续传关键它本身就提供了比较多的api来给我们调用,所以还是很不错,webapi一个很轻量的服务框架,你值得拥有see u.





本文转自Jeffcky博客园博客,原文链接:http://www.cnblogs.com/CreateMyself/p/6078052.html,如需转载请自行联系原作者


目录
相关文章
|
3月前
|
SQL 开发框架 前端开发
分享24个上传下载 和32个社区论坛ASP.NET源码,总有一款适合您
分享24个上传下载 和32个社区论坛ASP.NET源码,总有一款适合您
20 0
|
9月前
|
存储 前端开发 API
30分钟玩转Net MVC 基于WebUploader的大文件分片上传、断网续传、秒传(文末附带demo下载)
30分钟玩转Net MVC 基于WebUploader的大文件分片上传、断网续传、秒传(文末附带demo下载)
30分钟玩转Net MVC 基于WebUploader的大文件分片上传、断网续传、秒传(文末附带demo下载)
|
3天前
|
存储 移动开发 前端开发
对象存储oss使用问题之OSS SDK .net 使用下载例程报错如何解决
《对象存储OSS操作报错合集》精选了用户在使用阿里云对象存储服务(OSS)过程中出现的各种常见及疑难报错情况,包括但不限于权限问题、上传下载异常、Bucket配置错误、网络连接问题、跨域资源共享(CORS)设定错误、数据一致性问题以及API调用失败等场景。为用户降低故障排查时间,确保OSS服务的稳定运行与高效利用。
19 0
|
6月前
|
Windows
​史上最详细的Windows10系统离线安装.NET Framework 3.5的方法(附离线安装包下载)
​史上最详细的Windows10系统离线安装.NET Framework 3.5的方法(附离线安装包下载)
551 0
|
2月前
|
安全 C# 开发者
.NET开源的一键自动化下载、安装、激活Microsoft Office利器
.NET开源的一键自动化下载、安装、激活Microsoft Office利器
|
7月前
|
Apache
基于commons-net实现ftp创建文件夹、上传、下载功能.
基于commons-net实现ftp创建文件夹、上传、下载功能.
106 0
|
9月前
|
移动开发 监控 网络协议
基于Socket通讯(C#)和WebSocket协议(net)编写的两种聊天功能(文末附源码下载地址)
基于Socket通讯(C#)和WebSocket协议(net)编写的两种聊天功能(文末附源码下载地址)
|
4月前
|
JSON 开发框架 .NET
ASP.NET Core Web API设置响应输出的Json数据格式的两种方式
ASP.NET Core Web API设置响应输出的Json数据格式的两种方式
|
8月前
|
开发框架 .NET 中间件
Swagger的 ASP.NET Core Web API 帮助页
使用 Web API 时,了解其各种方法对开发人员来说可能是一项挑战。 Swagger 也称为OpenAPI,解决了为 Web API 生成有用文档和帮助页的问题。 它具有诸如交互式文档、客户端 SDK 生成和 API 可发现性等优点。
69 0
|
8月前
|
开发框架 JSON .NET
使用 ASP.NET Core 创建 Web API系列
使用 ASP.NET Core 创建 Web API系列
164 0