Wininet是微软提供的利用FTP、HTTP协议访问Internet资源的API接口,接口处理底层协议的的变化,例如代理服务器,从而使利用winiet的应用程序具有一直的行为。使用Wininet最知名的程序就是IE,而很多第三方应用也使用它来方位互联网。最近做了些工作,特记录下一些随笔。
Wininet利用HINTERNET句柄保存协议相关信息,并且HINTERNET句柄以树状的形式保存,其中InternetOpen返回的句柄是根节点。下一个层级是InternetConnect返回的句柄,最下层为HttpOpenRequest返回的句柄。有的函数在文档中指明了其HINTERNET参数必须使用哪一个层级的句柄,例如InternetConnect使用InternetOpen返回的句柄。而有的函数没有指明,例如InternetQueryOption或者InternetSetOption,其HINTERNET参数没有指明必须使用哪一个层级的句柄,这种情况下HINTERNET参数代表的是应用范围。而利用此HINTERNET句柄创建的子句柄都将继承设置选项。
为了减少网络负载,客户端可以要求服务器返回压缩的数据,也就是在Request Header中加入“Accept-Encoding: gzip, deflate”。在Windows Vista之前,应用程序必须自己解码。而在Vista之后,可以设置Wininet自动解码数据。通过以下设置即可
//WinInet自动解码
BOOL bAutoDecode = TRUE;
InternetSetOption(hRequestHandle, INTERNET_OPTION_HTTP_DECODING, &bAutoDecode, sizeof(BOOL));
若Wininet成功的解码了数据,它将去掉响应数据头content-encoding(实际上也会去掉content-length)。应用程序应该检查content-encoding项,如果存在(例如服务器的压缩方式与客户端支持的解码方式不同),不管有没有开启自动解码,应用程序也应该自己解码数据。应用程序调用InternetReadFile读取数据时,Wininet执行解压缩,若解压缩失败,InternetReadFile的错误码为ERROR_INTERNET_DECODING_FAILED。这时候应该去掉Accept-Encoding重新发送请求,或者设置Wininet不自动解码,而改由应用程序解码数据。
InternetOpen函数原型为
HINTERNET InternetOpen(
__in LPCTSTR lpszAgent,
__in DWORD dwAccessType,
__in LPCTSTR lpszProxyName,
__in LPCTSTR lpszProxyBypass,
__in DWORD dwFlags
);
参数dwAccessType表示访问Internet的方式,包括直接访问网络(INTERNET_OPEN_TYPE_DIRECT)、使用注册表的设置访问网络(INTERNET_OPEN_TYPE_PRECONFIG)、使用代理访问网络(INTERNET_OPEN_TYPE_PROXY)、使用注册表的设置访问网络但不使用自动代理(INTERNET_OPEN_TYPE_PRECONFIG_WITH_NO_AUTOPROXY)。而参数lpszProxyName及lpszProxyBypass表示若访问方式为INTERNET_OPEN_TYPE_PROXY时使用的代理服务器及那些忽略使用代理服务器的Url地址。参数dwFlags可以设置
INTERNET_FLAG_ASYNC
,则针对此句柄及派生出的句柄的操作都为异步操作。
InternetConnect函数原型为
HINTERNET InternetConnect(
__in HINTERNET hInternet,
__in LPCTSTR lpszServerName,
__in INTERNET_PORT nServerPort,
__in LPCTSTR lpszUsername,
__in LPCTSTR lpszPassword,
__in DWORD dwService,
__in DWORD dwFlags,
__in DWORD_PTR dwContext
);
此函数不会去连接网络,而是构造连接参数,其中dwService表示访问的服务类型,包括HTTP、FTP、Gopher。
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请求句柄,而不会连接网络发送数据。其中参数lplpszAcceptTypes类型为LPCTSTR*, 需要注意,合法的写法为
CONST TCHAR *szAcceptType[]={_T("text/css"),_T("*.*"), NULL};
参数dwFlags有几个值得说明的选项,比如若连接重置或者连接服务器失败返回缓存的内容、不验证证书中的名字或者有效日期、忽略特殊的重定向(HTTP->HTTPS or verse)、使用keep-alive语法(可以使用添加请求头替代)、不自动添加或者保存cookie、使用HTTPS(
INTERNET_FLAG_SECURE
)等。
在讲解HttpSendRequest之前,先说说InternetSetOption可以设置的一些参数,我列出了我自己感兴趣的一些项,包括INTERNET_OPTION_HTTP_DECODING、INTERNET_OPTION_RECEIVE_TIMEOUT、INTERNET_OPTION_SEND_TIMEOUT、INTERNET_OPTION_SECURITY_FLAGS等。
如果要使用HTTPS服务,HttpOpenRequest要设置参数INTERNET_FLAG_SECURE。还可以添加标志位要求忽略证书域名检测、有效期检测。如果要忽略无效的证书颁发机构,请发送HttpSendRequest前添加以下设置:
DWORD dwFlags;
DWORD dwBuffLen = sizeof(dwFlags);
InternetQueryOption(hRequestHandle, INTERNET_OPTION_SECURITY_FLAGS, (LPVOID)&dwFlags, &dwBuffLen);
dwFlags |= SECURITY_FLAG_IGNORE_UNKNOWN_CA;
InternetSetOption (hRequestHandle, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof (dwFlags));
或者
Again:
if (!HttpSendRequest (hReq,...))
dwError = GetLastError ();
if (dwError == ERROR_INTERNET_INVALID_CA)
{
DWORD dwFlags;
DWORD dwBuffLen = sizeof(dwFlags);
InternetQueryOption (hReq, INTERNET_OPTION_SECURITY_FLAGS,
(LPVOID)&dwFlags, &dwBuffLen);
dwFlags |= SECURITY_FLAG_IGNORE_UNKNOWN_CA;
InternetSetOption (hReq, INTERNET_OPTION_SECURITY_FLAGS,
&dwFlags, sizeof (dwFlags) );
goto again;
}
参见https://support.microsoft.com/zh-cn/kb/182888。
另外若HttpSendRequest返回错误码ERROR_INTERNET_SEC_CERT_REV_FAILED,需更改设置代码为
DWORD dwFlags;
DWORD dwBuffLen = sizeof(dwFlags);
InternetQueryOption(hRequestHandle, INTERNET_OPTION_SECURITY_FLAGS, (LPVOID)&dwFlags, &dwBuffLen);
dwFlags |= SECURITY_FLAG_IGNORE_UNKNOWN_CA;
dwFlags |= SECURITY_FLAG_IGNORE_REVOCATION;
InternetSetOption (hRequestHandle, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof (dwFlags));
关于证书验证参见http://stackoverflow.com/questions/14071099/openssl-wininet-client
调用HttpSendRequest发送请求后,函数内部会等待接收响应头,若在超时时间内没有收到响应,返回错误码12002。需要注意使用代理的情况,可能出现就是连接不了远程服务器,代理服务器依然返回客户端响应,只是非200状态码而已。
函数HttpSendRequest执行成功后,调用HttpQueryInfo查询状态,包括Raw Header、状态码等,默认情况下函数返回字符串,但也可以要求返回时间、数字,例如:
WCHAR ResponseHeader[2000];
DWORD dwSize = 2000;
bResult = HttpQueryInfo(hRequestHandle, HTTP_QUERY_RAW_HEADERS_CRLF,
ResponseHeader, &dwSize, NULL);
//获取响应头信息
DWORD dwStatusCode;
dwSize = 4;
bResult = HttpQueryInfo(hRequestHandle, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER,
&dwStatusCode, &dwSize, NULL);
根据msdn文档,有两种方式获取响应头信息
- Use one of the Query Info Flag constants associated with the HTTP header that your application needs.
- Use the HTTP_QUERY_CUSTOM attribute flag and pass the name of the HTTP header.
while(TRUE)
{
if (bResult)
{
//开始读取文件
bResult = InternetReadFile(hReq, szBuffer, sizeof(szBuffer), &dwLen);
if (bResult)
{
nBytesGet += dwLen;
if (dwLen == 0)
{
break;
}
}
}
else //数据接受完毕
{
break;
}
}
正常情况下最后一次读取InternetReadFile返回TRUE,返回字节数0。根据测试,InternetReadFile对传输Transfer-Encoding: chunked格式已做了处理,对用户来说是透明的。也可以在调用InternetQueryDataAvailable查询是否有可用的数据。如果没有可用的数据但是http传输并没有结束,InternetQueryDataAvailable将会阻塞直到有可读的数据。如果没有可读的数据,InternetQueryDataAvailable依然返回TRUE,而其输出的可用数据长度为0。
BOOL bRet = InternetQueryDataAvailable(hRequestHandle, &dwAvailable, 0, 0);
微软还有另外一套访问Internet的类库winhttp,它和wininet的功能相似,但有不同点,拷贝msdn的说法就是:
With a few exceptions, WinINet is a superset of WinHTTP. When selecting between the two, you should use WinINet, unless you plan to run within a service or service-like process that requires impersonation and session isolation.
意思就是除了一些差别,WinINet是WinHttp的超集,你应该选择WinInet,除非你开发服务器程序、window服务或需要身份仿真、会话隔离的类服务程序。
上面所说的服务器程序是英文中没有表述的,在我看的其它文档中有说明不应该用WinINet开发服务器程序(具体什么文档忘记了)。我对这句话不是很理解,于是祭出搜索神器,查到以下文档: INFO: WinInet Not Supported for Use in Services(https://support.microsoft.com/en-us/kb/238425)
里面说不能用在服务里的原因是Wininet需要使用注册表里的ssl信息、代理信息等,而这些信息保存在服务并不会加载的HKEY_CURRENT_USER键,所以这些信息对服务不可用。而不能用在服务器程序的原因需要从理解wininet的历史开始,wininet被开发的原因是被IE使用,其本质是被用在客户端环境中,其特点是较少的、连续的请求并且进程生命周期较短。服务器环境的特点是高并发请求、多线程并发、长时间运行。所以wininet并不适用,因为wininet对每一个服务器的并发连接数做了限制,默认为2,并且我做了个测试程序也验证了这个限制(但是在我的win7 x64、IE11的环境中并发数大于8个,原因还未找到)。并发限制的原因是符合HTTP规范,当然这个数可以修改,具体请参考WinInet limits connections per server(https://support.microsoft.com/en-us/kb/183110)。另外这个值也可以通过API去修改,代码如下
DWORD maximunConnect = 4;
if(!::InternetSetOption(NULL, //请自行测试HINTERNENT句柄值对应用范围的影响
INTERNET_OPTION_MAX_CONNS_PER_SERVER,&maximunConnect, sizeof(DWORD)))
{
int iErr = GetLastError();
}
另外还需注意同步和异步请求都受限于此值。