講講斷點(diǎn)續(xù)傳那點(diǎn)兒事

本篇文章已授權(quán)微信公眾號 dasu_Android(大蘇)獨(dú)家發(fā)布

這次想來講講斷點(diǎn)續(xù)傳,以前沒相關(guān)需求,所以一直沒去接觸,近階段了解了之后,其實(shí)并不復(fù)雜,那么也便來寫一篇記錄一下,分享給大伙,也方便自己后續(xù)查閱。

提問

Q1:如果你的 app 需要下載大文件,那么是否有方法可以縮短下載耗時(shí)?

Q2:如果你的 app 在下載大文件時(shí),程序因各種原因被迫中斷了,那么下次再重啟時(shí),文件是否還需要重頭開始下載?

Q3:你的 app 下載大文件時(shí),支持暫停并恢復(fù)下載么?即使這兩個(gè)操作分布在程序進(jìn)程被殺前后。

理論基礎(chǔ)

講之前,先來通俗的解釋下什么是斷點(diǎn)續(xù)傳

說得白一點(diǎn),其實(shí)也就是下載文件時(shí),不必重頭開始下載,而是從指定的位置繼續(xù)下載,這樣的功能就叫做斷點(diǎn)續(xù)傳。

既然如此,那么要實(shí)現(xiàn)斷點(diǎn)續(xù)傳的關(guān)鍵點(diǎn)其實(shí)也就是兩點(diǎn):

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

是吧,理論上來講,當(dāng)這兩點(diǎn)都可以做到的時(shí)候,自然就可以實(shí)現(xiàn)斷點(diǎn)續(xù)傳了。那么,要如何做到呢?

其實(shí),也很簡單,并不需要我們自己去寫一些什么,HTTP 協(xié)議本身就支持?jǐn)帱c(diǎn)續(xù)傳了,所以借助它就可以實(shí)現(xiàn)告知服務(wù)端,從指定位置下載的功能了。

而另一點(diǎn),就更簡單了,文件是下載到客戶端設(shè)備上的,那么只要獲取到這份下載到一半的文件,看一下它目前的大小,也就知道需要讓服務(wù)端從哪開始繼續(xù)下載了。

那么,下面就介紹一下涉及到的相關(guān)理論:

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

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

Range

當(dāng)請求頭中出現(xiàn) Range 字段時(shí),表示告知服務(wù)端,客戶端下載該文件想要從指定的位置開始下載,至于 Range 字段屬性值的格式有以下幾種:

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

當(dāng) app 想實(shí)現(xiàn)縮短大文件的下載耗時(shí),可以開啟多個(gè)下載線程,每個(gè)線程只負(fù)責(zé)文件的一部分下載,當(dāng)所有線程下載結(jié)束后,將每個(gè)線程下載的文件按順序拼接成一個(gè)完整的文件,這樣就可以達(dá)到縮短下載大文件的耗時(shí)目的了。

那么,此時(shí),就可以使用 Range:bytes=501-1000 這種格式了,每個(gè)線程在各自的請求頭字段中,以這種格式加入相對應(yīng)的信息即可達(dá)到目的了。

如果 app 想實(shí)現(xiàn)斷點(diǎn)續(xù)傳,文件下載到一半被迫中斷,下次啟動還可以繼續(xù)接著上次進(jìn)度下載時(shí),那么此時(shí)可以使用 Range:bytes=500- 這種格式了,只要先獲取本地那份文件目前的大小,通過在請求頭中加入 Range 字段信息即可。

Content-Length

Content-Length 字段出現(xiàn)在響應(yīng)頭中,用于告知客戶端此次下載的文件大小。

一般,如果客戶端需要實(shí)現(xiàn)下載進(jìn)度實(shí)時(shí)更新時(shí),就需要知道文件的總大小和目前下載的大小,后者可以通過對本地文件的操作得知,前者一般就是通過響應(yīng)頭中的 Content-Length 字段得知。

另外,如果想要實(shí)現(xiàn)多線程同時(shí)分段下載大文件功能時(shí),顯然在下載前,客戶端需要先知道文件總大小,才可以做到動態(tài)進(jìn)行分段,因此一般在下載前都會先發(fā)送一個(gè)不需要攜帶 body 信息請求,用于先獲取響應(yīng)頭中的 Content-Length 字段來得知文件總大小。

