異步方式并不是什么高深莫測的事物,WinInet API 更是大家耳熟能詳。
如果你仔細看過(guò) MSDN 和 internet 上關(guān)于 WinInet API 的文章,你會(huì )發(fā)現盡管在很多篇章中提到了異步方式的使用,但是大部分說(shuō)明都只說(shuō)可以使用,而沒(méi)有說(shuō)如何使用。盡管如此,還是有一些文章可以給我們很多的提示,我會(huì )在后面列出。
由于網(wǎng)絡(luò )數據傳輸經(jīng)常會(huì )消耗一定的時(shí)間,因此我們總是把這些可能消耗時(shí)間的操作放到一個(gè)單獨的子線(xiàn)程,以免影響主線(xiàn)程正常的進(jìn)行??墒钱斪泳€(xiàn)程發(fā)生長(cháng)時(shí)間阻塞的時(shí)候,主線(xiàn)程由于某種原因需要退出,我們通常希望子線(xiàn)程能在主線(xiàn)程退出前正常退出。這時(shí)主線(xiàn)程就不得不 wait 子線(xiàn)程,這樣就導致主線(xiàn)程也被阻塞了。當然,主線(xiàn)程可以不 wait 子線(xiàn)程而自行退出,還可以使用 TerminateThread 強行終止子線(xiàn)程,但是這樣的后果通常是不可預料的,內存泄漏或許是最輕的一種危害了。
使用異步方式是解決這類(lèi)問(wèn)題的正確手段,下面我們根據一個(gè)實(shí)例來(lái)分析一下 WinInet API 異步方式的使用方法和注意事項。
我們的例子完成這樣的功能:給定一個(gè) URL (如:
http://www.sina.com.cn/),使用 HTTP 協(xié)議下載該網(wǎng)頁(yè)或文件。我們一共創(chuàng )建了三個(gè)線(xiàn)程:主線(xiàn)程負責創(chuàng )建下載子線(xiàn)程,并等待子線(xiàn)程返回消息;子線(xiàn)程則使用異步方式的 WinInet API 完成下載任務(wù),并在各個(gè)階段返回消息給主線(xiàn)程;子線(xiàn)程還會(huì )創(chuàng )建一個(gè)回調函數線(xiàn)程,其作用我們稍后解釋。
實(shí)例代碼中涉及到一些線(xiàn)程,消息,事件,錯誤處理的 API,由于不是我討論的內容,就不仔細說(shuō)明了。
1. 主線(xiàn)程工作流程
a. 創(chuàng )建下載子線(xiàn)程
m_hMainThread = ::CreateThread(NULL,
0,
AsyncMainThread,
this,
NULL,
&m_dwMainThreadID);
b. 等待子線(xiàn)程返回消息
MSG msg;
while (1)
{
::GetMessage(&msg, m_hWnd, 0, 0);
if (msg.message == WM_ASYNCGETHTTPFILE)
{ //子線(xiàn)程發(fā)回消息
switch(LOWORD(msg.wParam))
{
case AGHF_FAIL:
{
MessageBox(_T("下載行動(dòng)失敗結束!"));
return;
}
case AGHF_SUCCESS:
MessageBox(_T("下載行動(dòng)成功結束!"));
return;
case AGHF_PROCESS:
//下載進(jìn)度通知
break;
case AGHF_LENGTH:
//獲取下載文件尺寸通知
break;
}
}
DispatchMessage(&msg);
}
2. 下載子線(xiàn)程工作流程
a. 使用標記 INTERNET_FLAG_ASYNC 初始化 InternetOpen
m_hInternet = ::InternetOpen(m_szAgent,
INTERNET_OPEN_TYPE_PRECONFIG,
NULL,
NULL,
INTERNET_FLAG_ASYNC);
起步并不費勁,也不難理解,MSDN 上說(shuō)這樣設置之后,以后所有的 API 調用都是異步的了。
警惕......
看起來(lái)好像很簡(jiǎn)單,但是會(huì )有無(wú)數的陷阱等著(zhù)我們掉進(jìn)去。
b. 設置狀態(tài)回調函數 InternetSetStatusCallback
::InternetSetStatusCallback(m_hInternet, AsyncInternetCallback);
第一個(gè)陷阱就在這里等著(zhù)你呢,文獻[2]中提到使用一個(gè)單獨的線(xiàn)程來(lái)進(jìn)行這項設置,并解釋說(shuō)如果不這樣會(huì )有潛在的影響,而在其他文檔中卻沒(méi)有這樣使用的例子。盡管看起來(lái)多余,并且增加了一些復雜度,我們還是先把這種方法寫(xiě)出來(lái)再討論。子線(xiàn)程需要創(chuàng )建一個(gè)回調函數線(xiàn)程:
//重置回調函數設置成功事件
::ResetEvent(m_hEvent[0]);
m_hCallbackThread = ::CreateThread(NULL,
0,
AsyncCallbackThread,
this,
NULL,
&m_dwCallbackThreadID);
//等待回調函數設置成功事件
::WaitForSingleObject(m_hEvent[0], INFINITE);
回調函數線(xiàn)程的實(shí)現如下:
DWORD WINAPI CAsyncGetHttpFile::AsyncCallbackThread(LPVOID lpParameter)
{
CAsyncGetHttpFile * pObj = (CAsyncGetHttpFile*)lpParameter;
::InternetSetStatusCallback(pObj->m_hInternet, AsyncInternetCallback);
//通知子線(xiàn)程回調函數設置成功,子線(xiàn)程可以繼續工作
::SetEvent(pObj->m_hEvent[0]);
//等待用戶(hù)終止事件或者子線(xiàn)程結束事件
//子線(xiàn)程結束前需要設置子線(xiàn)程結束事件,并等待回調線(xiàn)程結束
::WaitForSingleObject(pObj->m_hEvent[2], INFINITE);
return 0;
}
確實(shí)復雜了很多吧,雖然我試驗的結果發(fā)現兩種設置方法都能正確工作,但是確實(shí)發(fā)現了這兩種設置方法產(chǎn)生的一些不同效果,遺憾的是我沒(méi)有弄清具體的原因。我推薦大家使用后一種方法。
c. 打斷一下子線(xiàn)程的流程,由于回調函數和上一部分的關(guān)系如此密切,我們來(lái)看看它的實(shí)現
void CALLBACK CAsyncGetHttpFile::AsyncInternetCallback(
HINTERNET hInternet,
DWORD dwContext,
DWORD dwInternetStatus,
LPVOID lpvStatusInformation,
DWORD dwStatusInformationLength)
{
CAsyncGetHttpFile * pObj = (CAsyncGetHttpFile*)dwContext;
//在我們的應用中,我們只關(guān)心下面三個(gè)狀態(tài)
switch(dwInternetStatus)
{
//句柄被創(chuàng )建
case INTERNET_STATUS_HANDLE_CREATED:
pObj->m_hFile = (HINTERNET)(((LPINTERNET_ASYNC_RESULT)
(lpvStatusInformation))->dwResult);
break;
//句柄被關(guān)閉
case INTERNET_STATUS_HANDLE_CLOSING:
::SetEvent(pObj->m_hEvent[1]);
break;
//一個(gè)請求完成,比如一次句柄創(chuàng )建的請求,或者一次讀數據的請求
case INTERNET_STATUS_REQUEST_COMPLETE:
if (ERROR_SUCCESS == ((LPINTERNET_ASYNC_RESULT)
(lpvStatusInformation))->dwError)
{ //設置句柄被創(chuàng )建事件或者讀數據成功完成事件
::SetEvent(pObj->m_hEvent[0]);
}
else
{ //如果發(fā)生錯誤,則設置子線(xiàn)程退出事件
//這里也是一個(gè)陷阱,經(jīng)常會(huì )忽視處理這個(gè)錯誤,
::SetEvent(pObj->m_hEvent[2]);
}
break;
}
}
d. 繼續子線(xiàn)程的流程,使用 InternetOpenUrl 完成連接并獲取下載文件頭信息
//重置句柄被創(chuàng )建事件
::ResetEvent(m_hEvent[0]);
m_hFile = ::InternetOpenUrl(m_hInternet,
m_szUrl,
NULL,
NULL,
INTERNET_FLAG_DONT_CACHE | INTERNET_FLAG_RELOAD,
(DWORD)this);
if (NULL == m_hFile)
{
if (ERROR_IO_PENDING == ::GetLastError())
{
if (WaitExitEvent())
{
return FALSE;
}
}
else
{
return FALSE;
}
}
等我們把 WaitExitEvent 函數的實(shí)現列出在來(lái)再解釋發(fā)生的一切:
BOOL CAsyncGetHttpFile::WaitExitEvent()
{
DWORD dwRet = ::WaitForMultipleObjects(3, m_hEvent, FALSE, INFINITE);
switch (dwRet)
{
//句柄被創(chuàng )建事件或者讀數據請求成功完成事件
case WAIT_OBJECT_0:
//句柄被關(guān)閉事件
case WAIT_OBJECT_0+1:
//用戶(hù)要求終止子線(xiàn)程事件或者發(fā)生錯誤事件
case WAIT_OBJECT_0+2:
break;
}
return WAIT_OBJECT_0 != dwRet;
}
在這里我們終于看到異步方式的巨大優(yōu)勢了,InternetOpenUrl 函數要完成域名解析,服務(wù)器連接,發(fā)送請求,接收返回頭信息等任務(wù),異步方式中 InternetOpenUrl 并不等待成功創(chuàng )建了 m_hFile 才返回,我們看到 m_hFile 是可以在回調函數中賦值的。如果 InternetOpenUrl 的返回值為 NULL 并且 GetLastError 返回 ERROR_IO_PENDING,我們使用 WaitForMultipleObjects 來(lái)等待請求的成功完成,這樣主線(xiàn)程就有機會(huì )在這個(gè)等待過(guò)程中終止子線(xiàn)程的操作。我真是迫不及待的想把主線(xiàn)程如何強行終止子線(xiàn)程的代碼列出來(lái)了:
//設置要求子線(xiàn)程結束事件
::SetEvent(m_hEvent[2]);
//等待子線(xiàn)程安全退出
::WaitForSingleObject(m_hMainThread, INFINITE);
//關(guān)閉線(xiàn)程句柄
::CloseHandle(m_hMainThread);
哈哈,不需要使用 TerminateThread 終止線(xiàn)程,一切都是安全的,可預料的。
我們再考慮一種情況,這種情況好得超乎你的想象,InternetOpenUrl 返回了一個(gè)非空的 m_hFile 怎么辦?呵呵,這說(shuō)明 InternetOpenUrl 已經(jīng)成功創(chuàng )建了一個(gè) m_hFile,并且沒(méi)有發(fā)生任何阻塞,都不用等待任何事件,直接繼續下一步吧。
最后需要說(shuō)明得是,InternetOpenUrl 的最后一個(gè)參數會(huì )被作為回調函數的第二個(gè)參數使用。并且哪怕在回調函數中不需要這個(gè)參數,這個(gè)值你也不能設置為 0,否則 InternetOpenUrl 將不會(huì )按照異步的方式工作。
到這里,我們已經(jīng)將 WinInet API 的異步方式使用的關(guān)鍵部分都展示了,你應該可以使用 WinInet API 的異步方式寫(xiě)出你自己的應用了。不過(guò)還是讓我們繼續完成這個(gè)實(shí)例的其他部分。
e. 使用 HttpQueryInfo 分析頭信息
DWORD dwStatusSize = sizeof(m_dwStatusCode);
if (FALSE == ::HttpQueryInfo(m_hFile,
HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER,
&m_dwStatusCode,
&dwStatusSize,
NULL)) //獲取返回狀態(tài)碼
{
return FALSE;
}
//判斷狀態(tài)碼是不是 200
if (HTTP_STATUS_OK != m_dwStatusCode)
{
return FALSE;
}
DWORD dwLengthSize = sizeof(m_dwContentLength);
if (FALSE == ::HttpQueryInfo(m_hFile,
HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER,
&m_dwContentLength,
&dwLengthSize,
NULL)) //獲取返回的Content-Length
{
return FALSE;
}
...//通知主線(xiàn)程獲取文件大小成功
需要說(shuō)明的是 HttpQueryInfo 并不進(jìn)行網(wǎng)絡(luò )操作,因此它不需要進(jìn)行異步操作的處理。
f. 使用標記 IRF_ASYNC 讀數據 InternetReadFileEx
//為了向主線(xiàn)程報告進(jìn)度,我們設置每次讀數據最多 1024 字節
for (DWORD i=0; i<m_dwContentLength; )
{
INTERNET_BUFFERS i_buf = {0};
i_buf.dwStructSize = sizeof(INTERNET_BUFFERS);
i_buf.lpvBuffer = new TCHAR[1024];
i_buf.dwBufferLength = 1024;
//重置讀數據事件
::ResetEvent(m_hEvent[0]);
if (FALSE == ::InternetReadFileEx(m_hFile,
&i_buf,
IRF_ASYNC,
(DWORD)this))
{
if (ERROR_IO_PENDING == ::GetLastError())
{
if (WaitExitEvent())
{
delete[] i_buf.lpvBuffer;
return FALSE;
}
}
else
{
delete[] i_buf.lpvBuffer;
return FALSE;
}
}
else
{
//在網(wǎng)絡(luò )傳輸速度快,步長(cháng)較小的情況下,
//InternetReadFileEx 經(jīng)常會(huì )直接返回成功,
//因此要判斷是否發(fā)生了用戶(hù)要求終止子線(xiàn)程事件。
if (WAIT_OBJECT_0 == ::WaitForSingleObject(m_hEvent[2], 0))
{
::ResetEvent(m_hEvent[2]);
delete[] i_buf.lpvBuffer;
return FALSE;
}
}
i += i_buf.dwBufferLength;
...//保存數據
...//通知主線(xiàn)程下載進(jìn)度
delete[] i_buf.lpvBuffer;
}
這里 InternetReadFileEx 的異步處理方式同 InternetOpenUrl 的處理方式類(lèi)似,我沒(méi)有使用 InternetReadFile 因為它沒(méi)有異步的工作方式。
g. 最后清理戰場(chǎng),一切都該結束了
//關(guān)閉 m_hFile
::InternetCloseHandle(m_hFile);
//等待句柄被關(guān)閉事件或者要求子線(xiàn)程退出事件
while (!WaitExitEvent())
{
::ResetEvent(m_hEvent[0]);
}
//設置子線(xiàn)程退出事件,通知回調線(xiàn)程退出
::SetEvent(m_hEvent[2]);
//等待回調線(xiàn)程安全退出
::WaitForSingleObject(m_hCallbackThread, INFINITE);
::CloseHandle(m_hCallbackThread);
//注銷(xiāo)回調函數
::InternetSetStatusCallback(m_hInternet, NULL);
::InternetCloseHandle(m_hInternet);
...//通知主線(xiàn)程子線(xiàn)程成功或者失敗退出
實(shí)例中,我們建立一個(gè)完整的 HTTP 下載程序,并且可以在主線(xiàn)程中對下載過(guò)程進(jìn)行完全的監控。我們使用了 WinInet API 中的這些函數:
InternetOpen
InternetSetStatusCallback
InternetOpenUrl
HttpQueryInfo
InternetReadFileEx
InternetCloseHandle
其中 InternetOpenUrl 和 InternetReadFileEx 函數是按照異步方式工作的,文獻[4]中列出了可以按照異步方式工作的 API:
FtpCreateDirectory
FtpDeleteFile
FtpFindFirstFile
FtpGetCurrentDirectory
FtpGetFile
FtpOpenFile
FtpPutFile
FtpRemoveDirectory
FtpRenameFile
FtpSetCurrentDirectory
GopherFindFirstFile
GopherOpenFile
HttpEndRequest
HttpOpenRequest
HttpSendRequestEx
InternetConnect
InternetOpenUrl
InternetReadFileEx
參考文獻:
1.
http://www.codeproject.com/internet/asyncwininet.asp2. MSDN: <Technical Articles\Web Development\Authoring and Programming\Advanced FTP, or Teaching Fido To Phetch>
3. MSDN: <Platform SDK Documentation\Web Development\Internet Development SDK\Win32 Internet Functions\Common Functions>
4. MSDN: <Platform SDK Documentation\Web Development\Internet Development SDK\Win32 Internet Functions\Tutorials\Calling Win32 Internet Functions Asynchronously>