1. 程式人生 > >ASP.NET WebAPi之斷點續傳下載(下)

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