WinInet使用详解

       WinInet是windows平台对socket进行一层封装,用来直接处理http/ftp/Gopher协议的一套windows API。我第一次接触这个是在一个客户那里,当时因为客户都需要使用http代理才能浏览网页,所以折腾了一天,才基本掌握。而时隔没多久又忘记的差不多了,这个流程就像正则表达式一样,常学常忘,常忘常学。而网络上流传的WinInet代码实例,不是编译不通过,就是粗制滥造。因此现在整理和归纳出一个基本的流程出来,用于记录和备忘。本文只介绍WinInet的http协议部分,关于ftp和Gopher在msdn上搜索WinInet即可找到。



一、WinInet使用流程

WinInet使用详解_第1张图片

如上图所示:
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); 

http的版本号信息可以在参数lpszVersion中指定,目前支持的版本号有http 1.0和http 1.1,如果这个参数设置为NULL,则根据IE浏览器里面设置的http版本号来填充值。
 
http的数据请求方式一般有get和post方法,这个在之前的 HttpOpenRequest()的第二个参数lpszVerb 中设置:
如果是get方法,那么lpszVerb=_T("GET"),如果是post方法,则 
lpszVerb=_T("POST")或者 lpszVerb=_T("PUT") 。get方法一般是网址后面加问号跟上变量和变量值,变量与变量之间用&符号分割,例如:
http://www.baidu.com/index.php?var1=value1&var2=value2&var3=value3&var4=value4
浏览器一般对网址长度有限制,因此get方法也有长度限制,且get方法是明文的(在网址中可直接看到),所有另外一个方法是post,post是加密的,大多数浏览器中的表单会使用这种方法,如何设置post的数据呢?有两种方法:
方法一,调用HttpSendRequest()函数或者HttpSendRequestEx()。先看HttpSendRequest()函数签名:

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; 

方法二是:调用 InternetWriteFile()函数,当然这个函数也需要借助 H ttpSendRequestEx(),具体参见msdn。

在调用HttpSendRequest(Ex)()函数之前,你也可以调用
HttpAddRequestHeaders()追加一些需要一起发送的http头信息。

准备工作说了一堆,我们的http请求网址到底应该怎么填写?毕竟这才是我们的核心内容啊。我们来一个实例,请求以下网址:
http://www.hootina.org/index.php?preview=1 ;
因为http协议的默认端口号是80,所以这个网址也等价于:
http://www.hootina.org:80/index.php?preview=1
这是一个get请求。那么我们可以这么写:
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);

如果是不带get参数的网址:
http://www.hootina.org/index.php?preview=1
则写成 
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://,否则会解析失败。 

但我们一般不会直接手动将网址拆分放在各个函数里面,而是调用系统函数
InternetCrackUrl()来拆分。使用方法如下:

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); 

2.当我们做好以上的初始化工作以后,我们可以调用相关的数据获取函数去获取http请求的结果了。例如:
当我们调用HttpSendRequest()之后我们可以调用HttpQueryInfo()查询相关的http协议,比如http的状态码,比如404,502等:

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);

也可以使用 InternetSetStatusCallback()来设置http状态码发生变化时的回调函数:

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;

或者在知道http请求一定会返回结果的情况,直接调用InternetReadFile()函数去接收数据,省略先查询收到的字节长度的步骤。 


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创建。我将代码改回来后,一切又恢复正常。难道这个结构不能作为成员变量吗?这个问题原因目前仍在调查中。



你可能感兴趣的:(Windows编程提高班)