講講斷點續傳那點兒事

本篇文章已受權微信公衆號 dasu_Android(大蘇)獨家發佈android

此次想來說講斷點續傳,之前沒相關需求,因此一直沒去接觸,近階段瞭解了以後,其實並不複雜,那麼也便來寫一篇記錄一下,分享給大夥,也方便本身後續查閱。c++

提問

Q1:若是你的 app 須要下載大文件,那麼是否有方法能夠縮短下載耗時?數據庫

Q2:若是你的 app 在下載大文件時,程序因各類緣由被迫中斷了,那麼下次再重啓時,文件是否還須要重頭開始下載?api

Q3:你的 app 下載大文件時,支持暫停並恢復下載麼?即便這兩個操做分佈在程序進程被殺先後。微信

理論基礎

講以前,先來通俗的解釋下什麼是斷點續傳網絡

說得白一點,其實也就是下載文件時,沒必要重頭開始下載,而是從指定的位置繼續下載,這樣的功能就叫作斷點續傳。多線程

既然如此,那麼要實現斷點續傳的關鍵點其實也就是兩點:app

  • 如何告知服務端,從指定的位置下載
  • 如何知道客戶端想要的指定位置是多少

是吧,理論上來說,當這兩點均可以作到的時候,天然就能夠實現斷點續傳了。那麼,要如何作到呢?框架

其實,也很簡單,並不須要咱們本身去寫一些什麼,HTTP 協議自己就支持斷點續傳了,因此藉助它就能夠實現告知服務端,從指定位置下載的功能了。curl

而另外一點,就更簡單了,文件是下載到客戶端設備上的,那麼只要獲取到這份下載到一半的文件,看一下它目前的大小,也就知道須要讓服務端從哪開始繼續下載了。

那麼,下面就介紹一下涉及到的相關理論:

Range & Content-Length & Content-Range & If-Range

這些都是 HTTP 包中 Header 頭部的一些字段信息,其中 Range 和 If-Range 是請求頭中的字段,Content-Length 和 Content-Range 是響應頭中的字段。

Range

當請求頭中出現 Range 字段時,表示告知服務端,客戶端下載該文件想要從指定的位置開始下載,至於 Range 字段屬性值的格式有如下幾種:

格式 含義
Range:bytes=0-500 表示下載從0到500字節的文件,即頭500個字節
Range:bytes=501-1000 表示下載從500到1000這部分的文件,單位字節
Range:bytes=-500 表示下載最後的500個字節
Range:bytes=500- 表示下載從500開始到文件結束這部分的內容

當 app 想實現縮短大文件的下載耗時,能夠開啓多個下載線程,每一個線程只負責文件的一部分下載,當全部線程下載結束後,將每一個線程下載的文件按順序拼接成一個完整的文件,這樣就能夠達到縮短下載大文件的耗時目的了。

那麼,此時,就可使用 Range:bytes=501-1000 這種格式了,每一個線程在各自的請求頭字段中,以這種格式加入相對應的信息便可達到目的了。

若是 app 想實現斷點續傳,文件下載到一半被迫中斷,下次啓動還能夠繼續接着上次進度下載時,那麼此時可使用 Range:bytes=500- 這種格式了,只要先獲取本地那份文件目前的大小,經過在請求頭中加入 Range 字段信息便可。

Content-Length

Content-Length 字段出如今響應頭中,用於告知客戶端這次下載的文件大小。

通常,若是客戶端須要實現下載進度實時更新時,就須要知道文件的總大小和目前下載的大小,後者能夠經過對本地文件的操做得知,前者通常就是經過響應頭中的 Content-Length 字段得知。

另外,若是想要實現多線程同時分段下載大文件功能時,顯然在下載前,客戶端須要先知道文件總大小,才能夠作到動態進行分段,所以通常在下載前都會先發送一個不須要攜帶 body 信息請求,用於先獲取響應頭中的 Content-Length 字段來得知文件總大小。

