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

2024-06-20 23:08

本文主要是介绍ASP.NET WebAPi之断点续传下载(中),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

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

StreamContent compare to PushStreamContent

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

 

public class StreamContent : HttpContent
{// Fieldsprivate int bufferSize;private Stream content;private bool contentConsumed;private const int defaultBufferSize = 0x1000;private long start;// Methodspublic 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 Typesprivate 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 PushStreamContentreturn !(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

这篇关于ASP.NET WebAPi之断点续传下载(中)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1079523

相关文章

常用的jdk下载地址

jdk下载地址 安装方式可以看之前的博客: mac安装jdk oracle 版本:https://www.oracle.com/java/technologies/downloads/ Eclipse Temurin版本:https://adoptium.net/zh-CN/temurin/releases/ 阿里版本: github:https://github.com/

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

用命令行的方式启动.netcore webapi

用命令行的方式启动.netcore web项目 进入指定的项目文件夹,比如我发布后的代码放在下面文件夹中 在此地址栏中输入“cmd”,打开命令提示符,进入到发布代码目录 命令行启动.netcore项目的命令为:  dotnet 项目启动文件.dll --urls="http://*:对外端口" --ip="本机ip" --port=项目内部端口 例: dotnet Imagine.M

2、PF-Net点云补全

2、PF-Net 点云补全 PF-Net论文链接:PF-Net PF-Net (Point Fractal Network for 3D Point Cloud Completion)是一种专门为三维点云补全设计的深度学习模型。点云补全实际上和图片补全是一个逻辑,都是采用GAN模型的思想来进行补全,在图片补全中,将部分像素点删除并且标记,然后卷积特征提取预测、判别器判别,来训练模型,生成的像

2. 下载rknn-toolkit2项目

官网链接: https://github.com/airockchip/rknn-toolkit2 安装好git:[[1. Git的安装]] 下载项目: git clone https://github.com/airockchip/rknn-toolkit2.git 或者直接去github下载压缩文件,解压即可。

前端form表单+ifarme方式实现大文件下载

// main.jsimport Vue from 'vue';import App from './App.vue';import { downloadTokenFile } from '@/path/to/your/function'; // 替换为您的函数路径// 将 downloadTokenFile 添加到 Vue 原型上Vue.prototype.$downloadTokenF

【干货分享】基于SSM的体育场管理系统的开题报告(附源码下载地址)

中秋送好礼 中秋佳节将至,祝福大家中秋快乐,阖家幸福。本期免费分享毕业设计作品:《基于SSM的体育场管理系统》。 基于SSM的体育场管理系统的开题报告 一、课题背景与意义 随着全民健身理念的深入人心,体育场已成为广大师生和社区居民进行体育锻炼的重要场所。然而,传统的体育场管理方式存在诸多问题,如资源分配不均、预约流程繁琐、数据统计不准确等,严重影响了体育场的使用效率和用户体验。

Tomcat下载压缩包解压后应有如下文件结构

1、bin:存放启动和关闭Tomcat的命令的路径。 2、conf:存放Tomcat的配置,所有的Tomcat的配置都在该路径下设置。 3、lib:存放Tomcat服务器的核心类库(JAR文件),如果需要扩展Tomcat功能,也可将第三方类库复制到该路径下。 4、logs:这是一个空路径,该路径用于保存Tomcat每次运行后产生的日志。 5、temp:保存Web应用运行过程中生成的临时文件

下载/保存/读取 文件,并转成流输出

最近对文件的操作又熟悉了下;现在记载下来:学习在于 坚持!!!不以细小而不为。 实现的是:文件的下载、文件的保存到SD卡、文件的读取输出String 类型、最后是文件转换成流输出;一整套够用了; 重点: 1:   操作网络要记得开线程; 2:更新网络获取的数据 切记用Handler机制; 3:注意代码的可读性(这里面只是保存到SD卡,在项目中切记要对SD卡的有无做判断,然后再获取路径!)