libcurl使用easy模式阻塞卡死等問題的完美解決


引言:

    由於要在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 如果你不需要httpbody部分(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.      編寫得到下載目標文件的大小的函數

[cpp]  view plain copy
 
  1. long getDownloadFileLenth(const char *url){  
  2.     long downloadFileLenth = 0;  
  3.     CURL *handle = curl_easy_init();  
  4.     curl_easy_setopt(handle, CURLOPT_URL, url);  
  5.     curl_easy_setopt(handle, CURLOPT_HEADER, 1);    //只需要header頭  
  6.     curl_easy_setopt(handle, CURLOPT_NOBODY, 1);    //不需要body  
  7.     if (curl_easy_perform(handle) == CURLE_OK) {  
  8.         curl_easy_getinfo(handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &downloadFileLenth);  
  9.     } else {  
  10.         downloadFileLenth = -1;  
  11.     }  
  12.     return downloadFileLenth;  
  13. }  

2.      下載中回調自己寫的得到下載進度值的函數

下載回調函數的原型應該為:

[cpp]  view plain copy
 
  1. int progressFunc(const char* flag, double dtotal, double dnow, double ultotal, double ulnow);  


a.      應該在外部聲明一個遠程下載文件大小的全局變量

[cpp]  view plain copy
 
  1. double downloadFileLenth = 0;  


為了斷點續傳還應該聲明一個本地文件大小的全局變量

[cpp]  view plain copy
 
  1. double localFileLenth = 0;  


b.      編寫一個得到進度值的函數getProgressValue()

[cpp]  view plain copy
 
  1. int getProgressValue(const char* flag, double dt, double dn, double ult, double uln){  
  2.     double showTotal, showNow;  
  3.     if (downloadFileLenth == 0){  
  4.         downloadFileLenth = getDownloadFileLenth(url);  
  5.     }  
  6.     showTotal = downloadFileLenth;  
  7.     if (localFileLenth == 0){  
  8.         localFileLenth = getLocalFileLenth(filePath);  
  9.     }  
  10.     showNow = localFileLenth + dn;  
  11.     //然后就可以調用你自己的進度顯示函數了, 這里假設已經有一個進度函數, 那么只需要傳遞當前下載值和總下載值即可.  
  12.     showProgressValue(showNow, showTotal);  
  13. }  

c.       在下載中進行三個下載參數的設置

[cpp]  view plain copy
 
  1. curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 0);  
  2.   
  3. curl_easy_setopt(handle, CURLOPT_PROGRESSFUNCTION, getProgressValue);  //設置回調的進度函數  
  4.   
  5. curl_easy_setopt(handle, CURLOPT_PROGRESSDATA, “flag”);   //此設置對應上面的const char *flag  


3.      斷點續傳

libcurl實現斷點續傳很簡單,只用兩步即可實現,一是要得到本地文件已下載的大小,通過函數getLocalFileLenth()方法來得到,二是設置CURLOPT_RESUME_FROM_LARGE參數的值為已下載本地文件大小.

得到本地文件大小的函數:

[cpp]  view plain copy
 
  1. long getLocalFileLenth(const char* localPath);  


設置下載點如下即可:

                    

[cpp]  view plain copy
 
    1. curl_easy_setopt(handle, CURLOPT_RESUME_FROM_LARGE, getLocalFileLenth(localFile));        

項目背景:

近來jwisp在做OMA-DMandroid終端側,要實現的功能包括FUMO, SCOMO下載管理.由於項目是基於funambol的開源代碼,在實現FUMOSCOMO,使用了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套方案改進出來的. 此代碼可以方案移植到各位讀者需要的環境中去, 只需進行小的參數的改變即可適應

源代碼在下一節附上

源代碼附上:

 

[cpp]  view plain copy
 
  1. //全局變量  
  2. bool resumeDownload = false;        //是否需要下載的標記位  
  3. long downloadFileLenth = 0;         //需要下載的總大小, 遠程文件的大小  
[cpp]  view plain copy
 
  1. /* 得到本地文件大小的函數, 若不是續傳則返回0, 否則返回指定路徑地址的文件大小 */  
  2. long getLocalFileLenth(const char* localPath){  
  3.     if (!resumeDownload){  
  4.         return 0;  
  5.     }  
  6.     return fs_open(localPath).fs_size();  
  7. }  