但有一點須要注意:Content-Length 只表示此連接中下載的文件大小

什麼意思,也就是說,若是這條連接是一次性將整個文件下載下來的,那麼 Content-Length 就表示這個文件的總大小。

但,若是這條連接指定了 Range,代表了只是下載文件的指定部分的內容,那麼此時 Content-Length 表示的就只是這一部分的大小。

因此,若是客戶端實現了下載進度實時更新功能時,須要注意一下。由於若是文件是斷點續傳的,那麼進度條的分母就不能用每次 HTTP 連接中的 Content-Length。要麼下載前先發一條獲取用於文件總大小的請求,而後一直維護着這個數據,要麼就使用 Content-Range 字段。

Content-Range

Content-Range 字段也是出如今響應頭中,用於告知客戶端此連接下載的文件是哪一個部分的,以及文件的總大小。

好比,當客戶端在請求頭中指定了 Range:bayes=501-1000 來下載一個總大小爲 2000 字節文件的中間一部份內容時,此時,響應頭中的 Content-Range 字段信息以下:

Content-Range:bytes 501-1000/2000

斜槓前表示此連接下載的文件是哪一部分,斜槓後表示文件的總大小。

If-Range

斷點續傳,說白點也就是分屢次下載,既然不是一次性下載,那麼就沒法保證屢次下載的間隔。

也就是說,有可能出現這種場景,此次因爲某些緣由只下載的一部分,而下次重啓繼續下載,但可能等到過了不少天后才重啓去繼續下載,若是在這期間,服務端的這份文件更新了怎麼辦?

只要不是一次性下載的,那麼就有可能會出現這種場景,顯然,這時候,就不但願斷點續傳了,而是要讓客戶端直接重頭開始下載,畢竟文件都已經發生更新了,不是同一份了,再繼續恢復下載也沒有什麼意義。

那麼,客戶端要如何知道服務端的文件是否發生變化,要重頭下載呢?

這時就能夠結合 If-Range 字段來實現了,這個也是在請求頭中的字段,跟 Range 字段一塊兒使用,它的做用是給 Range 字段生效設置了一些條件,只有知足這些條件,Range 才能生效。

也就是說,只有先知足 If-Range,那麼才能經過 Range 來實現斷點續傳。

那它的條件值能夠設置爲哪些呢?有兩種,Last-Modified 或者 ETag,這兩個也都是響應頭中的字段。

具體能夠參考這篇文章:MDN If-Range

抓包示例

以上就是斷點續傳相關的理論基礎,下面抓個包,看看請求頭和響應頭中的信息,來總結一下理論基礎。

斷點續傳.png

首先先發起一個請求,設置了不攜帶 BODY 信息,這樣就能夠在下載前先獲取到文件的總大小。至於怎麼設置不攜帶 BODY 信息,不一樣的網絡框架不一樣,具體下節代碼示例中說明。

斷點續傳2.png

這是下載中斷後,重啓想要繼續下載時發起的請求信息,請求頭中指定了 Range:bytes=12341380- 表示本地已經下載了這麼多,須要從這裏開始繼續往下下載。

響應頭中返回了這部分的內容,並在 Content-Length 和 Content-Range 字段中給出了相關信息。

代碼示例

理論基礎掌握了,那麼下面就是來看看代碼怎麼實現。無論用什麼語言,使用了什麼網絡框架,要寫的代碼都有兩個部分:

  • 文件處理操做
  • 添加請求頭信息操做

文件處理操做有兩個關鍵點,一是獲取文件大小,二是以追加的方式寫文件。添加請求頭的操做則是參考各自網絡框架的指示便可。

下面介紹了三種示例,分別是 C++&libcurl,Android&HttpURLConnection,Android&OkHttp。&前面是語言,後面是所使用的網絡框架。

C++&libcurl

