天天看點

講講斷點續傳那點兒事

Q1:如果你的 app 需要下載下傳大檔案,那麼是否有方法可以縮短下載下傳耗時?

Q2:如果你的 app 在下載下傳大檔案時,程式因各種原因被迫中斷了,那麼下次再重新開機時,檔案是否還需要重頭開始下載下傳?

Q3:你的 app 下載下傳大檔案時,支援暫停并恢複下載下傳麼?即使這兩個操作分布在程式程序被殺前後。

本篇文章已授權微信公衆号 dasu_Android(大蘇)獨家釋出

這次想來講講斷點續傳,以前沒相關需求,是以一直沒去接觸,近階段了解了之後,其實并不複雜,那麼也便來寫一篇記錄一下,分享給大夥,也友善自己後續查閱。

提問

理論基礎

講之前,先來通俗的解釋下什麼是斷點續傳:

說得白一點,其實也就是下載下傳檔案時,不必重頭開始下載下傳,而是從指定的位置繼續下載下傳,這樣的功能就叫做斷點續傳。

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

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

是吧,理論上來講,當這兩點都可以做到的時候,自然就可以實作斷點續傳了。那麼,要如何做到呢?

其實,也很簡單,并不需要我們自己去寫一些什麼,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多線程斷點續傳下載下傳

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

兩篇我都有大概過了下,其實斷點續傳原理不難,真的蠻簡單的,是以實作上基本也大同小異,就是不同的網絡架構的 api 用法不同而已。以及,如何維護本地已下載下傳檔案的大小的思路,有的是直接去擷取檔案對象檢視,有的則是手動自己建個資料庫維護。

大家好,我是 dasu,歡迎關注我的公衆号(dasuAndroidTv),如果你覺得本篇内容有幫助到你,可以轉載但記得要關注,要标明原文哦,謝謝支援~