WinInet随笔

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.
请参考https://msdn.microsoft.com/en-us/library/windows/desktop/aa385373(v=vs.85).aspx

查询完Response Headers后,就可以读取http Body了(当然你也不必读取Response 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();
	}
另外还需注意同步和异步请求都受限于此值。




  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值