C++操作http之WinInet详解

WinInet是windows平台对socket进行一层封装,用来直接处理http/ftp/Gopher协议的一套windows API。本文只介绍WinInet的http协议部分,关于ftp和Gopher在msdn上搜索WinInet即可找到。

一. WinInet使用流程

如图所示:

  1. 首先必须依次调用InternetOpen()、InternetConnect()、InternetOpenRequest()产生三个HINTERNET句柄。
    需要注意的点:
    1).   后一个函数的第一个参数都是需要传入前一个函数生成的HINTERNET句柄,这也就是msdn上说的HINTERNET句柄的层级性(HTTP Hierarchy,see:https://msdn.microsoft.com/en-us/library/windows/desktop/aa383766(v=vs.85).aspx),也就是说InternetConnect()函数第一个参数必须是InternetOpen()函数返回的句柄,而InternetOpenRequest()函数第一个参数必须是InternetConnect()函数返回的句柄。

    2).   当你不再需要使用这些句柄时,你需要调用InternetCloseHandle()函数关闭这些个HINTERNET句柄, 而关闭顺序和创建相反。

    3).   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.


HINTERNET InternetOpen(
  _In_ LPCTSTR lpszAgent,
  _In_ DWORD   dwAccessType,
  _In_ LPCTSTR lpszProxyName,
  _In_ LPCTSTR lpszProxyBypass,
  _In_ DWORD   dwFlags
);

可以在这个函数lpszAgent参数里面设置所谓的UserAgent,常见的浏览器都有所谓的UserAgent,是一个字符串。例:

LPCSTR lpszAgent = _T("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/77.0.3865.120");
hInet = InternetOpenA(lpszAgent, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (NULL == hInet)
{
    printf("-1");
    return FALSE;
}

另外需要使用代理的http请求,也可以在lpszProxyName设置代理名称和lpszBypass中设置代理ip地址和端口号。当然你需要正确地设置dwAccessType值,我第一次使用时就在这个参数上面吃过亏,导致怎么也连不上http服务器。


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.0http 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()

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的lpvBufferdwBufferLength中设置,一个是缓冲区指针,一个是缓冲区长度。

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()函数,当然这个函数也需要借助HttpSendRequestEx(),具体参见msdn。

在调用HttpSendRequest(Ex)()函数之前,你也可以调用HttpAddRequestHeaders()追加一些需要一起发送的http头信息。
示例:http://www.hootina.org/index.php?preview=1

HINTERNET hInternet = InternetOpen("Microsoft Internet Explorer", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hConnect = InternetConnect(hInternet, "www.hootina.org", 80, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
HINTERNET hHttpRequest = HttpOpenRequest(hHttpSession, "GET", "/index.php?preview=1", _T("HTTP/1.1"), "", NULL, 0, 0);

注意:如果网址中有www,则网址前面不能加http://,反之如果没有www,则一定要带上http://,否则会解析失败。

http://192.168.1.77:49152/TxMediaRenderer_desc.xml 不需要加http://


  1. 当我们做好以上的初始化工作以后,我们可以调用相关的数据获取函数去获取http请求的结果了。例如:
    当我们调用HttpSendRequest()之后我们可以调用HttpQueryInfo()查询相关的http协议,比如http的状态码,比如404,502等:
//查询http状态码(这一步非必须),但是HttpSendRequest()必须要调用
	DWORD dwRetCode = 0;
	DWORD dwSizeOfRq = sizeof(DWORD);
	if (!HttpSendRequest(hRequestGet, NULL, 0, NULL, 0) || !HttpQueryInfo(hRequestGet, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &dwRetCode, &dwSizeOfRq, NULL) || dwRetCode >= 400)
	{
		InternetCloseHandle(hRequestGet);
		InternetCloseHandle(hConnect);
		InternetCloseHandle(hInet);
		printf("-4");
		return FALSE;
	}

也可以调用InternetQueryDataAvailable()查询返回的数据大小或者使用InternetReadFile()直接读取数据。

需要注意的是:你可以调用HttpOpen()一次,然后利用返回的句柄,调用HttpConnect()HttpOpenRequest()等函数多次,这样你可以建立多个http连接,进行多个http请求,当然关闭这些句柄时都要一个个地关闭干净。

  1. 另外和这些相关的比较有用的函数有: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); 
  1. 有时候明明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);
  1. 最后一点,因为WinInet系列函数不包含在windows.h这个头文件中,你需要在你的工程include单独的wininet.h,和引用wininet.lib。
#include <iostream>
#include <Windows.h>
#include <wininet.h>
 
#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);    //转换方法有误2020年3月19日
else        
     return false;

然后再比较实际读到的字节数和查询的字节数是否一致:

if (dwBytesGet != dwContLen)        
    return false;

或者在知道http请求一定会返回结果的情况,直接调用InternetReadFile()函数去接收数据,省略先查询收到的字节长度的步骤。
另外这里,我提供一个对上述API封装的版本,功能更强大:
cdsn下载地址:http://download.csdn.net/detail/analogous_love/9846450
github下载地址:https://github.com/baloonwj/HttpClientLib

原文地址:https://blog.csdn.net/analogous_love/article/details/72515002

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值