天天看點

ASP.NET WebAPi之斷點續傳下載下傳(上)

前言

之前一直感覺斷點續傳比較神秘,于是想去一探究竟,不知從何入手,以為就寫寫邏輯就行,結果搜尋一番,還得了解相關http協定知識,又花了許久功夫去看http協定中有關斷點續傳知識,有時候發覺東西隻有當你用到再去看相關内容時才會掌握的更加牢固,了解的更加透徹吧,下面我們首先來補補關于http協定中斷點續傳的知識。

http協定知識惡補

當請求一個html頁面時我們會看到請求頁面如下:

ASP.NET WebAPi之斷點續傳下載下傳(上)

第一眼看到上面Accept中的參數時我是懵逼的,之前也就看看緩存cookie等常見的頭資訊,于是借此機會也學習下這部分内容。

我們知道Accept是指用戶端允許請求傳回的内容類型,那為何這裡面參數有如此之多呢?在學習WebAPi時,我們在服務端未進行過濾時既可以傳回xml,也可以傳回json,此時如上圖一樣,text/html未比對上,接着比對xml類型,比對後則進行相應格式内容傳回,是以用戶端接受如此多類型内容,也是為了服務端那邊未設定特定内容響應,此時則根據用戶端設定的内容進行最合适的比對。

那麼問題來了,上面的q是啥玩意?

q(quality)

上面給出了用戶端能夠接受響應的内容類型,自然就有最合适的比對,此時就用到了q這個參數,在此我将q翻譯為quality即權重的意思,應該是比較合适的,它用來表示我們期待接受内容偏愛的程度即所占的權重。它的範圍是0-1,其預設值為1,這就類似質檢部門對産品合格判斷的一種媒體。例如當我們需要傳回視訊資源時,我們用戶端設定為如下:

