前言
上一篇我們穿插了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
所有的選擇不過是為了下一次選擇做準備