[cpp]  view plain copy
 
  1. /* 得到遠程文件的大小, 要下載的文件大小 */  
  2. long getDownloadFileLenth(const char *url){  
  3.     long downloadFileLenth = 0;  
  4.     CURL *handle = curl_easy_init();  
  5.     curl_easy_setopt(handle, CURLOPT_URL, url);  
  6.     curl_easy_setopt(handle, CURLOPT_HEADER, 1);    //只需要header頭  
  7.     curl_easy_setopt(handle, CURLOPT_NOBODY, 1);    //不需要body  
  8.     if (curl_easy_perform(handle) == CURLE_OK) {  
  9.         curl_easy_getinfo(handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &downloadFileLenth);  
  10.     } else {  
  11.         downloadFileLenth = -1;  
  12.     }  
  13.     return downloadFileLenth;  
  14. }  
[cpp]  view plain copy
 
  1. /* scomoDownload回調的計算進度條的函數 */  
  2. void getProgressValue(const char* localSize, double dt, double dn, double ult, double uln){  
  3.     double showTotal, showNow;  
  4.     showTotal = downloadFileLenth;  
  5.     int localNow = atoi (localSize.c_str());  
  6.     showNow = localNow + dn;  
  7.     showProgressBar(showTotal, showNow);  
  8. }  
[cpp]  view plain copy
 
  1. /* 直接進行下載的函數 */  
  2. public CurlCode scomoDownload(long timeout) {  
  3.     long localFileLenth = getLocalFileLenth();  
  4.     const char *localFileLenthStr;  
  5.     sprint(localFileLenthStr, %ld, localFileLenth);  
  6.     curl_easy_setopt(handle, CURLOPT_URL, mUrl);  
  7.     curl_easy_setopt(handle, CURLOPT_HEADER, 0);  
  8.     curl_easy_setopt(handle, CURLOPT_TIMEOUT, timeout);  
  9.     curl_easy_setopt(handle, CURLOPT_CONNECTIONTIMEOUT, 0);  
  10.     curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, &writeDataCallback);   
  11.     curl_easy_setopt(handle, CURLOPT_WRITEDATA, this);  
  12.     curl_easy_setopt(handle, CURLOPT_RESUME_FROM_LARGE, localFileLenth);   
  13.     curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 0);  
  14.     curl_easy_setopt(handle, CURLOPT_ PROGRESSFUNCTION, getProgressValue);   
  15.     curl_easy_setopt(handle, CURLOPT_PROGRESSDATA, localFileLenthStr);  
  16.     if (curl_easy_perform) {  
  17.         resumeDownload = true;  
  18.         return DS_FAILED;  
  19.     } else {  
  20.         resumeDownload = false;  
  21.         return DS_FINISHED;  
  22.     }  
  23. }  
[cpp]  view plain copy
 
  1. /* downloadControl函數用來控制整個下載過程的節奏, 控制下載的次數, 每次等待的時間等 */  
  2. public void downloadControler(){  
  3.     downloadFileLenth = getDownloadFileLenth();         //下載前得到要下載的文件大小賦值給全局變量  
  4.     int times = 605;                        //600次*50ms=5分鍾, 以此確保5分鍾內的重試次數, 而5次是正常下載的中斷次數, 意思即是5次內能正常完成下載.  
  5.     int count = 0;  
  6.     int timeout = 30;  
  7.     DSTATUS dstatus = DS_FAILED;  
  8.     while (count++ < times){  
  9.         status = scomoDownload(timeout);  
  10.         if (dstatus == DS_FINISHED){  
  11.             break;  
  12.         }  
  13.         Thread.sleep(500);              //每次下載中間間隔500毫秒  
  14.     }  
  15.     resumeDownload = false;             //不管下載成功或失敗, 完成while循環后將標志回位  
  16.     if (dstaus == DS_FINISHED) {  
  17.         updateApp();                    //執行軟件安裝的操作…  
  18.     }  
  19.     SAFE_DELETE(localFile);             //流程最后要確保本地文件刪除  
  20. }  

resumeDownload是一個非常重要的標記位,主要用來標識是否需要續傳下載在初始化時為false,在下載完成后也應回位成false, 下載過程中若因時間中斷未下載完成也為false.

處理下載中掉電后續傳也需要這個標記位在程序啟動時,進行檢測若上次沒下載完,修改標志位為true, 然后調用下載入口函數downloadController:

      if (scomo_status == 30){

             resumeDownload = true;

             downloadController();

}

 

若下載環境正常, 1個小時內可以完成的下載可以直接使用此方案來下載,不用修改控制但若是超過1小時的下載,需要將本方案進行改進基本上就是將605那里分開判斷600+x,其中600為每次斷網后應重試的次數, x為正常下載應該進行的計數,分別計算即可.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM