引言:
由於要在android手機測進行DM開發, 其中最重要的就是FUMO和SCOMO下載, 下載使用的是linux開源庫libcurl. 於是就把libcurl的使用研究了一遍, 有些心得, 並解決了一些網友的阻塞卡死問題, 於是jwisp將經驗和方案分享給大家.
一共四篇:
使用libcurl進行文件下載類項目開發(一) libcurl簡單使用介紹
(二)使用libcurl實現獲取目標文件大小, 下載進度顯示, 斷點續傳等功能
(三)Libcurl中使用curl_easy_perform阻塞, 遇到無信號卡死的完美解決方案
(四)使用Libcurl下載文件,解決無信號中斷,下載中掉電恢復后斷點續傳問題的源代碼
Libcurl使用介紹:
四個關鍵函數:
1. curl_easy_init() 初始化curl環境,新建curl對象,返回對象句柄,使用舉例: CURL *handler = curl_easy_init();
2. curl_easy_setopt() 各種設置, 包括URL設置等,使用舉例:
curl_easy_setopt(handler, CURLOPT_URL, “www.baidu.com”),其中中間的參數是設置的類別,比較重要,后面會列舉說明.
3. curl_easy_perform() 開始執行下載操作, 若下載失敗會返回錯誤碼.例如: CURLcode code = curl_easy_perform(handler)
4. curl_easy_getinfo() 得到各種下載信息, 包括下載文件名,文件大小等,此操作必須放在curl_easy_perform()之后執行才能得到正確的值.使用舉例: long resultCode;
curl_easy_getinfo(handler, CURLINFO_RESPONSE_CODE, &resultCode);
中間的參數也在后面列舉
必要的參數說明
curl_easy_setopt()參數:
CURLOPT_URL 設置目標URL地址
CURLOPT_HEADER 是否包含http頭,包含則設置為一個非0值
CURLOPT_NOBODY 如果你不需要http的body部分(除header頭以外其他部分),設置此項為一個非0值
CURLOPT_TIMETOU 設置一個超時時間,若超過此時間perform會立即返回,返回碼為下載失敗對應錯誤碼,單位秒.注意此時間為從調用perform開始后的總的下載時間,舉例curl_easy_setopt(handler, CURLOPT_TIMEOUT, 30)
設置超時時間為30秒,即下載必須在30秒內完成,否則返回下載失敗
CURLOPT_CONNECTIONTIMEOUT 連接超時時間,單位秒.這個參數在easy curl下載中基本沒什么實用價值.
CURLOPT_RESUME_FROM_LARGE 從什么位置開始下載,斷點續傳主要使用此參數進行配置,使用非常簡單,只需要傳遞一個字節偏移量即可,例如
curl_easy_setopt(handler, CURLOPT_ RESUME_FROM, 0),表示從第0個字節開始下載, curl_easy_setopt(handler, CURLOPT_ RESUME_FROM, 500),從第500個字節開始下載
CURLOPT_RANGE 下載指定字節的文件塊,參數對應的值格式為X-Y,例如curl_easy_setopt(handler, CURLOPT_ RESUME_FROM, “500-999”),下載從500個字節開始到999字節結束的文件塊
CURLOPT_NOPROGRESS 如果不需要下載進度設置此項為一個非0值
CURLOPT_PROGRESSFUNCTION 設置回調的進度函數,設置后,會不斷的調用進度函數,並傳遞參數總大小和已下載大小給該函數
CURLOPT_PROGRESSDATA 設置傳遞給回調的進度函數的一個參數,類型為字符串類型,后面jwisp會舉例說明
curl_easy_getinfo() 部分參數說明
CURLINFO_RESPONSE_CODE 得到perform的執行結果碼
CURLINFO_CONTENT_LENGTH_DOWNLOAD 得到下載文件大小
本節jwisp為大家舉例說明如果使用上節介紹的函數和參數,在使用libcurl的過程中,如何獲取下載目標文件的大小, 下載進度條,斷點續傳等,這些基本的函數,將為jwisp在最后處理下載過程異常中斷等問題提供支持.
1. 編寫得到下載目標文件的大小的函數
- long getDownloadFileLenth(const char *url){
- long downloadFileLenth = 0;
- CURL *handle = curl_easy_init();
- curl_easy_setopt(handle, CURLOPT_URL, url);
- 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;
- }
- return downloadFileLenth;
- }
2. 下載中回調自己寫的得到下載進度值的函數
下載回調函數的原型應該為:
- int progressFunc(const char* flag, double dtotal, double dnow, double ultotal, double ulnow);
a. 應該在外部聲明一個遠程下載文件大小的全局變量
- double downloadFileLenth = 0;
為了斷點續傳, 還應該聲明一個本地文件大小的全局變量
- double localFileLenth = 0;
b. 編寫一個得到進度值的函數getProgressValue()
- int getProgressValue(const char* flag, double dt, double dn, double ult, double uln){
- double showTotal, showNow;
- if (downloadFileLenth == 0){
- downloadFileLenth = getDownloadFileLenth(url);
- }
- showTotal = downloadFileLenth;
- if (localFileLenth == 0){
- localFileLenth = getLocalFileLenth(filePath);
- }
- showNow = localFileLenth + dn;
- //然后就可以調用你自己的進度顯示函數了, 這里假設已經有一個進度函數, 那么只需要傳遞當前下載值和總下載值即可.
- showProgressValue(showNow, showTotal);
- }
c. 在下載中進行三個下載參數的設置
- curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 0);
- curl_easy_setopt(handle, CURLOPT_PROGRESSFUNCTION, getProgressValue); //設置回調的進度函數
- curl_easy_setopt(handle, CURLOPT_PROGRESSDATA, “flag”); //此設置對應上面的const char *flag
3. 斷點續傳
用libcurl實現斷點續傳很簡單,只用兩步即可實現,一是要得到本地文件已下載的大小,通過函數getLocalFileLenth()方法來得到,二是設置CURLOPT_RESUME_FROM_LARGE參數的值為已下載本地文件大小.
得到本地文件大小的函數:
- long getLocalFileLenth(const char* localPath);
設置下載點如下即可:
- curl_easy_setopt(handle, CURLOPT_RESUME_FROM_LARGE, getLocalFileLenth(localFile));
項目背景:
近來jwisp在做OMA-DM的android終端側,要實現的功能包括FUMO, SCOMO下載管理.由於項目是基於funambol的開源代碼,在實現FUMO和SCOMO時,使用了libcurl的庫來實現下載服務測的指定文件,下載后實現android固件升級或android應用下載安裝功能,最后將FUMO/SCOMO的下載安裝結果上報給服務端.
下載流程比較簡單, 使用curl_easy_perform即可實現完整的下載流程,安裝后,使用OMA DM協議上報給平台側.但是jwisp這里的需求還有兩個異常情況需要處理:
1. 下載過程中,遇手機突然掉電(操作:扣電池), android終端再次啟動后應能恢復現場然后自動下載,下載方式為建議斷點續傳
2. 下載過程中信號中斷,中斷時間在5分鍾之內,終端應嘗試重新連接,連接次數在3次以上.重新連接后建議使用斷點續傳方式繼續下載.
libcurl使用時疑難問題
在使用libcurl時, jwisp發現, curl_easy_perform是阻塞的方式進行下載的, curl_easy_perform執行后,程序會在這里阻塞等待下載結束(成功結束或者失敗結束).此時若正常下載一段時間后,進行網絡中斷, curl_easy_perform並不會返回失敗,而是阻塞整個程序卡在這里,此時即使網絡連接重新恢復, curl_easy_perform也無法恢復繼續下載,導致整個程序出現”死機”狀態.
但是若先斷網, 然后進行curl_easy_perform的話,會直接返回失敗,不會阻塞
在網上搜索后發現, 大家在網上遇到這個問題的很多,但是解決方法很少,下面jwisp就把網上建議的可以使用的解決方法羅列:
1. 使用multi模式下載,而不使用easy模式,此方法的唯一好處就是multi並不會阻塞,而是立即返回.但是缺點是帶來了問題,其一就是需要自己去阻塞,當我們需要返回時再返回,其二還需要啟動一個線程,需要自己控制整個過程的節奏.
2. 在下載中,另起一個線程,若發現下載狀態卡死(可以通過定期檢查文件大小來實現),則從外部中斷下載線程.此方法需另起線程,而且直接中斷線程,會給整個程序帶來不穩定.
在嘗試使用網上的方法失敗后, jwisp終於設計出了自己的方案,並完美解決信號中斷異常,下載中掉電異常,斷點續傳等問題.並且此方案不需要啟動任何另外的線程,不需要手動進行阻塞,在信號中斷后,恢復連接最快可在0.5秒內恢復下載.並且恢復下載方式全部為斷點續傳.
主要的設計思路如下, 下載過程中,設置超時時間為30秒, 30秒后若下載未完成就重新連接進行下載(這個可解決卡死問題),每次下載時進行判斷,若不是首次下載則獲得當前已下載文件大小,從該大小處進行續傳,若網絡仍處於斷開狀態,再次連接會立即返回失敗,此時讓當前線程等待0.5秒后進行連接(這個可以解決瞬間恢復連接的問題),連接次數不超過600次(這個用來保證5分鍾后返回失敗).掉電需要在程序已啟動時檢查是否上次未下載完如果是,則直接調用下載續傳方法即可.這樣基本上所有的問題的流程就都能順利走下來,並且下載過程體驗好,可隨時取消.
該方案主要通過兩個函數來實現, 一個負責進行斷點續傳和基本設置,並執行下載,一個負責控制整個下載重試次數,返回下載結果.並且需要注意的是,安裝完成后,應將相應的文件刪除掉.
此方案容易理解, 代碼比較簡單, 但是在此方案之前, jwisp試過不下10中解決方案, 最終這套方案就是由這10套方案改進出來的. 此代碼可以方案移植到各位讀者需要的環境中去, 只需進行小的參數的改變即可適應
源代碼在下一節附上
源代碼附上:
- //全局變量
- bool resumeDownload = false; //是否需要下載的標記位
- long downloadFileLenth = 0; //需要下載的總大小, 遠程文件的大小
- /* 得到本地文件大小的函數, 若不是續傳則返回0, 否則返回指定路徑地址的文件大小 */
- long getLocalFileLenth(const char* localPath){
- if (!resumeDownload){
- return 0;
- }
- return fs_open(localPath).fs_size();
- }
- /* 得到遠程文件的大小, 要下載的文件大小 */
- long getDownloadFileLenth(const char *url){
- long downloadFileLenth = 0;
- CURL *handle = curl_easy_init();
- curl_easy_setopt(handle, CURLOPT_URL, url);
- 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;
- }
- return downloadFileLenth;
- }
- /* scomoDownload回調的計算進度條的函數 */
- void getProgressValue(const char* localSize, double dt, double dn, double ult, double uln){
- double showTotal, showNow;
- showTotal = downloadFileLenth;
- int localNow = atoi (localSize.c_str());
- showNow = localNow + dn;
- showProgressBar(showTotal, showNow);
- }
- /* 直接進行下載的函數 */
- public CurlCode scomoDownload(long timeout) {
- long localFileLenth = getLocalFileLenth();
- const char *localFileLenthStr;
- sprint(localFileLenthStr, %ld, localFileLenth);
- curl_easy_setopt(handle, CURLOPT_URL, mUrl);
- curl_easy_setopt(handle, CURLOPT_HEADER, 0);
- curl_easy_setopt(handle, CURLOPT_TIMEOUT, timeout);
- curl_easy_setopt(handle, CURLOPT_CONNECTIONTIMEOUT, 0);
- curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, &writeDataCallback);
- curl_easy_setopt(handle, CURLOPT_WRITEDATA, this);
- curl_easy_setopt(handle, CURLOPT_RESUME_FROM_LARGE, localFileLenth);
- curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 0);
- curl_easy_setopt(handle, CURLOPT_ PROGRESSFUNCTION, getProgressValue);
- curl_easy_setopt(handle, CURLOPT_PROGRESSDATA, localFileLenthStr);
- if (curl_easy_perform) {
- resumeDownload = true;
- return DS_FAILED;
- } else {
- resumeDownload = false;
- return DS_FINISHED;
- }
- }
- /* downloadControl函數用來控制整個下載過程的節奏, 控制下載的次數, 每次等待的時間等 */
- public void downloadControler(){
- downloadFileLenth = getDownloadFileLenth(); //下載前得到要下載的文件大小賦值給全局變量
- int times = 605; //600次*50ms=5分鍾, 以此確保5分鍾內的重試次數, 而5次是正常下載的中斷次數, 意思即是5次內能正常完成下載.
- int count = 0;
- int timeout = 30;
- DSTATUS dstatus = DS_FAILED;
- while (count++ < times){
- status = scomoDownload(timeout);
- if (dstatus == DS_FINISHED){
- break;
- }
- Thread.sleep(500); //每次下載中間間隔500毫秒
- }
- resumeDownload = false; //不管下載成功或失敗, 完成while循環后將標志回位
- if (dstaus == DS_FINISHED) {
- updateApp(); //執行軟件安裝的操作…
- }
- SAFE_DELETE(localFile); //流程最后要確保本地文件刪除
- }
resumeDownload是一個非常重要的標記位,主要用來標識是否需要續傳下載, 在初始化時為false,在下載完成后也應回位成false, 下載過程中若因時間中斷未下載完成也為false.
處理下載中掉電后續傳也需要這個標記位, 在程序啟動時,進行檢測, 若上次沒下載完,修改標志位為true, 然后調用下載入口函數downloadController:
if (scomo_status == 30){
resumeDownload = true;
downloadController();
}
若下載環境正常, 1個小時內可以完成的下載可以直接使用此方案來下載,不用修改控制, 但若是超過1小時的下載,需要將本方案進行改進. 基本上就是將605那里分開判斷600+x,其中600為每次斷網后應重試的次數, x為正常下載應該進行的計數,分別計算即可.