但有一點(diǎn)需要注意:Content-Length 只表示此鏈接中下載的文件大小

什么意思,也就是說,如果這條鏈接是一次性將整個(gè)文件下載下來的,那么 Content-Length 就表示這個(gè)文件的總大小。

但,如果這條鏈接指定了 Range,表明了只是下載文件的指定部分的內(nèi)容,那么此時(shí) Content-Length 表示的就只是這一部分的大小。

所以,如果客戶端實(shí)現(xiàn)了下載進(jìn)度實(shí)時(shí)更新功能時(shí),需要注意一下。因?yàn)槿绻募菙帱c(diǎn)續(xù)傳的,那么進(jìn)度條的分母就不能用每次 HTTP 鏈接中的 Content-Length。要么下載前先發(fā)一條獲取用于文件總大小的請求,然后一直維護(hù)著這個(gè)數(shù)據(jù),要么就使用 Content-Range 字段。

Content-Range

Content-Range 字段也是出現(xiàn)在響應(yīng)頭中,用于告知客戶端此鏈接下載的文件是哪個(gè)部分的,以及文件的總大小。

比如,當(dāng)客戶端在請求頭中指定了 Range:bayes=501-1000 來下載一個(gè)總大小為 2000 字節(jié)文件的中間一部分內(nèi)容時(shí),此時(shí),響應(yīng)頭中的 Content-Range 字段信息如下:

Content-Range:bytes 501-1000/2000

斜杠前表示此鏈接下載的文件是哪一部分,斜杠后表示文件的總大小。

If-Range

斷點(diǎn)續(xù)傳,說白點(diǎn)也就是分多次下載,既然不是一次性下載,那么就無法保證多次下載的間隔。

也就是說,有可能出現(xiàn)這種場景,這次由于某些原因只下載的一部分,而下次重啟繼續(xù)下載,但可能等到過了很多天后才重啟去繼續(xù)下載,如果在這期間,服務(wù)端的這份文件更新了怎么辦?

只要不是一次性下載的,那么就有可能會出現(xiàn)這種場景,顯然,這時(shí)候,就不希望斷點(diǎn)續(xù)傳了,而是要讓客戶端直接重頭開始下載,畢竟文件都已經(jīng)發(fā)生更新了,不是同一份了,再繼續(xù)恢復(fù)下載也沒有什么意義。

那么,客戶端要如何知道服務(wù)端的文件是否發(fā)生變化,要重頭下載呢?

這時(shí)就可以結(jié)合 If-Range 字段來實(shí)現(xiàn)了,這個(gè)也是在請求頭中的字段,跟 Range 字段一起使用,它的作用是給 Range 字段生效設(shè)置了一些條件,只有滿足這些條件,Range 才能生效。

也就是說,只有先滿足 If-Range,那么才能通過 Range 來實(shí)現(xiàn)斷點(diǎn)續(xù)傳。

那它的條件值可以設(shè)置為哪些呢?有兩種,Last-Modified 或者 ETag,這兩個(gè)也都是響應(yīng)頭中的字段。

具體可以參考這篇文章:MDN If-Range

抓包示例

以上就是斷點(diǎn)續(xù)傳相關(guān)的理論基礎(chǔ),下面抓個(gè)包,看看請求頭和響應(yīng)頭中的信息,來總結(jié)一下理論基礎(chǔ)。

斷點(diǎn)續(xù)傳.png

首先先發(fā)起一個(gè)請求,設(shè)置了不攜帶 BODY 信息,這樣就可以在下載前先獲取到文件的總大小。至于怎么設(shè)置不攜帶 BODY 信息,不同的網(wǎng)絡(luò)框架不同,具體下節(jié)代碼示例中說明。

斷點(diǎn)續(xù)傳2.png

這是下載中斷后,重啟想要繼續(xù)下載時(shí)發(fā)起的請求信息,請求頭中指定了 Range:bytes=12341380- 表示本地已經(jīng)下載了這么多,需要從這里開始繼續(xù)往下下載。

