使用 CInternetSession 封裝多線程 http 文件下載
作者:付黎
www.crazy-bit.com
源代碼下載
/*
URLDownloadToFile();
主要是下載升級(jí)包..............................
*/
如何下載一個(gè)http文件?我們當(dāng)然可以用socket自己實(shí)現(xiàn)http協(xié)議去做,但費(fèi)時(shí)費(fèi)力還易出bug,對(duì)于一個(gè)客戶端程序穩(wěn)定易維護(hù)是第一位的,所幸MS給我們提供了功能強(qiáng)大的internet API函數(shù)族,MFC的CInternetSession對(duì)它們進(jìn)行了一些簡(jiǎn)單的封裝,但如此簡(jiǎn)單的封裝對(duì)我等拿來(lái)主義者來(lái)說(shuō)只是個(gè)半成品。必須經(jīng)過(guò)再加工才能食用。
先來(lái)介紹一下CInternetSession的使用:
下面的代碼是讀取鏈接的基本方法:
// CInternetSession在遇到一些錯(cuò)誤時(shí)會(huì)拋出異常,因此必須包起來(lái)
TRY


{
CInternetSession sess ;

// 統(tǒng)一以二進(jìn)制方式下載
DWORD dwFlag = INTERNET_FLAG_TRANSFER_BINARY|INTERNET_FLAG_DONT_CACHE|INTERNET_FLAG_RELOAD ;
CHttpFile * pF = (CHttpFile*)sess.OpenURL(strFilename, 1, dwFlag); ASSERT(pF);
if (!pF)

{AfxThrowInternetException(1);}

// 得到文件大小
CString str ;
pF->QueryInfo (HTTP_QUERY_CONTENT_LENGTH, str) ;
int nFileSize = _ttoi(str) ;

char * p = new[nFileSize] ;
while (true)

{
// 每次下載8Kb
int n = pF->Read (p, (nFileSize < 8192) ? nFileSize : 8192) ;
if (n <= 0)
break ;
p += n ; nFileSize -= n ;
}
delete[] p ;
delete pF ;
}

CATCH_ALL(e)
{}
END_CATCH_ALL
這段代碼有一個(gè)問(wèn)題,在獲取文件大小這個(gè)地方,對(duì)于靜態(tài)網(wǎng)頁(yè) HTTP_QUERY_CONTENT_LENGTH 查詢會(huì)返回文件大小,但對(duì)于asp,php這樣的動(dòng)態(tài)網(wǎng)頁(yè),查詢會(huì)返回0。必須通過(guò)不斷的調(diào)用 CHttpFile::GetLength 來(lái)一點(diǎn)一點(diǎn)累加內(nèi)容,就像這樣:
int n = pF->GetLength() ;
while (n)


{
int * p = new BYTE[n] ;
pF->Read (p, n) ;
delete[] p ;
n = pF->GetLength() ;
}

不過(guò)網(wǎng)絡(luò)斷線同樣會(huì)讓 GetLength 返回0,必須把這種情況屏蔽掉。
if (n == 0)


{
DWORD dw ;
if (::InternetQueryDataAvailable ((HINTERNET)(*pF), &dw, 0, 0) && (dw == 0))

{
// 到這里就代表文件下載成功了
}
}


OK,我們已經(jīng)把機(jī)制摸清了,剩下就是把這些體力活全扔進(jìn)線程里,又一個(gè)麻煩產(chǎn)生了:線程里如何向外界通知事件(開(kāi)始下載,下載完成之類)呢?直接調(diào)用回調(diào)函數(shù)當(dāng)然可以,但這時(shí)回調(diào)函數(shù)是置于我們的線程中,造成在回調(diào)函數(shù)中對(duì)資源的訪問(wèn)必須非常小心,防止多線程沖突。下一步,加鎖同步...。
掙扎在多線程泥潭中的人已經(jīng)夠多的了,其實(shí)我們有一個(gè)更安全方便的方法,借助 SendMessage 把線程里的事件發(fā)送到窗口線程統(tǒng)一處理,windows會(huì)幫我們把所有消息排隊(duì)執(zhí)行,相當(dāng)于把多線程程序轉(zhuǎn)成了單線程^_^ (我一個(gè)同事把此類用于包含數(shù)百個(gè)線程的爬蟲(chóng)程序中,非常穩(wěn)定)
封裝結(jié)果及使用:
template<class T>
class FCDownloadFileWndBase : public T


{
public:
// 默認(rèn)構(gòu)造函數(shù)

FCDownloadFileWndBase ()
{}
// CDialog 構(gòu)造函數(shù)

FCDownloadFileWndBase (UINT nID, CWnd* pParent) : T(nID, pParent)
{}
// CFormView 構(gòu)造函數(shù)

FCDownloadFileWndBase (UINT nID) : T(nID)
{}

// 創(chuàng)建一個(gè)線程下載文件URL,如果URL正在下載中,此函數(shù)什么也不做立即返回
void DownloadFile (LPCTSTR strFileURL, int nPriority=THREAD_PRIORITY_IDLE) ;

protected:
// 檢查鏈接最后修改時(shí)間,有些服務(wù)器會(huì)禁止查看時(shí)間,strTime為空
// 用戶必須重載實(shí)現(xiàn)本接口,返回TRUE則繼續(xù)下載文件,返回FALSE則不再下載文件
virtual BOOL DownloadFile_OnCheckTime (CString strFileURL, CString strTime) =0 ;

// 當(dāng)鏈接成功下載完成后會(huì)調(diào)用此接口

virtual void DownloadFile_OnFinished (CString strFileURL, char* pBuffer, int nLength)
{}

// 當(dāng)IE設(shè)置代理服務(wù)器并且服務(wù)器需要帳號(hào)認(rèn)證時(shí)候回調(diào)

virtual void DownloadFile_OnProxyValidate (CString strFileURL, CString& strUsername, CString& strPassword)
{}

// 出現(xiàn)錯(cuò)誤時(shí)回調(diào)

virtual void DownloadFile_OnError (CString strFileURL)
{}

// 開(kāi)始下載一個(gè)鏈接

virtual void DownloadFile_OnStartDownload (CString strFileURL)
{}

// 當(dāng)前進(jìn)度,每下載一塊數(shù)據(jù)就會(huì)回調(diào)

virtual void DownloadFile_OnProgress (CString strFileURL, int nNow, int nTotal)
{}
};

使用起來(lái)非常簡(jiǎn)單,讓你的窗口從它派生,然后選擇你感興趣的事件重載之即可。
幾點(diǎn)說(shuō)明:
- 本類會(huì)自動(dòng)使用IE里的連接設(shè)置,如果代理服務(wù)器需要帳號(hào)驗(yàn)證,會(huì)回調(diào) DownloadFile_OnProxyValidate 讓用戶輸入帳號(hào)密碼;
- 因?yàn)槭褂昧四0妫圆恢С諱FC丑陋的dynamic機(jī)制:-( ,請(qǐng)把 DECLARE_DYNAMIC 和 IMPLEMENT_DYNAMIC 宏從你的類中移除。如果你需要運(yùn)行時(shí)類型檢查,可以用C++的RTTI機(jī)制dynamic_cast/typeid;
download article source from here:Download.rar