前言
上一篇我們穿插了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