響應(yīng)頭中返回了這部分的內(nèi)容,并在 Content-Length 和 Content-Range 字段中給出了相關(guān)信息。

代碼示例

理論基礎(chǔ)掌握了,那么下面就是來看看代碼怎么實(shí)現(xiàn)。不管用什么語言,使用了什么網(wǎng)絡(luò)框架,要寫的代碼都有兩個(gè)部分:

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

文件處理操作有兩個(gè)關(guān)鍵點(diǎn),一是獲取文件大小,二是以追加的方式寫文件。添加請求頭的操作則是參考各自網(wǎng)絡(luò)框架的指示即可。

下面介紹了三種示例,分別是 C++&libcurl,Android&HttpURLConnection,Android&OkHttp。&前面是語言,后面是所使用的網(wǎng)絡(luò)框架。

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

//下載前先發(fā)送一次請求,獲取文件的總大小
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. 獲取本地已下載的大小,有則斷點(diǎn)續(xù)傳
    curl_off_t localFileLenth = getLocalFileLength();
    //2. 以追加的方式寫入文件
    FILE *file = fopen(mLocalFilePath, "ab+");
    CURL* mHandler = curl_easy_init();
    if (mHandler && file)
    {
         //3. 設(shè)置url
        curl_easy_setopt(mHandler, CURLOPT_URL, mDownloadFileUrl);
        //4. 設(shè)置請求頭 Range 字段信息,localFileLength 不等于0時(shí),值大小就表示從哪開始下載 
        curl_easy_setopt(mHandler, CURLOPT_RESUME_FROM_LARGE, localFileLenth);
        
        //5. 設(shè)置接收數(shù)據(jù)的處理函數(shù)和存放變量
        curl_easy_setopt(mHandler, CURLOPT_WRITEFUNCTION, writeFile);
        curl_easy_setopt(mHandler, CURLOPT_WRITEDATA, file);
        // 6. 發(fā)起請求
        CURLcode rel = curl_easy_perform(mHandler);
        fclose(file);
        return rel;
    }
    curl_easy_cleanup(mHandler);
    return CURLE_FAILED_INIT;
}

writeFile 函數(shù)和下載進(jìn)度通知的函數(shù)我都沒貼,用過 libcurl 的應(yīng)該都知道怎么寫,或者網(wǎng)上搜一下,資料很多。上面就是將斷點(diǎn)續(xù)傳的幾個(gè)關(guān)鍵函數(shù)貼出來,理清楚了即可。

Android&HttpURLConnection

Android&OkHttp

由于最近都在忙 C++ 的項(xiàng)目了,Android 暫時(shí)還沒時(shí)間自己寫個(gè) demo 測試一下,所以先給幾篇網(wǎng)上找的鏈接占個(gè)坑,后續(xù)抽個(gè)時(shí)間自己再來寫個(gè) demo。

之所以列了這兩點(diǎn),是因?yàn)楦杏X目前 Android 中網(wǎng)絡(luò)框架大多都是用的 OkHttp 了,而下載文件還有很多都是用的 HttpURLConnection,所以這兩個(gè)都想研究一下,怎么寫斷點(diǎn)續(xù)傳。

Android多線程斷點(diǎn)續(xù)傳下載

Android使用OKHttp3實(shí)現(xiàn)下載(斷點(diǎn)續(xù)傳、顯示進(jìn)度)

兩篇我都有大概過了下,其實(shí)斷點(diǎn)續(xù)傳原理不難,真的蠻簡單的,所以實(shí)現(xiàn)上基本也大同小異,就是不同的網(wǎng)絡(luò)框架的 api 用法不同而已。以及,如何維護(hù)本地已下載文件的大小的思路,有的是直接去獲取文件對象查看,有的則是手動自己建個(gè)數(shù)據(jù)庫維護(hù)。


大家好,我是 dasu,歡迎關(guān)注我的公眾號(dasuAndroidTv),如果你覺得本篇內(nèi)容有幫助到你,可以轉(zhuǎn)載但記得要關(guān)注,要標(biāo)明原文哦,謝謝支持~


dasuAndroidTv2.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容