使用WinInet API 进行异步编程

 
     
     
小鬼我最近要做一个断点继传、 多线程、稳定的http下载程序。 到现在,已经可以实现 多线程和同步的下载,可是我的程序还要让用户停止下载时可以实现顺利退出,于是我想用异步,可是 WinInet API 在文档中的说明不是很懂,尤其是 INTERNET_STATUS_CALLBACK 怎样使用,我的思路是这样的:InternetOpen with INTERNET_FLAG_ASYNC -> InternetSetStatusCallback -> InternetConnect -> HttpOpenRequest -> HttpSendRequestEx ->HttpEndRequest ->InternetReadFileEx 以上函数都是异步进行的,我想知道异步是什么进行的,最好是细节的,哪位高手指点一下,小鬼我感激不尽!又由于是断点继传和 多线程,所以我还想知道这里能不能用InternetSetFilePointer?要不我又什么办呢?谢谢先:-)
mousubinmsb五级(中级) 信誉:982005-9-15 19:45:51得分: 60
 
                  
         
         
异步方式并不是什么高深莫测的事物, 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; } }
Top
mousubinmsb五级(中级) 信誉:982005-9-15 19:45:58得分: 0
 
                  
         
         
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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值