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

前言

以前一直感受斷點續傳比較神祕,因而想去一探究竟,不知從何入手,覺得就寫寫邏輯就行,結果搜索一番,還得了解相關http協議知識,又花了許久功夫去看http協議中有關斷點續傳知識,有時候發覺東西只有當你用到再去看相關內容時纔會掌握的更加牢固,理解的更加透徹吧,下面咱們首先來補補關於http協議中斷點續傳的知識。html

http協議知識惡補

當請求一個html頁面時咱們會看到請求頁面以下:web

第一眼看到上面Accept中的參數時我是懵逼的,以前也就看看緩存cookie等常見的頭信息,因而藉此機會也學習下這部份內容。json

咱們知道Accept是指客戶端容許請求返回的內容類型,那爲什麼這裏面參數有如此之多呢?在學習WebAPi時,咱們在服務端未進行過濾時既能夠返回xml,也能夠返回json,此時如上圖同樣,text/html未匹配上,接着匹配xml類型,匹配後則進行相應格式內容返回,因此客戶端接受如此多類型內容,也是爲了服務端那邊未設置特定內容響應,此時則根據客戶端設置的內容進行最合適的匹配。api

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

q(quality)

上面給出了客戶端可以接受響應的內容類型,天然就有最合適的匹配,此時就用到了q這個參數,在此我將q翻譯爲quality即權重的意思,應該是比較合適的,它用來表示咱們期待接受內容偏心的程度即所佔的權重。它的範圍是0-1,其默認值爲1,這就相似質檢部門對產品合格判斷的一種介質。例如當咱們須要返回視頻資源時,咱們客戶端設置爲以下:緩存

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

此時咱們將上述翻譯以下:cookie

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

咱們更加期待返回的是audio/basic類型的資源,由於其權重爲1大於audio/*類型的資源,若爲匹配到則繼續匹配下一個資源,audio/*則表示屬於audio類型的全部子類型資源。多線程

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

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

上述也算是漲知識了。繼續回到咱們的話題,此時咱們下載一個文件則看到以下圖所示:

 

由於未實現斷點續傳,此時咱們經過右鍵能夠看到沒法暫停,以下:

咱們繼續往下走,接下來來實現斷點續傳看看:

斷點續傳下載

在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;
        }

接下來咱們看看演示結果:

從上面演示咱們看出目前已經實現了斷點續傳,瀏覽器下載管理器出現了暫停的按鈕,可是當暫停後沒法繼續進行後續下載,在這裏存在問題,咱們下節再進行後續講解。同時當返回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。

相關文章
相關標籤/搜索