本篇文章已授權微信公眾號 dasu_Android(大蘇)獨家發布
這次想來講講斷點續傳,以前沒相關需求,所以一直沒去接觸,近階段了解了之后,其實並不復雜,那么也便來寫一篇記錄一下,分享給大伙,也方便自己后續查閱。
提問
Q1:如果你的 app 需要下載大文件,那么是否有方法可以縮短下載耗時?
Q2:如果你的 app 在下載大文件時,程序因各種原因被迫中斷了,那么下次再重啟時,文件是否還需要重頭開始下載?
Q3:你的 app 下載大文件時,支持暫停並恢復下載么?即使這兩個操作分布在程序進程被殺前后。
理論基礎
講之前,先來通俗的解釋下什么是斷點續傳:
說得白一點,其實也就是下載文件時,不必重頭開始下載,而是從指定的位置繼續下載,這樣的功能就叫做斷點續傳。
既然如此,那么要實現斷點續傳的關鍵點其實也就是兩點:
- 如何告知服務端,從指定的位置下載
- 如何知道客戶端想要的指定位置是多少
是吧,理論上來講,當這兩點都可以做到的時候,自然就可以實現斷點續傳了。那么,要如何做到呢?
其實,也很簡單,並不需要我們自己去寫一些什么,HTTP 協議本身就支持斷點續傳了,所以借助它就可以實現告知服務端,從指定位置下載的功能了。
而另一點,就更簡單了,文件是下載到客戶端設備上的,那么只要獲取到這份下載到一半的文件,看一下它目前的大小,也就知道需要讓服務端從哪開始繼續下載了。
那么,下面就介紹一下涉及到的相關理論:
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
抓包示例
以上就是斷點續傳相關的理論基礎,下面抓個包,看看請求頭和響應頭中的信息,來總結一下理論基礎。
首先先發起一個請求,設置了不攜帶 BODY 信息,這樣就可以在下載前先獲取到文件的總大小。至於怎么設置不攜帶 BODY 信息,不同的網絡框架不同,具體下節代碼示例中說明。
這是下載中斷后,重啟想要繼續下載時發起的請求信息,請求頭中指定了 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使用OKHttp3實現下載(斷點續傳、顯示進度)
兩篇我都有大概過了下,其實斷點續傳原理不難,真的蠻簡單的,所以實現上基本也大同小異,就是不同的網絡框架的 api 用法不同而已。以及,如何維護本地已下載文件的大小的思路,有的是直接去獲取文件對象查看,有的則是手動自己建個數據庫維護。
大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支持~