WinInet是windows平台对socket进行一层封装,用来直接处理http/ftp/Gopher协议的一套windows API。我第一次接触这个是在一个客户那里,当时因为客户都需要使用http代理才能浏览网页,所以折腾了一天,才基本掌握。而时隔没多久又忘记的差不多了,这个流程就像正则表达式一样,常学常忘,常忘常学。而网络上流传的WinInet代码实例,不是编译不通过,就是粗制滥造。因此现在整理和归纳出一个基本的流程出来,用于记录和备忘。本文只介绍WinInet的http协议部分,关于ftp和Gopher在msdn上搜索WinInet即可找到。
一、WinInet使用流程
如上图所示:
1、首先必须依次调用InternetOpen()、InternetConnect()、InternetOpenRequest()产生三个HINTERNET句柄。
需要注意三点:
第一:后一个函数的第一个参数都是需要传入前一个函数生成的HINTERNET句柄,这也就是msdn上说的HINTERNET句柄的层级性(HTTP Hierarchy,see:https://msdn.microsoft.com/en-us/library/windows/desktop/aa383766(v=vs.85).aspx),也就是说InternetConnect()函数第一个参数必须是InternetOpen()函数返回的句柄,而InternetOpenRequest()函数第一个参数必须是InternetConnect()函数返回的句柄。
第二:当你不再需要使用这些句柄时,你需要调用InternetCloseHandle()函数关闭这些个HINTERNET句柄, 而关闭顺序和创建相反,也就是说先关闭InternetOpenRequest()创建的句柄,再关闭InternetConnect()创建的句柄,最后关闭InternetOpen()的句柄。
第三:msdn上说,在DllMain()函数或者全局对象的构造和析构函数里面调用InternetOpen()、InternetConnect()、InternetOpenRequest()和InternetCloseHandle()是不安全的:
Like all other aspects of the WinINet API, this function cannot be safely called from within DllMain or the constructors and destructors of global objects.
第一个函数HttpOpen()的函数签名如下:
HINTERNET InternetOpen(
_In_ LPCTSTR lpszAgent,
_In_ DWORD dwAccessType,
_In_ LPCTSTR lpszProxyName,
_In_ LPCTSTR lpszProxyBypass,
_In_ DWORD dwFlags
);
可以在这个函数lpszAgent参数里面设置所谓的UserAgent,常见的浏览器都有所谓的UserAgent,是一个字符串,例如IE8的UserAgent是:Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0。实际编码的时候,不用紧张,lpszAgent随便写一个你喜欢的字符串即可。
另外需要使用代理的http请求,也可以在lpszProxyName设置代理名称和lpszBypass中设置代理ip地址和端口号。当然你需要正确地设置dwAccessType 值,我第一次使用时就在这个参数上面吃过亏,导致怎么也连不上http服务器。
HttpOpenRequest()函数签名如下:
HINTERNET HttpOpenRequest( _In_ HINTERNET hConnect,
_In_ LPCTSTR lpszVerb,
_In_ LPCTSTR lpszObjectName,
_In_ LPCTSTR lpszVersion,
_In_ LPCTSTR lpszReferer,
_In_ LPCTSTR *lplpszAcceptTypes,
_In_ DWORD dwFlags,
_In_ DWORD_PTR dwContext);
BOOL HttpSendRequest( _In_ HINTERNET hRequest,
_In_ LPCTSTR lpszHeaders,
_In_ DWORD dwHeadersLength,
_In_ LPVOID lpOptional,
_In_ DWORD dwOptionalLength);
参数lpOptional指向的就是需要发送的数据缓冲区, 参数dwOptionalLength则是发送数据的长度。
同理对于加强版的HttpSendRequestEx()则在
BOOL HttpSendRequestEx( _In_ HINTERNET hRequest,
_In_ LPINTERNET_BUFFERS lpBuffersIn,
_Out_ LPINTERNET_BUFFERS lpBuffersOut,
_In_ DWORD dwFlags,
_In_ DWORD_PTR dwContext);
第二个参数lpBufferIn的lpvBuffer和dwBufferLength中设置,一个是缓冲区指针,一个是缓冲区长度。
typedef struct _INTERNET_BUFFERS
{
DWORD dwStructSize;
_INTERNET_BUFFERS *Next;
LPCTSTR lpcszHeader;
DWORD dwHeadersLength;
DWORD dwHeadersTotal;
LPVOID lpvBuffer;
DWORD dwBufferLength;
DWORD dwBufferTotal;
DWORD dwOffsetLow;
DWORD dwOffsetHigh;
} INTERNET_BUFFERS, * LPINTERNET_BUFFERS;
HINTERNET hInternet = InternetOpen("Microsoft Internet Explorer", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hHttpSession = InternetConnect(hInternet, "www.hootina.org", 80, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
HINTERNET hHttpRequest = HttpOpenRequest(hHttpSession, "GET", "/index.php?preview=1", NULL, "", NULL, 0, 0);
HINTERNET hInternet = InternetOpen("Microsoft Internet Explorer", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hHttpSession = InternetConnect(hInternet, "www.hootina.org", 80, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
HINTERNET hHttpRequest = HttpOpenRequest(hHttpSession, "GET", "/index.php", NULL, "", NULL, 0, 0);
注意:如果网址中有www,则网址前面不能加http://,反之如果没有www,则一定要带上http://,否则会解析失败。
URL_COMPONENTS crackedURL = {0};
char szHostName[128];
char szUrlPath[256];
crackedURL.dwStructSize = sizeof (URL_COMPONENTS);
crackedURL.lpszHostName = szHostName;
crackedURL.dwHostNameLength = ARRAYSIZE(szHostName);
crackedURL.lpszUrlPath = szUrlPath;
crackedURL.dwUrlPathLength = ARRAYSIZE(szUrlPath);
InternetCrackUrl(URL_STRING, (DWORD)strlen(URL_STRING), 0, &crackedURL);
然后调用:
HINTERNET hInternet = InternetOpen("Microsoft Internet Explorer", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hHttpSession = InternetConnect(hInternet , crackedURL.lpszHostName, crackedURL.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
HINTERNET hHttpRequest = HttpOpenRequest(hConn, "GET", crackedURL.lpszUrlPath, NULL, "", NULL, 0, 0);
DWORD dwRetCode = 0;
DWORD dwSizeOfRq = sizeof(DWORD);
HttpSendRequest(hHttpRequest, NULL, 0, NULL, 0);
HttpQueryInfo(hHttpRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &dwRetCode, &dwSizeOfRq, NULL);
也可以调用InternetQueryDataAvailable()查询返回的数据大小或者使用InternetReadFile()直接读取数据。
需要注意的是:你可以调用HttpOpen()一次,然后利用返回的句柄,调用HttpConnect()和HttpOpenRequest()等函数多次,这样你可以建立多个http连接,进行多个http请求,当然关闭这些句柄时都要一个个地关闭干净。
3. 另外和这些相关的比较有用的函数有:InternetSetOptionEx(),你可以使用它设置http的一些选项,比如代理用户名和密码:
InternetSetOptionEx(m_hInternet, INTERNET_OPTION_PROXY_USERNAME, (LPVOID)m_strUser.c_str(), m_strUser.size() + 1, 0);
::InternetSetOptionEx(m_hInternet, INTERNET_OPTION_PROXY_PASSWORD, (LPVOID)m_strPwd.c_str(), m_strPwd.size() + 1, 0);
INTERNET_STATUS_CALLBACK lpCallBackFunc;
lpCallBackFunc = ::InternetSetStatusCallback(m_hInternet, (INTERNET_STATUS_CALLBACK)&StatusCallback);
4.有时候明明http服务器上的信息已经更新,但发送http请求还是得到原来的数据,这是由于http缓存的问题,禁用缓存可以将
m_hHttpRequest = ::HttpOpenRequest(m_hHttpSession, _T("GET"), crackedURL.lpszUrlPath, NULL, _T(""), NULL, 0, 0);
m_hHttpRequest = ::HttpOpenRequest(m_hHttpSession, _T("GET"), crackedURL.lpszUrlPath, NULL, _T(""), NULL, INTERNET_FLAG_NO_CACHE_WRITE, 0);
5.最后一点,因为WinInet系列函数不包含在windows.h这个头文件中,你需要在你的工程include单独的wininet.h,和引用wininet.lib。
下面这段代码完整地演示了利用WinInet http API下载index.php这个文件,下载网址是:http://www.hootina.org/index.php,你可以拷贝以修改适用你自己的项目:
#include
#include
#include
#pragma comment(lib, "wininet.lib")
#define URL_STRING_TEST _T("http://www.hootina.org/index.php")
int main()
{
/**
* 解析网址为主机、端口和目标页面
*/
TCHAR szHostName[128];
TCHAR szUrlPath[256];
URL_COMPONENTS crackedURL = { 0 };
crackedURL.dwStructSize = sizeof (URL_COMPONENTS);
crackedURL.lpszHostName = szHostName;
crackedURL.dwHostNameLength = ARRAYSIZE(szHostName);
crackedURL.lpszUrlPath = szUrlPath;
crackedURL.dwUrlPathLength = ARRAYSIZE(szUrlPath);
InternetCrackUrl(URL_STRING_TEST, (DWORD)_tcslen(URL_STRING_TEST), 0, &crackedURL);
/**
* http请求相关初始化工作
*/
HINTERNET hInternet = InternetOpen(_T("Microsoft InternetExplorer"), INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (hInternet == NULL)
return -1;
HINTERNET hHttpSession = InternetConnect(hInternet, crackedURL.lpszHostName, crackedURL.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
if (hHttpSession == NULL)
{
InternetCloseHandle(hInternet);
return -2;
}
HINTERNET hHttpRequest = HttpOpenRequest(hHttpSession, _T("GET"), crackedURL.lpszUrlPath, NULL, _T(""), NULL, 0, 0);
if (hHttpRequest == NULL)
{
InternetCloseHandle(hHttpSession);
InternetCloseHandle(hInternet);
return -3;
}
/**
* 查询http状态码(这一步不是必须的),但是HttpSendRequest()必须要调用
*/
DWORD dwRetCode = 0;
DWORD dwSizeOfRq = sizeof(DWORD);
if (!HttpSendRequest(hHttpRequest, NULL, 0, NULL, 0) ||
!HttpQueryInfo(hHttpRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &dwRetCode, &dwSizeOfRq, NULL)
|| dwRetCode >= 400)
{
InternetCloseHandle(hHttpRequest);
InternetCloseHandle(hHttpSession);
InternetCloseHandle(hInternet);
return -4;
}
/**
* 查询文件大小
*/
DWORD dwContentLen;
//这个地方有错误,参见后面分析!
if (!InternetQueryDataAvailable(hHttpRequest, &dwContentLen, 0, 0) || dwContentLen == 0)
{
InternetCloseHandle(hHttpRequest);
InternetCloseHandle(hHttpSession);
InternetCloseHandle(hInternet);
return -6;
}
FILE* file = fopen("index.php", "wb+");
if (file == NULL)
{
InternetCloseHandle(hHttpRequest);
InternetCloseHandle(hHttpSession);
InternetCloseHandle(hInternet);
return -7;
}
DWORD dwError;
DWORD dwBytesRead;
DWORD nCurrentBytes = 0;
char szBuffer[1024] = { 0 };
while (TRUE)
{
//开始读取文件
if (InternetReadFile(hHttpRequest, szBuffer, sizeof(szBuffer), &dwBytesRead))
{
if (dwBytesRead == 0)
{
break;
}
nCurrentBytes += dwBytesRead;
fwrite(szBuffer, 1, dwBytesRead, file);
}
else
{
dwError = GetLastError();
break;
}
}
fclose(file);
InternetCloseHandle(hInternet);
InternetCloseHandle(hHttpSession);
InternetCloseHandle(hHttpRequest);
//这个地方有错误,参见后面分析!
if (dwContentLen != nCurrentBytes)
return -8;
return 0;
}
注意:上面下载index.php隐藏着一个错误,这也是新手常犯的错误:
程序中先用InternetQueryDataAvailable()函数查询返回的字节数,然后再利用InternetReadFile()读取字节数,如果 InternetReadFile()读取的总字节数和InternetQueryDataAvailable()查询到的字节数不相等,则认为出错。这种思路是不正确的。问题就在于InternetQueryDataAvailable(),首先http请求返回的是字节流,假如一个请求先返回30个字节,后再收到70个字节,那么当返回30个字节的时候正好调用了InternetQueryDataAvailable()得到的值也就是30。而接下来调用InternetReadFile()实际却读到了100个字节。这个时候因为二者不相等,所以程序就认为出错了。这也就是msdn上说的:The amount of data remaining will not be recalculated until all available data indicated by the call to InternetQueryDataAvailable is read.(除非你调用InternetReadFile()后再次调用InternetQueryDataAvailable()才能重新计算可用的数据大小)
正确的做法是调用HttpQueryInfo()去查询http请求返回的头部中的content-length字段去确定可以读取的字节数,代码:
WCHAR buf[64] = { 0 };
DWORD dwSizeOfReq = sizeof(buf);
DWORD dwContLen = 0;
//需要注意的是,如果适用HttpQueryInfoW,那么buf必须也是宽字符版本,
//虽然HttpQueryInfo()之前只是一个缓冲区,因为如果不使用宽字符,
//buf得到的字节数可能会因为\0的原因被截断。
if (HttpQueryInfo(m_hHttpRequest, HTTP_QUERY_CONTENT_LENGTH, buf, &dwSizeOfReq, NULL))
dwContLen = _wtol(buf);
else
return false;
if (dwBytesGet != dwContLen)
return false;
OK,就这么多了。如果发现任何错误欢迎与我交流。
另外这里,我提供一个对上述API封装的版本,功能更强大:
cdsn下载地址:http://download.csdn.net/detail/analogous_love/9846450
github下载地址:https://github.com/baloonwj/HttpClientLib
张远龙 发表于2016.07.05
============================继续更新===================
张远龙 更新于2016.11.10
我今天在写一个服务器压力测试程序时,将如下代码封装成一个类成员函数ParseURL()时,发现一个奇怪的问题:
URL_COMPONENTS crackedURL = {0};
TCHAR szHostName[128];
TCHAR szUrlPath[256];
crackedURL.dwStructSize = sizeof (URL_COMPONENTS);
crackedURL.lpszHostName =szHostName;
crackedURL.dwHostNameLength = ARRAYSIZE(szHostName);
crackedURL.lpszUrlPath =szUrlPath;
crackedURL.dwUrlPathLength = ARRAYSIZE(szUrlPath);
if (!::InternetCrackUrl(strURL.c_str(), (DWORD)strURL.length(), 0, &crackedURL))
return false;
为了能够在另外的类成员函数中使用URL_COMPONENTS crackedURL 结构,我将这个结构定义成类的成员,当然同时也将szHostName和szUrlPath也定义成成员变量,但在调用InternetCrackUrl()函数的时候总是失败,错误原因显示缓冲区不足,而我的host和网址字符串长度并没有超出szHostName和szUrlPath。或者即使调用成功了,后面创建httpSession时也会因为主机host为NULL,导致httpSession创建。我将代码改回来后,一切又恢复正常。难道这个结构不能作为成员变量吗?这个问题原因目前仍在调查中。