在WinInet编程中,同步的使用方法如下:
InternetOpen->InternetOpenUrl->HttpQueryInfo->InternetReadFile->InternetCloseHandle;在InternetOpenUrl和InternetReadFile时会导致程序阻塞,知道操作完成,同步的好处就是比较简单,调试方便。
异步的使用方法如下:
1)InternetOpen,需指定是异步;
2)InternetSetStatusCallback,设置回调;
3)InternetOpenUrl,需指定回调参数;
4)WaitForSingObject或WaitForMultipleObjects,接收信号量;
5)HttpQueryInfo;
6)InternetReadFileEx,需指定回调参数;
7)WaitForSingObject或WaitForMultipleObjects,接收信号量;
8)InternetSetStatusCallback,卸载回调;
9)InternetCloseHandle。
异步比同步要复杂了不少,重点在于回调函数。在回调中,系统会及时返回各种系统定义的HTTP消息,我们根据这些消息来设置某些信号量。在WaitForSingObject或WaitForMultipleObjects里,等待这些信号(当然也可以等待用户的取消动作)。当有正确的信号返回时,继续往下的操作。下面一个例子代码:上面的理论和代码同样适用与wince或windows mobile平台
#include<windows.h>
#include
<wininet.h>
#include
<iostream.h>
DWORD dwNumKSent;
DWORD dwNumKToSend;
DWORD dwNumBytesComplete
= 0
;
char lpOutBuf[1024
];
HANDLE hConnectedEvent, hRequestCompleteEvent;
HINTERNET hInstance, hConnect, hRequest;
char *lpszUrl, *
lpszServer;
BOOL bAllDone =
FALSE;
void
__stdcall Callback(HINTERNET hInternet,
DWORD dwContext,
DWORD dwInternetStatus,
LPVOID lpStatusInfo,
DWORD dwStatusInfoLen);
void main(int argc, char *
argv[])
{
if (argc != 4
)
{
cout << "Usage: sendreqexasync <server> <url> <size in kilobytes>" <<
endl;
cout << " Example: sendreqexasync www.foo.com /postfolder/upload.exe 256" <<
endl;
return
;
}
lpszServer = argv[1
];
lpszUrl = argv[2
];
dwNumKToSend = atoi(argv[3
]);
FillMemory(lpOutBuf, 1024, 'A'
);
hConnectedEvent =
CreateEvent(NULL, FALSE, FALSE, NULL);
hRequestCompleteEvent =
CreateEvent(NULL, FALSE, FALSE, NULL);
hInstance = InternetOpen("sendreqexasync"
,
INTERNET_OPEN_TYPE_PRECONFIG,
NULL,
NULL,
INTERNET_FLAG_ASYNC);
if (hInstance ==
NULL)
{
cout << "InternetOpen failed, error " <<
GetLastError();
return
;
}
if
(InternetSetStatusCallback(hInstance,
(INTERNET_STATUS_CALLBACK)&Callback) ==
INTERNET_INVALID_STATUS_CALLBACK)
{
cout << "InternetSetStatusCallback failed, error " <<
GetLastError();
return
;
}
hConnect =
InternetConnect(hInstance,
lpszServer,
INTERNET_DEFAULT_HTTP_PORT,
NULL,
NULL,
INTERNET_SERVICE_HTTP,
0
,
1
);
if (hConnect ==
NULL)
{
if (GetLastError() !=
ERROR_IO_PENDING)
{
cout << "InternetConnect failed, error " <<
GetLastError();
return
;
}
WaitForSingleObject(hConnectedEvent, INFINITE);
}
hRequest =
HttpOpenRequest(hConnect,
"POST"
,
lpszUrl,
NULL,
NULL,
NULL,
INTERNET_FLAG_RELOAD |
INTERNET_FLAG_NO_CACHE_WRITE,
2
);
if (hRequest ==
NULL)
{
if (GetLastError() !=
ERROR_IO_PENDING)
{
cout << "HttpOpenRequest failed, error " <<
GetLastError();
return
;
}
WaitForSingleObject(hRequestCompleteEvent, INFINITE);
}
INTERNET_BUFFERS IntBuff;
FillMemory(&IntBuff, sizeof(IntBuff), 0
);
IntBuff.dwStructSize= sizeof
(IntBuff);
IntBuff.dwBufferTotal = 1024*
dwNumKToSend;
IntBuff.lpcszHeader = "Content-Type: text/text/r/n"
;
IntBuff.dwHeadersLength =
lstrlen(IntBuff.lpcszHeader);
if (!
HttpSendRequestEx(hRequest,
&
IntBuff,
NULL,
0
,
2
))
{
if (GetLastError() !=
ERROR_IO_PENDING)
{
cout << "HttpSendRequestEx failed, error " <<
GetLastError();
return
;
}
cout << "HttpSendRequestEx called successfully" <<
endl;
cout.flush();
WaitForSingleObject(hRequestCompleteEvent, INFINITE);
}
for (dwNumKSent = 0; dwNumKSent < dwNumKToSend; dwNumKSent++
)
{
DWORD dwBytesWritten;
if(!
InternetWriteFile(hRequest,
lpOutBuf,
1024
,
&
dwBytesWritten))
{
if (GetLastError() !=
ERROR_IO_PENDING)
{
cout << "InternetWriteFile failed, error " <<
GetLastError();
return
;
}
else
{
cout
<< "InternetWriteFile completing asynchronously" <<
endl;
cout.flush();
WaitForSingleObject(hRequestCompleteEvent, INFINITE);
}
}
}
cout << "Calling HttpEndRequest" <<
endl;
cout.flush();
if (!HttpEndRequest(hRequest, NULL, HSR_INITIATE, 2
))
{
if (GetLastError() ==
ERROR_IO_PENDING)
{
cout << "HttpEndRequest called" <<
endl;
cout.flush();
WaitForSingleObject(hRequestCompleteEvent, INFINITE);
}
else
{
cout
<< "HttpEndRequest failed, error " << GetLastError() <<
endl;
return
;
}
}
cout << "------------------- Read the response -------------------" <<
endl;
char lpReadBuff[256
];
do
{
INTERNET_BUFFERS InetBuff;
FillMemory(
&InetBuff, sizeof(InetBuff), 0
);
InetBuff.dwStructSize = sizeof
(InetBuff);
InetBuff.lpvBuffer =
lpReadBuff;
InetBuff.dwBufferLength = sizeof(lpReadBuff) - 1
;
cout << "Calling InternetReadFileEx" <<
endl;
cout.flush();
if (!
InternetReadFileEx(hRequest,
&
InetBuff,
0, 2
))
{
if (GetLastError() ==
ERROR_IO_PENDING)
{
cout << "Waiting for InternetReadFile to complete" <<
endl;
cout.flush();
WaitForSingleObject(hRequestCompleteEvent, INFINITE);
}
else
{
cout
<< "InternetReadFileEx failed, error " <<
GetLastError();
cout.flush();
return
;
}
}
lpReadBuff[InetBuff.dwBufferLength] = 0
;
cout <<
lpReadBuff;
cout.flush();
if (InetBuff.dwBufferLength == 0
)
bAllDone =
TRUE;
} while (bAllDone ==
FALSE);
cout << endl << endl << "------------------- Request Complete ----------------" <<
endl;
}
void
__stdcall Callback(HINTERNET hInternet,
DWORD dwContext,
DWORD dwInternetStatus,
LPVOID lpStatusInfo,
DWORD dwStatusInfoLen)
{
cout << "Callback dwInternetStatus: " << dwInternetStatus << " Context: " << dwContext <<
endl;
cout.flush();
switch
(dwContext)
{
case 1: // Connection handle
if (dwInternetStatus ==
INTERNET_STATUS_HANDLE_CREATED)
{
INTERNET_ASYNC_RESULT *pRes = (INTERNET_ASYNC_RESULT *
)lpStatusInfo;
hConnect = (HINTERNET)pRes->
dwResult;
cout << "Connect handle created" <<
endl;
cout.flush();
SetEvent(hConnectedEvent);
}
break
;
case 2: // Request handle
switch
(dwInternetStatus)
{
case
INTERNET_STATUS_HANDLE_CREATED:
{
INTERNET_ASYNC_RESULT *pRes = (INTERNET_ASYNC_RESULT *
)lpStatusInfo;
hRequest = (HINTERNET)pRes->
dwResult;
cout << "Request handle created" <<
endl;
cout.flush();
}
break
;
case
INTERNET_STATUS_REQUEST_SENT:
{
DWORD *lpBytesSent = (DWORD*
)lpStatusInfo;
cout << "Bytes Sent: " << *lpBytesSent <<
endl;
dwNumBytesComplete += *
lpBytesSent;
}
break
;
case
INTERNET_STATUS_REQUEST_COMPLETE:
{
INTERNET_ASYNC_RESULT *pAsyncRes = (INTERNET_ASYNC_RESULT *
)lpStatusInfo;
cout << "Function call finished" <<
endl;
cout << "dwResult: " << pAsyncRes->dwResult <<
endl;
cout << "dwError: " << pAsyncRes->dwError <<
endl;
cout.flush();
SetEvent(hRequestCompleteEvent);
}
break
;
case
INTERNET_STATUS_RECEIVING_RESPONSE:
cout << "Receiving Response" <<
endl;
cout.flush();
break
;
case
INTERNET_STATUS_RESPONSE_RECEIVED:
{
DWORD *dwBytesReceived = (DWORD*
)lpStatusInfo;
cout << "Received " << *dwBytesReceived <<
endl;
cout.flush();
}
}
}
}
参考的异步类:
1
include <wininet.h>
2
#include <mmsystem.h>
3
4
class
AsyncWinINet
5
{
6
public
:
7
typedef void (*notify_fp)(const StringMap&
);
8
9
class
thread_info
10
{
11
public
:
12
thread_info(const String& _url, //请求下载的地址(in)
13
const StringMap& _request_headrs, //请求头request_headrs(in)
14
const notify_fp& _pfp, //下载进度通知回调函数指针
15
const StringMap&
_pfp_param,
16
String& _response_headrs, //返回头response_headrs(out)
17
const String& _saved_filename, //下载内容保存文件名(in)
18
String& _response_content, //返回内容(out)
19
size_t _read_content_size) //控制保存在response_content中内容的长度(in)) :
20
: request_headrs(_request_headrs), pfp(_pfp),
21
pfp_param(_pfp_param), //pfp函数传回参数
22
response_headrs(_response_headrs), saved_filename(_saved_filename),
23
response_content(_response_content), read_content_size(_read_content_size)
24
{
25
this->
response_headrs.clear();
26
this->
response_content.clear();
27
this->url =
StringUtil::EncodeURIComponent(_url);
28
for(int i = 0; i < 3; ++
i)
29
{
30
this->hEvent[i] =
CreateEvent(NULL,TRUE,FALSE,NULL);
31
}
32
}
33
34
HANDLE hThread;
35
DWORD dwThreadID;
36
HANDLE hCallbackThread;
37
DWORD dwCallbackThreadID;
38
HANDLE hEvent[3
];
39
LPVOID hInternet;
40
LPVOID hFile;
41
DWORD dwStatusCode;
42
DWORD dwContentLength;
43
44
String url; //请求下载的地址(in)
45
const StringMap& request_headrs; //请求头request_headrs(in)
46
const notify_fp& pfp; //下载进度通知回调函数指针
47
const StringMap& pfp_param; //pfp函数传回参数
48
49
String& response_headrs; //返回头response_headrs(out)
50
const String& saved_filename; //下载内容保存文件名(in)
51
String& response_content; //返回内容(out)
52
size_t read_content_size; //控制保存在response_content中内容的长度(in)
53
};
54
55
/*
******************************************************************************
56
* 函数:download
57
* 功能:下载,返回WinINet_ERR_CODE值
58
* 说明:关于notify_fp 类型说明: 函数的参数为StringMap类型,传回的变量名与变量值
59
* 2007-12
60
*******************************************************************************/
61
static DWORD download(const String& url, //请求下载的地址(in)
62
const StringMap& request_headrs, //请求头request_headrs(in)
63
const notify_fp& pfp, //下载进度通知回调函数指针
64
const StringMap& pfp_param, //pfp函数传回参数
65
String& response_headrs, //返回头response_headrs(out)
66
const String& saved_filename, //下载内容保存文件名(in)
67
String& response_content, //返回内容(out)
68
size_t read_content_size = 0); //控制保存在response_content中内容的长度(in)
69
70
protected
:
71
static BOOL WaitExitEvent(thread_info *
p);
72
static
DWORD WINAPI AsyncThread(LPVOID lpParameter);
73
static
DWORD WINAPI AsyncCallbackThread(LPVOID lpParameter);
74
static
VOID CALLBACK AsyncInternetCallback(HINTERNET hInternet,
75
DWORD dwContext,
76
DWORD dwInternetStatus,
77
LPVOID lpvStatusInformation,
78
DWORD dwStatusInformationLength);
79
80
};
81
1
#include "AsyncWinINet.h"
2
3
#include "stdafx.h"
4
5
#pragma comment(lib, "Winmm.lib"
)
6
#pragma comment(lib, "Wininet.lib"
)
7
8
DWORD AsyncWinINet::download(const Fagex::String &url, const Fagex::StringMap &
request_headrs,
9
const Fagex::AsyncWinINet::notify_fp &pfp, const Fagex::StringMap &pfp_param, Fagex::String &
response_headrs,
10
const Fagex::String &saved_filename, Fagex::String &
response_content, size_t read_content_size)
11
{
12
thread_info info(url, request_headrs, pfp,
13
pfp_param, response_headrs, saved_filename,
14
response_content, read_content_size);
15
16
info.hThread =
CreateThread(NULL,
17
0
,
18
AsyncWinINet::AsyncThread,
19
&
info,
20
NULL,
21
&
info.dwThreadID);
22
23
WaitForSingleObject(info.hThread, INFINITE); //等待子线程安全退出
24
CloseHandle(info.hThread);//关闭线程句柄
25
26
return
TRUE;
27
}
28
29
//---------------------------------------------------------------------
30
DWORD WINAPI AsyncWinINet::AsyncThread(LPVOID lpParameter)
31
{
32
thread_info* p = (thread_info*
)lpParameter;
33
34
//a. 使用标记 INTERNET_FLAG_ASYNC 初始化 InternetOpen
35
String user_agent("Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler ; .NET CLR 2.0.50727)"
);
36
StringMap iheadrs(p->request_headrs.begin(), p->
request_headrs.end());
37
StringMap::iterator it = iheadrs.find("User-Agent"
);
38
if(it == iheadrs.end()) iheadrs["User-Agent"] =
user_agent;
39
else user_agent = it->
second;
40
41
p->hInternet =
InternetOpen(user_agent.c_str(),
42
INTERNET_OPEN_TYPE_PRECONFIG,
43
NULL,
44
NULL,
45
INTERNET_FLAG_ASYNC);
46
47
//
ResetEvent(p->hEvent[0]);
48
//
p->hCallbackThread = CreateThread(NULL,
49
//
0,
50
//
AsyncWinINet::AsyncCallbackThread,
51
//
p,
52
//
NULL,
53
//
&p->dwCallbackThreadID);
54
//WaitForSingleObject(p->hEvent[0], INFINITE);//等待回调函数设置成功事件
55
InternetSetStatusCallback(p->
hInternet, AsyncWinINet::AsyncInternetCallback);
56
57
String sheadrs;
58
for(it = iheadrs.begin(); it != iheadrs.end(); ++
it)
59
{
60
sheadrs += it->first + ":" + it->
second;
61
if(it->second.find(StringUtil::enter) == String::npos) { sheadrs +=
StringUtil::enter; }
62
}
63
sheadrs +=
StringUtil::enter;
64
65
DWORD start_time =
timeGetTime();
66
ResetEvent(p->hEvent[0]); //重置句柄被创建事件
67
p->hFile = InternetOpenUrl(p->hInternet, p->
url.c_str(), sheadrs.c_str(), sheadrs.length(),
68
INTERNET_FLAG_DONT_CACHE |
INTERNET_FLAG_RELOAD, (DWORD)p);
69
70
FILE *fp = fopen(p->saved_filename.c_str(), "w+"
);
71
while(true
)
72
{
73
if (NULL == p->
hFile)
74
{
75
DWORD dwError =
::GetLastError();
76
if (ERROR_IO_PENDING == dwError || ERROR_SUCCESS ==
dwError)
77
{
78
if (WaitExitEvent(p)) { break
; }
79
}
80
else break
;
81
}
82
83
//读取返回文件头
84
DWORD dwLength = 0
;
85
LPVOID lpOutBuffer =
NULL;
86
while(true) //读取response_headrs数据
87
{
88
if(!HttpQueryInfo(p->
hFile, HTTP_QUERY_RAW_HEADERS_CRLF,
89
lpOutBuffer, &
dwLength, NULL))
90
{
91
DWORD err_code =
GetLastError();
92
if (err_code == ERROR_HTTP_HEADER_NOT_FOUND) break
;
93
else if(err_code ==
ERROR_INSUFFICIENT_BUFFER)
94
{
95
lpOutBuffer = new char
[dwLength];
96
continue
;
97
}
98
else break
;
99
}
100
break
;
101
}
102
if(lpOutBuffer !=
NULL)
103
{
104
p->response_headrs.append((char*
)lpOutBuffer,dwLength);
105
delete [] lpOutBuffer;
106
}
107
108
//e. 使用 HttpQueryInfo 分析头信息 HttpQueryInfo 使用非阻塞方式,所以不用等待
109
DWORD dwStatusSize = sizeof(p->
dwStatusCode);
110
if (FALSE == HttpQueryInfo(p->hFile, //获取返回状态码
111
HTTP_QUERY_STATUS_CODE |
HTTP_QUERY_FLAG_NUMBER,
112
&p->dwStatusCode, &dwStatusSize, NULL)) { break
; }
113
114
//判断状态码是不是 200
115
if (HTTP_STATUS_OK != p->dwStatusCode) break
;
116
117
StringMap msgMap(p->pfp_param.begin(), p->
pfp_param.end());
118
msgMap["url"] = p->
url;
119
120
//
获取返回的Content-Length
121
//
DWORD dwLengthSize = sizeof(p->dwContentLength);
122
//
if (FALSE == HttpQueryInfo(p->hFile,
123
//
HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER,
124
//
&p->dwContentLength, &dwLengthSize, NULL)) { p->dwContentLength = 0; }
125
126
//
f. 使用标记 IRF_ASYNC 读数据 InternetReadFileEx
127
//为了向主线程报告进度,我们设置每次读数据最多 1024 字节
128
129
char lpvBuffer[1024
];
130
p->dwContentLength = 0; //Content-Length: 202749
131
while(true
)
132
{
133
INTERNET_BUFFERS i_buf = {0
};
134
i_buf.dwStructSize = sizeof
(INTERNET_BUFFERS);
135
i_buf.lpvBuffer =
lpvBuffer;
136
i_buf.dwBufferLength = 1024
;
137
138
//重置读数据事件
139
ResetEvent(p->hEvent[0
]);
140
if (FALSE == InternetReadFileEx(p->hFile, &
i_buf, IRF_ASYNC, (DWORD)p))
141
{
142
if (ERROR_IO_PENDING ==
::GetLastError())
143
{
144
if (WaitExitEvent(p)) break
;
145
}
146
else break
;
147
}
148
else
149
{
150
//
在网络传输速度快,步长较小的情况下,InternetReadFileEx 经常会直接返回成功,
151
//因此要判断是否发生了用户要求终止子线程事件。
152
if (WAIT_OBJECT_0 == WaitForSingleObject(p->hEvent[2], 0
))
153
{
154
ResetEvent(p->hEvent[2
]);
155
break
;
156
}
157
}
158
159
if(i_buf.dwBufferLength == 0
)
160
{
161
DWORD time = timeGetTime() -
start_time;
162
if(time != 0
)
163
{
164
Real speed = (Real)p->
dwContentLength;
165
speed /= ((Real)time)/1000.0f
;
166
speed /= 1024.0f
;
167
msgMap["speed"] =
StringUtil::toString((DWORD)speed);
168
}
169
if(p->pfp) p->
pfp(msgMap);
170
break
;
171
}
172
if
(fp)
173
{
174
fwrite(i_buf.lpvBuffer, sizeof(char
), i_buf.dwBufferLength, fp);
175
}
176
if(p->read_content_size > p->
response_content.size())
177
{
178
p->response_content.append((char*
)i_buf.lpvBuffer, i_buf.dwBufferLength);
179
}
180
p->dwContentLength +=
i_buf.dwBufferLength;
181
}
182
break
;
183
}
184
185
if
(fp)
186
{
187
fflush(fp); fclose(fp); fp =
NULL;
188
}
189
190
if(p->
hFile)
191
{
192
InternetCloseHandle(p->hFile);//关闭 m_hFile
193
while (!WaitExitEvent(p)) //等待句柄被关闭事件或者要求子线程退出事件
194
{
195
ResetEvent(p->hEvent[0
]);
196
}
197
}
198
199
//设置子线程退出事件,通知回调线程退出
200
SetEvent(p->hEvent[2
]);
201
202
//
等待回调线程安全退出
203
//
WaitForSingleObject(p->hCallbackThread, INFINITE);
204
//
CloseHandle(p->hCallbackThread);
205
206
//注销回调函数
207
InternetSetStatusCallback(p->
hInternet, NULL);
208
InternetCloseHandle(p->
hInternet);
209
210
return
TRUE;
211
}
212
213
//------------------------------------------------------------------------------------
214
DWORD WINAPI AsyncWinINet::AsyncCallbackThread(LPVOID lpParameter)
215
{
216
thread_info *p = (thread_info*
)lpParameter;
217
InternetSetStatusCallback(p->
hInternet, AsyncWinINet::AsyncInternetCallback);
218
219
//通知子线程回调函数设置成功,子线程可以继续工作
220
SetEvent(p->hEvent[0
]);
221
222
//
等待用户终止事件或者子线程结束事件
223
//子线程结束前需要设置子线程结束事件,并等待回调线程结束
224
WaitForSingleObject(p->hEvent[2
], INFINITE);
225
226
return 0
;
227
}
228
229
//----------------------------------------------------------------------------
230
VOID CALLBACK AsyncWinINet::AsyncInternetCallback(HINTERNET hInternet,
231
DWORD dwContext,
232
DWORD dwInternetStatus,
233
LPVOID lpvStatusInformation,
234
DWORD dwStatusInformationLength)
235
{
236
thread_info* p = (thread_info*
)dwContext;
237
238
//在我们的应用中,我们只关心下面三个状态
239
switch
(dwInternetStatus)
240
{
241
//句柄被创建
242
case
INTERNET_STATUS_HANDLE_CREATED:
243
p->hFile =
(HINTERNET)(((LPINTERNET_ASYNC_RESULT)
244
(lpvStatusInformation))->
dwResult);
245
break
;
246
247
//句柄被关闭
248
case
INTERNET_STATUS_HANDLE_CLOSING:
249
SetEvent(p->hEvent[1
]);
250
break
;
251
252
//一个请求完成,比如一次句柄创建的请求,或者一次读数据的请求
253
case
INTERNET_STATUS_REQUEST_COMPLETE:
254
if (ERROR_SUCCESS ==
((LPINTERNET_ASYNC_RESULT)
255
(lpvStatusInformation))->
dwError)
256
{
257
//设置句柄被创建事件或者读数据成功完成事件
258
SetEvent(p->hEvent[0
]);
259
}
260
else
261
{
262
//如果发生错误,则设置子线程退出事件 这里也是一个陷阱,经常会忽视处理这个错误,
263
SetEvent(p->hEvent[2
]);
264
}
265
break
;
266
267
case
INTERNET_STATUS_CONNECTION_CLOSED:
268
SetEvent(p->hEvent[2
]);
269
break
;
270
271
}
272
}
273
274
//--------------------------------------------------------------------
275
BOOL AsyncWinINet::WaitExitEvent(thread_info *
p)
276
{
277
DWORD dwRet = WaitForMultipleObjects(3, p->
hEvent, FALSE, INFINITE);
278
279
switch
(dwRet)
280
{
281
case WAIT_OBJECT_0://句柄被创建事件或者读数据请求成功完成事件
282
case WAIT_OBJECT_0+1://句柄被关闭事件
283
case WAIT_OBJECT_0+2://用户要求终止子线程事件或者发生错误事件
284
break
;
285
}
286
return WAIT_OBJECT_0 !=
dwRet;
287
}
288
异步方式并不是什么高深莫测的事物,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