ASP.NET WebAPi之断点续传下载(下)
前言
上一篇我们穿插了C#的内容,本篇我们继续来讲讲webapi中断点续传的其他情况以及利用webclient来实现断点续传,至此关于webapi断点续传下载以及上传内容都已经全部完结,一直嚷嚷着把SQL Server和Oracle数据库再重新过一遍,这篇过完,就要开始新的征程,每一个阶段都应该有自己的小目标,要不然当工作太忙没时间去充电,太闲又变得懒散,想想一切是为了未来买得起孩子高档的奶粉就又有动力了。
话题
关于webapi断点续传下载的情况,之前我们利用webapi内置的api展开了具体的实现,这一节我们利用已经老掉牙的技术来实现,这个是看了一篇老外文章而想到的,具体地址忘记了,利用内存映射文件来实现断点续传,内存映射文件最常见的应用场景莫过于对于多个进程之间共享数据,我们知道进程与进程之间只能操作已经分配好各自的内存,当我们需要一个进程与另外一个进程共享一块数据时我们该如何做呢,这个时候就要用到内存映射文件(MemoryMappedFile),内存映射文件是单一机器多进程间数据通信的最高效的方式,好了关于内存映射文件具体内容可以参考园友【.net 流氓】的文章。我们通过内存映射文件管理虚拟内存然后将其映射到磁盘上具体的文件中,当然我们得知道所谓的文件能够被映射并不是将文件复制到虚拟内存中,而是由于会被应用程序访问到,很显然windows会加载部分物理文件,通过使用内存映射文件我们能够保证操作系统会优化磁盘访问,此外我们能够得到内存缓存的形式。因为文件被映射到虚拟内存中,所以在管理大文件时我们需要在64位模式下运行我们的程序,否则将无法满足我们所需的所有空间。
断点续传(内存映射文件)
关于涉及到的类以及接口在之前文章已经叙述,这里我们就不再啰嗦,这里我们给出下载文件的逻辑。
/// <summary> /// 下载文件 /// </summary> /// <param name="fileName"></param> /// <returns></returns> public HttpResponseMessage GetFile(string fileName) { if (!FileProvider.Exists(fileName)) { throw new HttpResponseException(HttpStatusCode.NotFound); } long fileLength = FileProvider.GetLength(fileName); var fileInfo = GetFileInfoFromRequest(this.Request, fileLength); ......... }
我们从请求信息中获取到了文件的信息,接下来我们就是利用内存映射文件的时候
MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.Read);
自定义一个映射名称,若此时已存在我们则继续读打开的文件,若不存在我们将打开要下载的文件并创建内存映射文件。
mmf = MemoryMappedFile.CreateFromFile(FileProvider.Open(fileName), mapName, fileLength, MemoryMappedFileAccess.Read, null, HandleInheritability.None, false);
接着我们创建一个映射文件内存的视图流并返回最终将其写入到响应中的HttpContent中。
mmf.CreateViewStream(0, fileLength, MemoryMappedFileAccess.Read); response.Content = new StreamContent(stream);
整个利用内存映射文件下载文件的逻辑如下:
/// <summary> /// 下载文件 /// </summary> /// <param name="fileName"></param> /// <returns></returns> public HttpResponseMessage GetFile(string fileName) { if (!FileProvider.Exists(fileName)) { throw new HttpResponseException(HttpStatusCode.NotFound); } long fileLength = FileProvider.GetLength(fileName); var fileInfo = GetFileInfoFromRequest(this.Request, fileLength); var mapName = string.Format("FileDownloadMap_{0}", fileName); MemoryMappedFile mmf = null; try { mmf = MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.Read); } catch (FileNotFoundException) { mmf = MemoryMappedFile.CreateFromFile(FileProvider.Open(fileName), mapName, fileLength, MemoryMappedFileAccess.Read, null, HandleInheritability.None, false); } using (mmf) { Stream stream = fileInfo.IsPartial ? mmf.CreateViewStream(fileInfo.From, fileInfo.Length, MemoryMappedFileAccess.Read) : mmf.CreateViewStream(0, fileLength, MemoryMappedFileAccess.Read); var response = new HttpResponseMessage(); response.Content = new StreamContent(stream); SetResponseHeaders(response, fileInfo, fileLength, fileName); return response; } }
有时候运行会出现如下错误:
再要么下载过程中出现访问遭到拒绝的情况
若将权限修改为 MemoryMappedFileAccess.ReadWrite ,也会出现访问遭到拒绝的情况,此二者错误的出现暂未找到解决方案,期待读者能给出一点见解或者答案。
断点续传(WebClient)
利用WebClient进行断点续传下载最主要的是对象 HttpWebRequest 中的AddRange方法,类似webapi中的RangeHeaderItemValue对象的from和to显示表明请求从哪里到哪里的数据。其余的就比较简单了,我们获取文件下载路径以及下载目的路径,下载过程中获取目的路径中文件的长度,若路径长度大于0则继续添加,小于0则从0创建文件并下载直到响应头中的文件长度即Content-Length和目的文件长度相等才下载完成。
第一步(打开Url下载)
client.OpenRead(url);
第二步(若目标文件已存在,比较其余响应头中文件长度,若大于则删除重新下载)
if (File.Exists(filePath)) { var finfo = new FileInfo(filePath); if (client.ResponseHeaders != null && finfo.Length >= Convert.ToInt64(client.ResponseHeaders["Content-Length"])) { File.Delete(filePath); } }
第三步(断点续传逻辑)
long existLen = 0; FileStream saveFileStream; if (File.Exists(destinationPath)) { var fInfo = new FileInfo(destinationPath); existLen = fInfo.Length; } if (existLen > 0) saveFileStream = new FileStream(destinationPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); else saveFileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); var httpWebRequest = (HttpWebRequest)System.Net.HttpWebRequest.Create(sourceUrl); httpWebRequest.AddRange((int)existLen); var httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using (var respStream = httpWebResponse.GetResponseStream()) { var timout = httpWebRequest.Timeout; respStream.CopyTo(saveFileStream); }
第四步(判断目标文件长度与响应头中文件,相等则下载完成)
fileInfo = fileInfo ?? new FileInfo(destinationFilePath); if (fileInfo.Length == Convert.ToInt64(client.ResponseHeaders["Content-Length"])) { Console.WriteLine("下载完成......."); } else { throw new WebException("下载中断,请尝试重新下载......"); }
整个利用WebClient下载逻辑如下:
(1)控制台调用,开始下载
Console.WriteLine("开始下载......"); try { DownloadFile("http://localhost:61567/FileLocation/UML.pdf", "d:\\temp\\uml.pdf"); } catch (Exception ex) { if (!string.Equals(ex.Message, "Stack Empty.", StringComparison.InvariantCultureIgnoreCase)) { Console.WriteLine("{0}{1}{1} 出错啦: {1} {2}", ex.Message, Environment.NewLine, ex.InnerException.ToString()); } }
(2)下载文件并判断下载是否完成
public static void DownloadFile(string url, string filePath) { var client = new WebClient(); ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, errors) => { return true; }; try { client.OpenRead(url); FileInfo fileInfo = null; if (File.Exists(filePath)) { var finfo = new FileInfo(filePath); if (client.ResponseHeaders != null && finfo.Length >= Convert.ToInt64(client.ResponseHeaders["Content-Length"])) { File.Delete(filePath); } } DownloadFileWithResume(url, filePath); fileInfo = fileInfo ?? new FileInfo(destinationFilePath); if (fileInfo.Length == Convert.ToInt64(client.ResponseHeaders["Content-Length"])) { Console.WriteLine("下载完成......."); } else { throw new WebException("下载中断,请尝试重新下载......"); } } catch (Exception ex) { Console.WriteLine("Error: {0} {1}", ex.Message, Environment.NewLine); Console.WriteLine("下载中断,请尝试重新下载......"); throw; } }
(3)断点续传逻辑
/// <summary> /// 断点续传下载 /// </summary> /// <param name="sourceUrl"></param> /// <param name="destinationPath"></param> private static void DownloadFileWithResume(string sourceUrl, string destinationPath) { long existLen = 0; FileStream saveFileStream; if (File.Exists(destinationPath)) { var fInfo = new FileInfo(destinationPath); existLen = fInfo.Length; } if (existLen > 0) saveFileStream = new FileStream(destinationPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); else saveFileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); var httpWebRequest = (HttpWebRequest)System.Net.HttpWebRequest.Create(sourceUrl); httpWebRequest.AddRange((int)existLen); var httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using (var respStream = httpWebResponse.GetResponseStream()) { var timout = httpWebRequest.Timeout; respStream.CopyTo(saveFileStream); } }
总结
至此在webapi中利用内存映射文件下载以及在控制台中利用WebClient下载叙述基本已经完结,其中或多或少还是存在一点问题,后续有时间再来看看,对于上述出现的问题,有解决方案的读者可以提供一下。接下来我将开始新的征程,开始SQL Server和Oracle数据库学习之旅。
更新
所有代码已经上传到右上角github,有需要请下载,或者直接点击如下地址clone:WebAPiResumeDownload