Accept: audio/*; q=0.2, audio/basic      

此時我們将上述翻譯如下:

audio/basic; q=1     audio/*; q=0.2      

我們更加期待傳回的是audio/basic類型的資源,因為其權重為1大于audio/*類型的資源,若為比對到則繼續比對下一個資源,audio/*則表示屬于audio類型的所有子類型資源。

接下來,我們再來看一個例子:

Accept: text/plain; q=0.5, text/html,text/x-dvi; q=0.8, text/x-c      

此時我們則可以翻譯為如下:

Accept:      text/html;q=1或者 text/x-c;q=1     text/x-dvi; q=0.8     text/plain; q=0.5      

傾向于傳回text/html或者text/x-c類型資源,若都不存在,則傳回權重為0.8的text/x-dvi,最終還是不存在則傳回text/plain。

Accept-Ranges

在響應頭中添加此字段允許服務端來顯示表明對資源範圍的接受。如果服務端接受一個位元組範圍的資源的請求則此時變成如下:

Accept-Ranges: bytes      

如果服務端不接受任何範圍的請求資源此時則在響應頭添加如下來告訴用戶端不要發送範圍請求的資源:

Accept-Ranges: none      

Content-Range

當在響應頭中添加接受位元組範圍的資源時,此時若用戶端請求資源檔案比較大時即隻是傳回部分資料時,此時則傳回狀态碼為206的部分内容,在Content-Range響應頭資訊中實時顯示目前資料的進度。比如如下:

//開始500個位元組資料     Content-Range: bytes 0-499/1234     //第二個500個位元組資料     Content-Range: bytes 500-999/1234     //除了開始500個位元組之外的資料     Content-Range: bytes 500-1233/1234     //最後500個位元組資料(表示資料最終傳輸完畢)     Content-Range: bytes 734-1233/1234      

如果用戶端請求資源到達所給資源的界限此時則傳回416的狀态碼。

注意:當請求資源為位元組範圍請求時,不要在響應頭中使用 multipart/byteranges 類型的content-type。 

斷點續傳場景

當正在下載下傳時出于其他任何原是以時下載下傳中斷,那麼下載下傳使用者隻能重新下載下傳,這樣的體驗想必是比較痛苦的,最煩躁的是如果使用者是在移動端下載下傳大檔案時,居然下載下傳中斷了,接下來又得重新下載下傳,此時想必使用者會放棄下載下傳。此時斷點續傳則應運而生。 斷點續傳則需要用到上述Accept-Ranges和Content-Range将其添加到響應頭中。例如如下:

HEAD http://localhost/api/files/get?filename=blog_backup.zip      User-Agent: IIS     Host: localhost     HTTP/1.1 200 OK       Content-Length: 1182367743       Content-Type: application/octet-stream       Accept-Ranges: bytes       Server: Microsoft-IIS/10.0       Content-Disposition: attachment; filename=blog_backup.zip      
HEAD http://localhost/api/files/get?filename=blog_backup.zip        User-Agent: IIS     Host: localhost       Range: bytes=0-999     HTTP/1.1 206 Partial Content       Content-Length: 1000       Content-Type: application/octet-stream       Content-Range: bytes 0-999/1182367743       Accept-Ranges: bytes       Server: Microsoft-IIS/10.0       Content-Disposition: attachment; filename=blog_backup.zip      

接下來我們來實作簡單的下載下傳以及斷點續傳下載下傳對比看看效果。 

在webapi中提供了一系列友善我們調用的api,比如 ContentDispositionHeaderValue 來設定附件而不像在webform中手動在響應頭中進行拼接。以及傳回的MimeType類型 MediaTypeHeaderValue 。首先我們看看最普通的下載下傳。

普通下載下傳

普通的下載下傳無非就是擷取到檔案的辨別再打開下載下傳的檔案夾,最後得到檔案流傳回到響應的HttpContent對象中以及設定附件即可。我們看看如下代碼還是比較簡單的,這種相對比較簡單的下載下傳想必我們大家定是信手拈來。

//響應的MimeType類型             private const string MimeType = "application/octet-stream";             //配置檔案中配置的檔案所在路徑             private const string AppSettingDirPath = "DownloadDir";            //将配置檔案中取得的路徑賦給此變量             private readonly string DirFilePath;             this.DirFilePath = ConfigurationManager.AppSettings[AppSettingDirPath];      

接下來就是最重要的下載下傳邏輯了,如下:

public HttpResponseMessage Download(string fileName)             {                 var fullFilePath = Path.Combine(this.DirFilePath, fileName);                 if (!File.Exists(fullFilePath))                 {                     throw new HttpResponseException(HttpStatusCode.NotFound);                 }                 FileStream fileStream = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);                 var response = new HttpResponseMessage();                 response.Content = new StreamContent(fileStream);                 response.Content.Headers.ContentDisposition                     = new ContentDispositionHeaderValue("attachment") { FileName = fileName };                 response.Content.Headers.ContentType                     = new MediaTypeHeaderValue(MimeType);                 response.Content.Headers.ContentLength                     = fileStream.Length;                 return response;             }      

那麼問題來了,我們可不可以在擷取檔案流傳回到HttpContent之前是不是應該首先将檔案流放入到緩沖流中然後再傳回呢?如下:

var bufferStream = new BufferedStream(fileStream);      response.Content = new StreamContent(bufferStream);      

我們想着是不是将檔案流率先放入到緩沖流中效果是否更佳呢?剛開始我也是這樣想來着,但是經過查證資料發現:

為了得到更好的性能,在檔案流中已經包含有緩沖流的緩沖邏輯,對于用緩沖流來包裹檔案流的情況沒有任何好處,還有一點就是在.NET Framework中沒有任何一個流需要用到緩沖流,但是,但是有一種情況除外則是若我們自定義實作流且預設沒有實作緩沖的邏輯情況下需要用到緩沖流,資料來源于:Filestream and BufferedStream

上述也算是漲知識了。繼續回到我們的話題,此時我們下載下傳一個檔案則看到如下圖所示:

ASP.NET WebAPi之斷點續傳下載下傳(上)

因為未實作斷點續傳,此時我們通過右鍵可以看到無法暫停,如下:

ASP.NET WebAPi之斷點續傳下載下傳(上)

我們繼續往下走,接下來來實作斷點續傳看看:

斷點續傳下載下傳

在WebAPi提供了Range屬性其傳回對象為 RangeHeaderValue 裡面有存在每個範圍的集合如下:

// 摘要:              //     Gets the ranges specified from the System.Net.Http.Headers.RangeHeaderValue             //     object.             //             // 傳回結果:              //     Returns System.Collections.Generic.ICollection<T>.The ranges from the System.Net.Http.Headers.RangeHeaderValue             //     object.             public ICollection<RangeItemHeaderValue> Ranges { get; }      

這是為利用多線程下載下傳而提供,這裡我們僅僅實作一個範圍的下載下傳。我們通過判斷這個對象的值是否為null來實作斷點續傳。

if (Request.Headers.Range == null ||                      Request.Headers.Range.Ranges.Count == 0 ||                      Request.Headers.Range.Ranges.FirstOrDefault().From.Value == 0)                 {                     var sourceStream = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);                     response = new HttpResponseMessage(HttpStatusCode.OK);                     response.Content = new StreamContent(sourceStream);                     response.Headers.AcceptRanges.Add("bytes");//告訴用戶端接受資源為位元組                     response.Content.Headers.ContentLength = sourceStream.Length;                     response.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeType);                     response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")                     {                         FileName = fileName                     };                 }      

擷取目前已經下載下傳位元組數,接着繼續進行剩下位元組下載下傳。

else                 {                     var item = Request.Headers.Range.Ranges.FirstOrDefault();                     if (item != null && item.From.HasValue)                     {                         response = this.GetPartialContent(fileName, item.From.Value);                     }                 }      

剩餘位元組數下載下傳

private HttpResponseMessage GetPartialContent(string fileName, long partial)             {                 var response = new HttpResponseMessage();                 var fullFilePath = Path.Combine(this.DirFilePath, fileName);                 FileInfo fileInfo = new FileInfo(fullFilePath);                 long startByte = partial;                 var memoryStream = new MemoryStream();                 var buffer = new byte[65536];                 using (var fileStream = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))                 {                     var bytesRead = 0;                     fileStream.Seek(startByte, SeekOrigin.Begin);                     int length = Convert.ToInt32((fileInfo.Length - 1) - startByte) + 1;                     while (length > 0 && bytesRead > 0)                     {                         bytesRead = fileStream.Read(buffer, 0, Math.Min(length, buffer.Length));                         memoryStream.Write(buffer, 0, bytesRead);                         length -= bytesRead;                     }                     response.Content = new StreamContent(memoryStream);                  }                 response.Headers.AcceptRanges.Add("bytes");                 response.StatusCode = HttpStatusCode.PartialContent;                 response.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeType);                 response.Content.Headers.ContentLength = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read).Length;                 response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")                 {                     FileName = fileName                 };                 return response;             }      

接下來我們看看示範結果:

ASP.NET WebAPi之斷點續傳下載下傳(上)

從上面示範我們看出目前已經實作了斷點續傳,浏覽器下載下傳管理器出現了暫停的按鈕,但是當暫停後無法繼續進行後續下載下傳,在這裡存在問題,我們下節再進行後續講解。同時當傳回HttpContent發現居然還有一個可以傳回的HttpContent即 PushStreamContent ,此時我們可以将剩餘部分位元組下載下傳進行如下修改:

Action<Stream, HttpContent, TransportContext> pushContentAction = (outputStream, content, context) =>                 {                     try                     {                         var buffer = new byte[65536];                         using (var fileStream = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))                         {                             var bytesRead = 0;                             fileStream.Seek(startByte, SeekOrigin.Begin);                             int length = Convert.ToInt32((fileInfo.Length - 1) - startByte) + 1;                             while (length > 0 && bytesRead > 0)                             {                                 bytesRead = fileStream.Read(buffer, 0, Math.Min(length, buffer.Length));                                 outputStream.Write(buffer, 0, bytesRead);                                 length -= bytesRead;                             }                         }                     }                     catch (HttpException ex)                     {                         throw ex;                     }                     finally                     {                         outputStream.Close();                     }                 };                 response.Content = new PushStreamContent(pushContentAction, new MediaTypeHeaderValue(MimeType));                 response.StatusCode = HttpStatusCode.PartialContent;                 response.Headers.AcceptRanges.Add("bytes");                 response.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeType);                 response.Content.Headers.ContentLength = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read).Length;                 response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")                 {                     FileName = fileName                 };                 return response;      

如上所做也可行,傳回StreamContent不就ok了嗎,為何還出現一個PushStreamContent呢?這又是一個遺留問題!

總結

本節我們講述了在webapi中普通下載下傳以及斷點續傳下載下傳,對于斷點續傳下載下傳當暫停後無法繼續進行下載下傳,暫時還存在一定問題,對于傳回的内容既可以為StreamContent,也可以是PushStreamContent,這二者有何差別呢?二者的應用場景是什麼呢?這又是一個問題,關于此二者我們下節再講,webapi一個很輕量的服務架構,你值得擁有,see u。

所有的選擇不過是為了下一次選擇做準備