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