分享
 
 
 

WinInet API 的异步方式使用

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

作者邮箱:zhtrue@sina.com

异步方式并不是什么高深莫测的事物,WinInet API 更是大家耳熟能详。

如果你仔细看过 MSDN 和 internet 上关于 WinInet API 的文章,你会发现尽管在很多篇章中提到了异步方式的使用,但是大部分说明都只说可以使用,而没有说如何使用。尽管如此,还是有一些文章可以给我们很多的提示,我会在后面列出。

由于网络数据传输经常会消耗一定的时间,因此我们总是把这些可能消耗时间的操作放到一个单独的子线程,以免影响主线程正常的进行。可是当子线程发生长时间阻塞的时候,主线程由于某种原因需要退出,我们通常希望子线程能在主线程退出前正常退出。这时主线程就不得不 wait 子线程,这样就导致主线程也被阻塞了。当然,主线程可以不 wait 子线程而自行退出,还可以使用 TerminateThread 强行终止子线程,但是这样的后果通常是不可预料的,内存泄漏或许是最轻的一种危害了。

使用异步方式是解决这类问题的正确手段,下面我们根据一个实例来分析一下 WinInet API 异步方式的使用方法和注意事项。

我们的例子完成这样的功能:给定一个 URL (如:http://www.sina.com.cn/),使用 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. http://www.codeproject.com/internet/asyncwininet.asp

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>

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有