我是靠谱客的博主 自然小懒虫,最近开发中收集的这篇文章主要介绍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得到的字节数可能会因为的原因被截断。

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

最后

以上就是自然小懒虫为你收集整理的C++操作http之WinInet详解的全部内容,希望文章能够帮你解决C++操作http之WinInet详解所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(65)

评论列表共有 0 条评论

立即
投稿
返回
顶部