c++编写断点续传和多线程下载模块【转】
隨后,“斷點續傳”的概念就出來了,顧名思義,就是如果下載中斷,在重新建立連接后,跳過已經下載的部分,而只下載還沒有下載的部分。
無論“多線程下載”技術是否洪以容先生的發明,洪以容使得這項技術得到前所未有的關注是不爭的事實。在“網絡螞蟻”軟件流行開后,許多下載軟件也都紛紛 效仿,是否具?quot;多線程下載"技術、甚至能支持多少個下載線程都成了人們評測下載軟件的要素。"多線程下載"的基礎是WEB服務器支持遠程的隨機 讀取,也即支持"斷點續傳"。這樣,在下載時可以把文件分成若干部分,每一部分創建一個下載線程進行下載。
現在,不要說編寫專門的下載 軟件,在自己編寫的軟件中,加入下載功能有時也非常必要。如讓自己的軟件支持自動在線升級,或者在軟件中自動下載新的數據進行數據更新,這都是很有用、而 且很實用的功能。本文的主題即怎樣編寫一個支持"斷點續傳"和"多線程"的下載模塊。當然,下載的過程非常復雜,在一篇文章中難以全部闡明,所以,與下載 過程關系不直接的部分基本上都忽略了,如異常處理和網絡錯誤處理等,敬請各位讀者注意。我使用的開發環境是C++ Builder 5.0,使用其他開發環境或者編程語言的朋友請自行作適當修改。
HTTP協議簡介
下載文件是電腦與WEB服務器交互的過程,它們交互的"語言"的專業名稱是協議。傳送文件的協議有多種,最常用的是HTTP(超文本傳輸協議)和FTP(文件傳送協議),我采用的是HTTP。
HTTP協議最基本的命令只有三條:Get、Post和Head。Get從WEB服務器請求一個特定的對象,比如HTML頁面或者一個文件,WEB 服務器通過一個Socket連接發送此對象作為響應;Head命令使服務器給出此對象的基本描述,比如對象的類型、大小和更新時間。Post命令用于向 WEB服務器發送數據,通常使把信息發送給一個單獨的應用程序,經處理生成動態的結果返回給瀏覽器。下載即是通過Get命令實現。
基本的下載過程
編寫下載程序,可以直接使用Socket函數,但是這要求開發人員理解、熟悉TCP/IP協議。為了簡化Internet客戶端軟件的開 發,Windows提供了一套WinInet API,對常用的網絡協議進行了封裝,把開發Internet軟件的門檻大大降低了。我們需要使用的WinInet API函數如圖1所示,調用順序基本上是從上到下,其具體的函數原型請參考MSDN。
圖1
在使用這些函數時,必須嚴格區分它們使用的句柄。這些句柄的類型是一樣的,都是HINTERNET,但是作用不同,這一點非常讓人迷惑。按照這些句柄的產生順序和調用關系,可以分為三個級別,下一級的句柄由上一級的句柄得到。
InternetOpen是最先調用的函數,它返回的HINTERNET句柄級別最高,我習慣定義為hSession,即會話句柄。
InternetConnect使用hSession句柄,返回的是http連接句柄,我把它定義為hConnect。
HttpOpenRequest使用hConnect句柄,返回的句柄是http請求句柄,定義為hRequest。
HttpSendRequest、HttpQueryInfo、InternetSetFilePointer和InternetReadFile都使用HttpOpenRequest返回的句柄,即hRequest。
當這幾個句柄不再使用是,應該用函數InternetCloseHandle把它關閉,以釋放其占用的資源。
首先建立一個名為THttpGetThread、創建后自動掛起的線程模塊,我希望線程在完成后自動銷毀,所以在構造函數中設置:
FreeOnTerminate = True; // 自動刪除
并增加以下成員變量:
char?Buffer[HTTPGET_BUFFER_MAX+4];?//?數據緩沖區AnsiString?FURL;?//?下載對象的URL
AnsiString?FOutFileName;?//?保存的路徑和名稱
HINTERNET?FhSession;?//?會話句柄
HINTERNET?FhConnect;?//?http連接句柄
HINTERNET?FhRequest;?//?http請求句柄
bool?FSuccess;?//?下載是否成功
int?iFileHandle;?//?輸出文件的句柄
?
?、建⒘?lt;/p>
按照功能劃分,下載過程可以分為4部分,即建立連接、讀取待下載文件的信息并分析、下載文件和釋 放占用的資源。建立連接的函數如下,其中ParseURL的作用是從下載URL地址中取得主機名稱和下載的文件的WEB路 徑,DoOnStatusText用于輸出當前的狀態:
?
?
//初始化下載環境void?THttpGetThread::StartHttpGet(void)
{
??AnsiString?HostName,FileName;
??ParseURL(HostName,?FileName);
??try
??{
?//?1.建立會話
?FhSession?=?InternetOpen("http-get-demo",
?INTERNET_OPEN_TYPE_PRECONFIG,
?NULL,NULL,
?0);?//?同步方式
?if(?FhSession==NULL)throw(Exception("Error:InterOpen"));
?DoOnStatusText("ok:InterOpen");
?//?2.建立連接
?FhConnect=InternetConnect(FhSession,
?HostName.c_str(),
?INTERNET_DEFAULT_HTTP_PORT,
?NULL,NULL,
?INTERNET_SERVICE_HTTP,?0,?0);
?if(FhConnect==NULL)throw(Exception("Error:InternetConnect"));
?DoOnStatusText("ok:InternetConnect");
?//?3.初始化下載請求
?const?char?*FAcceptTypes?=?"*/*";
?FhRequest?=?HttpOpenRequest(FhConnect,
?"GET",?//?從服務器獲取數據
?FileName.c_str(),?//?想讀取的文件的名稱
?"HTTP/1.1",?//?使用的協議
?NULL,
?&FAcceptTypes,
?INTERNET_FLAG_RELOAD,
?0);
?if(?FhRequest==NULL)throw(Exception("Error:HttpOpenRequest"));
?DoOnStatusText("ok:HttpOpenRequest");
?//?4.發送下載請求
?HttpSendRequest(FhRequest,?NULL,?0,?NULL,?0);
?DoOnStatusText("ok:HttpSendRequest");
??}catch(Exception?&exception)
??{
?EndHttpGet();?//?關閉連接,釋放資源
?DoOnStatusText(exception.Message);
??}
}
//?從URL中提取主機名稱和下載文件路徑
void?THttpGetThread::ParseURL(AnsiString?&HostName,AnsiString?&FileName)
{
??AnsiString?URL=FURL;
??int?i=URL.Pos("http://");
??if(i>0)
??{
?URL.Delete(1,?7);
??}
??i=URL.Pos("/");
??HostName?=?URL.SubString(1,?i-1);
??FileName?=?URL.SubString(i,?URL.Length());
}
?
???? 可以看到,程序按照圖1中的順序,依次調用InternetOpen、InternetConnect、HttpOpenRequest函數得到3個相關的句柄,然后通過HttpSendRequest函數把下載的請求發送給WEB服務器。
InternetOpen的第一個參數是無關的,最后一個參數如果設置為INTERNET_FLAG_ASYNC,則將建立異步連接,這很有實際意義,考慮到本文的復雜程度,我沒有采用。但是對于需要更高下載要求的讀者,強烈建議采用異步方式。
HttpOpenRequest打開一個請求句柄,命令是"GET",表示下載文件,使用的協議是"HTTP/1.1"。
另外一個需要注意的地方是HttpOpenRequest的參數FAcceptTypes,表示可以打開的文件類型,我設置為"*/*"表示可以打開所有文件類型,可以根據實際需要改變它的值。
2、讀取待下載的文件的信息并分析
在發送請求后,可以使用HttpQueryInfo函數獲取文件的有關信息,或者取得服務器的信息以及服務器支持的相關操作。對于下載程序,最常用的是傳遞HTTP_QUERY_CONTENT_LENGTH參數取得文件的大小,即文件包含的字節數。模塊如下所示:
//?取得待下載文件的大小int?__fastcall?THttpGetThread::GetWEBFileSize(void)
{
??try
??{
?DWORD?BufLen=HTTPGET_BUFFER_MAX;
?DWORD?dwIndex=0;
?bool?RetQueryInfo=HttpQueryInfo(FhRequest,
?HTTP_QUERY_CONTENT_LENGTH,
?Buffer,?&BufLen,
?&dwIndex);
?if(?RetQueryInfo==false)?throw(Exception("Error:HttpQueryInfo"));
?DoOnStatusText("ok:HttpQueryInfo");
?int?FileSize=StrToInt(Buffer);?//?文件大小
?DoOnGetFileSize(FileSize);
??}catch(Exception?&exception)
??{
?DoOnStatusText(exception.Message);
??}
??return?FileSize;
}
?
????? 模塊中的DoOnGetFileSize是發出取得文件大小的事件。取得文件大小后,對于采用多線程的下載程序,可以按照這個值進行合適的文件分塊,確定每個文件塊的起點和大小。
3、下載文件的模塊
開始下載前,還應該先安排好怎樣保存下載結果。方法很多,我直接采用了C++ Builder提供的文件函數打開一個文件句柄。當然,也可以采用Windows本身的API,對于小文件,全部緩沖到內存中也可以考慮。
//?打開輸出文件,以保存下載的數據DWORD?THttpGetThread::OpenOutFile(void)
{
??try
??{
??if(FileExists(FOutFileName))
?DeleteFile(FOutFileName);
??iFileHandle=FileCreate(FOutFileName);
??if(iFileHandle==-1)?throw(Exception("Error:FileCreate"));
??DoOnStatusText("ok:CreateFile");
??}catch(Exception?&exception)
??{
?DoOnStatusText(exception.Message);
??}
??return?0;
}
//?執行下載過程
void?THttpGetThread::DoHttpGet(void)
{
??DWORD?dwCount=OpenOutFile();
??try
??{
?//?發出開始下載事件
?DoOnStatusText("StartGet:InternetReadFile");
?//?讀取數據
?DWORD?dwRequest;?//?請求下載的字節數
?DWORD?dwRead;?//?實際讀出的字節數
?dwRequest=HTTPGET_BUFFER_MAX;
?while(true)
?{
??Application->ProcessMessages();
??bool?ReadReturn?=?InternetReadFile(FhRequest,
?(LPVOID)Buffer,
?dwRequest,
?&dwRead);
??if(!ReadReturn)break;
??if(dwRead==0)break;
??//?保存數據
??Buffer[dwRead]='\0';
??FileWrite(iFileHandle,?Buffer,?dwRead);
??dwCount?=?dwCount?+?dwRead;
??//?發出下載進程事件
??DoOnProgress(dwCount);
?}
?Fsuccess=true;
??}catch(Exception?&exception)
??{
?Fsuccess=false;
?DoOnStatusText(exception.Message);
??}
??FileClose(iFileHandle);
??DoOnStatusText("End:InternetReadFile");
}
?下載過程并不復雜,與讀取本地文件一樣,執行一個簡單的循環。當然,如此方便的編程還是得益于微軟對網絡協議的封裝。
4、釋放占用的資源
這個過程很簡單,按照產生各個句柄的相反的順序調用InternetCloseHandle函數即可。
void?THttpGetThread::EndHttpGet(void){
??if(FConnected)
??{
?DoOnStatusText("Closing:InternetConnect");
?try
?{
??InternetCloseHandle(FhRequest);
??InternetCloseHandle(FhConnect);
??InternetCloseHandle(FhSession);
?}catch(){}
?FhSession=NULL;
?FhConnect=NULL;
?FhRequest=NULL;
?FConnected=false;
?DoOnStatusText("Closed:InternetConnect");
??}
}
???? 我覺得,在釋放句柄后,把變量設置為NULL是一種良好的編程習慣。在這個示例中,還出于如果下載失敗,重新進行下載時需要再次利用這些句柄變量的考慮。
5、功能模塊的調用
這些模塊的調用可以安排在線程對象的Execute方法中,如下所示:
void?__fastcall?THttpGetThread::Execute(){
??FrepeatCount=5;
??for(int?i=0;i<FRepeatCount;i++)
??{
?StartHttpGet();
?GetWEBFileSize();
?DoHttpGet();
?EndHttpGet();
?if(FSuccess)break;
??}
??//?發出下載完成事件
??if(FSuccess)DoOnComplete();
??else?DoOnError();
}
??? 這里執行了一個循環,即如果產生了錯誤自動重新進行下載,實際編程中,重復次數可以作為參數自行設置。
實現斷點續傳功能
在基本下載的代碼上實現斷點續傳功能并不是很復雜,主要的問題有兩點:
1、 檢查本地的下載信息,確定已經下載的字節數。所以應該對打開輸出文件的函數作適當修改。我們可以建立一個輔助文件保存下載的信息,如已經下載的字節數等。 我處理得較為簡單,先檢查輸出文件是否存在,如果存在,再得到其大小,并以此作為已經下載的部分。由于Windows沒有直接取得文件大小的API,我編 寫了GetFileSize函數用于取得文件大小。注意,與前面相同的代碼被省略了。
DWORD?THttpGetThread::OpenOutFile(void){
??……
??if(FileExists(FOutFileName))
??{
?DWORD?dwCount=GetFileSize(FOutFileName);
?if(dwCount>0)
?{
??iFileHandle=FileOpen(FOutFileName,fmOpenWrite);
??FileSeek(iFileHandle,0,2);?//?移動文件指針到末尾
??if(iFileHandle==-1)?throw(Exception("Error:FileCreate"));
??DoOnStatusText("ok:OpenFile");
??return?dwCount;
?}
?DeleteFile(FOutFileName);
??}
??……
}
?2、 在開始下載文件(即執行InternetReadFile函數)之前,先調整WEB上的文件指針。這就要求WEB服務器支持隨機讀取文件的操作,有些服務器對此作了限制,所以應該判斷這種可能性。對DoHttpGet模塊的修改如下,同樣省略了相同的代碼:
void?THttpGetThread::DoHttpGet(void){
??DWORD?dwCount=OpenOutFile();
??if(dwCount>0)?//?調整文件指針
??{
?dwStart?=?dwStart?+?dwCount;
?if(!SetFilePointer())?//?服務器不支持操作
?{
??//?清除輸出文件
??FileSeek(iFileHandle,0,0);?//?移動文件指針到頭部
?}
??}
??……
}
?多線程下載
要實現多線程下載,最主要的問題是下載線程的創建和管理,已經下載完成后文件的各個部分的準確合并,同時,下載線程也要作必要的修改。
1、下載線程的修改
為了適應多線程程序,我在下載線程加入如下成員變量:
int FIndex; // 在線程數組中的索引
DWORD dwStart; // 下載開始的位置
DWORD dwTotal; // 需要下載的字節數
DWORD FGetBytes; // 下載的總字節數
并加入如下屬性值:
__property?AnsiString?URL?=?{?read=FURL,?write=FURL?};__property?AnsiString?OutFileName?=?{?read=FOutFileName,?write=FOutFileName};
__property?bool?Successed?=?{?read=FSuccess};
__property?int?Index?=?{?read=FIndex,?write=FIndex};
__property?DWORD?StartPostion?=?{?read=dwStart,?write=dwStart};
__property?DWORD?GetBytes?=?{?read=dwTotal,?write=dwTotal};
__property?TOnHttpCompelete?OnComplete?=?{?read=FOnComplete,?write=FOnComplete?};
?同時,在下載過程DoHttpGet中增加如下處理,
void?THttpGetThread::DoHttpGet(void){
??……
??try
??{
?……
?while(true)
?{
??Application->ProcessMessages();
??//?修正需要下載的字節數,使得dwRequest?+?dwCount?<dwTotal;
??if(dwTotal>0)?//?dwTotal=0表示下載到文件結束
??{
?if(dwRequest+dwCount>dwTotal)
?dwRequest=dwTotal-dwCount;
??}
??……
??if(dwTotal>0)?//?dwTotal?<=0表示下載到文件結束
??{
?if(dwCount>=dwTotal)break;
??}
?}
??}
??……
??if(dwCount==dwTotal)FSuccess=true;
}
?2、建立多線程下載組件
我先建立了以TComponent為基類、名為THttpGetEx的組件模塊,并增加以下成員變量:
//?內部變量THttpGetThread?**HttpThreads;?//?保存建立的線程
AnsiString?*OutTmpFiles;?//?保存結果文件各個部分的臨時文件
bool?*FSuccesss;?//?保存各個線程的下載結果
//?以下是屬性變量
int?FHttpThreadCount;?//?使用的線程個數
AnsiString?FURL;
AnsiString?FOutFileName;
?各個變量的用途都如代碼注釋,其中的FSuccess的作用比較特別,下文會再加以詳細解釋。因為線程的運行具有不可逆性,而組件可能會連續地下載不同的 文件,所以下載線程只能動態創建,使用后隨即銷毀。創建線程的模塊如下,其中GetSystemTemp函數取得系統的臨時文件 夾,OnThreadComplete是線程下載完成后的事件,其代碼在其后介紹:
//?分配資源void?THttpGetEx::AssignResource(void)
{
??FSuccesss=new?bool[FHttpThreadCount];
??for(int?i=0;i<FHttpThreadCount;i++)
?FSuccesss[i]=false;
??OutTmpFiles?=?new?AnsiString[FHttpThreadCount];
??AnsiString?ShortName=ExtractFileName(FOutFileName);
??AnsiString?Path=GetSystemTemp();
??for(int?i=0;i<FHttpThreadCount;i++)
?OutTmpFiles[i]=Path+ShortName+"-"+IntToStr(i)+".hpt";
??HttpThreads?=?new?THttpGetThread?*[FHttpThreadCount];
}
//?創建一個下載線程
THttpGetThread?*?THttpGetEx::CreateHttpThread(void)
{
??THttpGetThread?*HttpThread=new?THttpGetThread(this);
??HttpThread->URL=FURL;
??……?//?初始化事件
??HttpThread->OnComplete=OnThreadComplete;?//?線程下載完成事件
??return?HttpThread;
}
//?創建下載線程數組
void?THttpGetEx::CreateHttpThreads(void)
{
??AssignResource();
??//?取得文件大小,以決定各個線程下載的起始位置
??THttpGetThread?*HttpThread=CreateHttpThread();
??HttpThreads[FHttpThreadCount-1]=HttpThread;
??int?FileSize=HttpThread->GetWEBFileSize();
??//?把文件分成FHttpThreadCount塊
??int?AvgSize=FileSize/FHttpThreadCount;
??int?*Starts=?new?int[FHttpThreadCount];
??int?*Bytes?=?new?int[FHttpThreadCount];
??for(int?i=0;i<FHttpThreadCount;i++)
??{
?Starts[i]=i*AvgSize;
?Bytes[i]?=AvgSize;
??}
??//?修正最后一塊的大小
??Bytes[FHttpThreadCount-1]=AvgSize+(FileSize-AvgSize*FHttpThreadCount);
??//?檢查服務器是否支持斷點續傳
??HttpThread->StartPostion=Starts[FHttpThreadCount-1];
??HttpThread->GetBytes=Bytes[FHttpThreadCount-1];
??bool?CanMulti=HttpThread->SetFilePointer();
??if(CanMulti==false)?//?不支持,直接下載
??{
?FHttpThreadCount=1;
?HttpThread->StartPostion=0;
?HttpThread->GetBytes=FileSize;
?HttpThread->Index=0;
?HttpThread->OutFileName=OutTmpFiles[0];
??}else
??{
?HttpThread->OutFileName=OutTmpFiles[FHttpThreadCount-1];
?HttpThread->Index=FHttpThreadCount-1;
?//?支持斷點續傳,建立多個線程
?for(int?i=0;i<FHttpThreadCount-1;i++)
?{
??HttpThread=CreateHttpThread();
??HttpThread->StartPostion=Starts[i];
??HttpThread->GetBytes=Bytes[i];
??HttpThread->OutFileName=OutTmpFiles[i];
??HttpThread->Index=i;
??HttpThreads[i]=HttpThread;
?}
??}
??//?刪除臨時變量
??delete?Starts;
??delete?Bytes;
}
?下載文件的下載的函數如下:
void?__fastcall?THttpGetEx::DownLoadFile(void){
??CreateHttpThreads();
??THttpGetThread?*HttpThread;
??for(int?i=0;i<FHttpThreadCount;i++)
??{
?HttpThread=HttpThreads[i];
?HttpThread->Resume();
??}
}
?線程下載完成后,會發出OnThreadComplete事件,在這個事件中判斷是否所有下載線程都已經完成,如果是,則合并文件的各個部分。應該注意, 這里有一個線程同步的問題,否則幾個線程同時產生這個事件時,會互相沖突,結果也會混亂。同步的方法很多,我的方法是創建線程互斥對象。
const?char?*MutexToThread="http-get-thread-mutex";void?__fastcall?THttpGetEx::OnThreadComplete(TObject?*Sender,?int?Index)
{
??//?創建互斥對象
??HANDLE?hMutex=?CreateMutex(NULL,FALSE,MutexToThread);
??DWORD?Err=GetLastError();
??if(Err==ERROR_ALREADY_EXISTS)?//?已經存在,等待
??{
?WaitForSingleObject(hMutex,INFINITE);//8000L);
?hMutex=?CreateMutex(NULL,FALSE,MutexToThread);
??}
??//?當一個線程結束時,檢查是否全部認為完成
??FSuccesss[Index]=true;
??bool?S=true;
??for(int?i=0;i<FHttpThreadCount;i++)
??{
?S?=?S?&&?FSuccesss[i];
??}
??ReleaseMutex(hMutex);
??if(S)//?下載完成,合并文件的各個部分
??{
?//?1.?復制第一部分
?CopyFile(OutTmpFiles[0].c_str(),FOutFileName.c_str(),false);
?//?添加其他部分
?int?hD=FileOpen(FOutFileName,fmOpenWrite);
?FileSeek(hD,0,2);?//?移動文件指針到末尾
?if(hD==-1)
?{
??DoOnError();
??return;
?}
?const?int?BufSize=1024*4;
?char?Buf[BufSize+4];
?int?Reads;
?for(int?i=1;i<FHttpThreadCount;i++)
?{
??int?hS=FileOpen(OutTmpFiles[i],fmOpenRead);
??//?復制數據
??Reads=FileRead(hS,(void?*)Buf,BufSize);
??while(Reads>0)
??{
?FileWrite(hD,(void?*)Buf,Reads);
?Reads=FileRead(hS,(void?*)Buf,BufSize);
??}
??FileClose(hS);
?}
?FileClose(hD);
??}
}
?結語
?到此,多線程下載的關鍵部分就介紹完了。但是在實際應用時,還有許多應該考慮的因素,如網絡速度、斷線等等都是必須 考慮的。當然還有一些細節上的考慮,但是限于篇幅,就難以一一寫明了。如果讀者朋友能夠參照本文編寫出自己滿意的下載程序,我也就非常欣慰了。我也非常希 望讀者能由此與我互相學習,共同進步。
?
?
?
?
?
?
?
?
?
轉載于:https://www.cnblogs.com/NeuqUstcIim/archive/2008/08/19/1271394.html
總結
以上是生活随笔為你收集整理的c++编写断点续传和多线程下载模块【转】的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 数据挖掘,机器学习,统计学习的区别与联系
- 下一篇: 基于电子病历的临床医疗大数据挖掘流程与方