//引入libcurl庫
#include <curl\curl.h>
#pragma comment(lib,"libcurl.lib") 
//文件操做庫
#include <sys/stat.h>
#include <fstream>

char* mLocalFilePath;//下載到本地的文件

//獲取已下載部分的大小,若是沒有則返回0
curl_off_t getLocalFileLength()
{
    curl_off_t ret = 0;
    struct stat fileStat;
    ret = stat(mLocalFilePath, &fileStat);
    if (ret == 0)
    {
        return fileStat.st_size;//返回本地文件已下載的大小
    }
    else
    {
        return 0;
    }
}

//下載前先發送一次請求,獲取文件的總大小
double getDownloadFileLength()
{
    double rel = 0, downloadFileLenth = 0;
    CURL *handle = curl_easy_init();
    curl_easy_setopt(handle, CURLOPT_URL, mDownloadFileUrl);
    curl_easy_setopt(handle, CURLOPT_HEADER, 1);    //只須要header頭
    curl_easy_setopt(handle, CURLOPT_NOBODY, 1);    //不須要body
    if (curl_easy_perform(handle) == CURLE_OK) {
        curl_easy_getinfo(handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &downloadFileLenth);
    }
    else {
        downloadFileLenth = -1;
    }
    rel = downloadFileLenth;
    curl_easy_cleanup(handle);
    return rel;
}

//文件下載
CURLcode downloadInternal()
{
    //1. 獲取本地已下載的大小,有則斷點續傳
    curl_off_t localFileLenth = getLocalFileLength();
    //2. 以追加的方式寫入文件
    FILE *file = fopen(mLocalFilePath, "ab+");
    CURL* mHandler = curl_easy_init();
    if (mHandler && file)
    {
         //3. 設置url
        curl_easy_setopt(mHandler, CURLOPT_URL, mDownloadFileUrl);
        //4. 設置請求頭 Range 字段信息,localFileLength 不等於0時,值大小就表示從哪開始下載 
        curl_easy_setopt(mHandler, CURLOPT_RESUME_FROM_LARGE, localFileLenth);
        
        //5. 設置接收數據的處理函數和存放變量
        curl_easy_setopt(mHandler, CURLOPT_WRITEFUNCTION, writeFile);
        curl_easy_setopt(mHandler, CURLOPT_WRITEDATA, file);
        // 6. 發起請求
        CURLcode rel = curl_easy_perform(mHandler);
        fclose(file);
        return rel;
    }
    curl_easy_cleanup(mHandler);
    return CURLE_FAILED_INIT;
}

writeFile 函數和下載進度通知的函數我都沒貼,用過 libcurl 的應該都知道怎麼寫,或者網上搜一下,資料不少。上面就是將斷點續傳的幾個關鍵函數貼出來,理清楚了便可。

Android&HttpURLConnection

Android&OkHttp

因爲最近都在忙 C++ 的項目了,Android 暫時還沒時間本身寫個 demo 測試一下,因此先給幾篇網上找的連接佔個坑,後續抽個時間本身再來寫個 demo。

之因此列了這兩點,是由於感受目前 Android 中網絡框架大多都是用的 OkHttp 了,而下載文件還有不少都是用的 HttpURLConnection,因此這兩個都想研究一下,怎麼寫斷點續傳。

Android多線程斷點續傳下載

Android使用OKHttp3實現下載(斷點續傳、顯示進度)

兩篇我都有大概過了下,其實斷點續傳原理不難,真的蠻簡單的,因此實現上基本也大同小異,就是不一樣的網絡框架的 api 用法不一樣而已。以及,如何維護本地已下載文件的大小的思路,有的是直接去獲取文件對象查看,有的則是手動本身建個數據庫維護。
***
你們好,我是 dasu,歡迎關注個人公衆號(dasuAndroidTv),若是你以爲本篇內容有幫助到你,能夠轉載但記得要關注,要標明原文哦,謝謝支持~
dasuAndroidTv2.png

相關文章
相關標籤/搜索