经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
基于SSL(TLS)的HTTPS网页下载——如何编写健壮的可靠的网页下载
来源:cnblogs  作者:一只会铲史的猫  时间:2021/6/15 9:19:42  对本文有异议

源码下载地址
案例开发环境:VS2010
本案例未使用openssl库,内部提供了sslite.dll库进行TLS会话,该库提供了ISSLSession接口用于建立SSL会话。下载的是网易(www.163.com)的主页。程序执行后会打印SSL会话的加密套件名称和Http响应头,并在C盘根目录下输出“TestSSLHttp.html”和“TestSSLHttp_body.html”两个文件。前者是服务器响应的原始文件即包含了响应头,后者是响应数据文件(本案例中为主页HTML)。

HTTP协议很简单,写个简单的socket程序通过GET命令就能把网页给down下来。但接收大的网络资源就复杂多了。何时解析、如何解析完整的HTTP响应头,就是个头疼问题。因为你不能指望一次recv就能接收完所有响应数据,也不能指望服务器先发送完HTTP响应头,然后再发送响应数据(有可能是两者一并发送的)。只有把HTTP响应头彻底解析了,我们才能知道后续接收的Body数据有多大,何时才能接收完毕。

比如通过响应头的"Content-Length"字段,才能知道后续Body的大小。这个大小可能超过了你之前开辟的接收数据缓存区大小。当然你可以在得知Body大小后,重新开辟一个与"Content-Length"一样大小的缓存区。但这样做显然是不明智的,比如你get的是一部4K高清蓝光小电影,蓝光电影不一定能get到,蓝屏电脑倒有可能get到。。。。。。

遇到服务器明确给出"Content-Length"字段,是一件值得额手称庆的大喜事,但不是每个IT民工都这么幸运。如果遇到的是不靠谱的服务器,发送的是"Transfer-Encoding: chunked",那你就必须锻炼自己真正的解析和组织能力了。这些分块传输的数据,显然不会以你接收的节奏到达你的缓冲区,比如先接收到一个block块大小,然后是一个完整的块数据,很有可能你会接收到多个块或者不完整的块,这就需要你站在宏观的角度把他们拼接起来。

如果你遇到的是甩的一米的服务器,它不仅给你的是chunked,而且还增加了"Content-Encoding: gzip",那么你就需要拼接后进行解压,当然你也可能遇到的是"deflate"压缩。
附:我写过web服务器,所以也知道服务器的心理。。。。。。
HttpServer:一款Windows平台下基于IOCP模型的高并发轻量级web服务器

题外话:我一直困惑的是HTTP协议为何不是对分块数据单独gzip压缩然后传输,而只能是整体gzip压缩后再分块传输。这个对大资源传输很关键,比如上面的4K高清蓝光小电影,显然不能通过gzip+chunked方式传输,土豪服务器例外。

当然你也可以用开源的llhttp来解析收到的http数据,从而避免上述可能会遇到的各种坑。最新版本的nodejs中就使用llhttp代替之前的的http-parser,据说解析效率有大幅提升。为此我下载了nodejs源码,并编译了一把,这是一个快乐的过程,因为你可以看到v8引擎,openssl,zlib等各种开源库。。。。,不过llhttp只负责解析,不负责缓存,因此你还是需要在解析的过程中,进行数据缓存。
关于V8引擎的使用参见文章
V8引擎静态库及其调用方法

以下是sslite库提供的接口,SSLConnect是建立连接,SSLHandShake是SSL握手,握手成功后即可调用SSLSend和SSLRecv进行数据接收和发送,非常简单。如果接收数据很多,SSLRecv会通过回调函数将数据抛给调用层。

以下是部分源码截图,注释很多,就不一一解释了。

  1. #define END_RESPONSE_HEADER "\r\n\r\n"
  2. #define CRLF "\r\n"
  3.  
  4. // 用于保存http响应的解析的相关参数
  5. #define MAX_RESPONSE_HEADER_LEN 8196 // 响应头最大为8K
  6. typedef struct http_params_st{
  7. BOOL bHeaderComplete; // 响应头数据是否接收完毕
  8. BOOL bMessageComplete; // 响应数据是否接收完毕
  9. BOOL bChunked; // 传输方式是否为分块传输
  10. int iStatusCode; // HTTP响应码
  11. __int64 i64TotalReaded; // 一共读取的数据
  12. __int64 i64ContentLen; // Content-Length长度(响应头中解析出的"Content-Length"字段)
  13. __int64 i64BodyLen; // 实际的body数据长度
  14.  
  15. char szResponseHeader[MAX_RESPONSE_HEADER_LEN]; // 缓存HTTP响应头
  16. int iResponseHeaderLen; // 响应头的长度
  17. BOOL bResponseParsed; // 响应头是否已解析
  18. HANDLE hFile; // 文件句柄,用于保存接收到的所有响应数据(原始数据)
  19. HANDLE hFileBody; // 文件句柄,仅保存body数据
  20. map<string, string> mapHeader; // 响应头中key=value对
  21. http_params_st(){
  22. iStatusCode = 0;
  23. bHeaderComplete = FALSE;
  24. bMessageComplete = FALSE;
  25. bChunked = FALSE;
  26. i64TotalReaded = 0;
  27. i64ContentLen = 0;
  28. i64BodyLen = 0;
  29. memset(szResponseHeader, 0, MAX_RESPONSE_HEADER_LEN);
  30. bResponseParsed = FALSE;
  31. iResponseHeaderLen = 0;
  32. hFile = NULL;
  33. hFileBody = NULL;
  34. }
  35. }HTTP_PARAMS;
  36. // 字符串去除头尾的空格
  37. extern void StrTrim(char* pszSrc);
  38. // 解析HTTP 响应头
  39. extern BOOL ParseResponseHeader(HTTP_PARAMS* pHttpParams);
  40. // 根据关键字获取对应的值
  41. extern BOOL GetValueByKey(HTTP_PARAMS* pHttpParams, string strKey, string& strValue);
  42. //=============================以下llhttp的回调函数=============================
  43. // HTTP响应头读取完毕
  44. static int on_llhttp_headers_complete(llhttp_t* llhttp)
  45. {
  46. HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data;
  47. pHttpParams->bHeaderComplete = TRUE;
  48. return HPE_OK;
  49. }
  50. // HTTP响应读取完毕
  51. static int on_llhttp_message_complete(llhttp_t* llhttp)
  52. {
  53. HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data;
  54. pHttpParams->bMessageComplete = TRUE;
  55. return HPE_OK;
  56. }
  57. // llhttp上抛的body数据
  58. static int on_llhttp_body(llhttp_t* llhttp, const char *at, size_t length)
  59. {
  60. HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data;
  61. pHttpParams->i64BodyLen += length;
  62. if(INVALID_HANDLE_VALUE != pHttpParams->hFileBody && NULL != pHttpParams->hFileBody)
  63. {
  64. DWORD dwWrited = 0;
  65. ::WriteFile(pHttpParams->hFileBody, at, length, &dwWrited, NULL);
  66. }
  67. return HPE_OK;
  68. }
  69. //=============================以下为SSL层返回的业务数据=============================
  70. static int OnSSLHttpDataNotify(const BYTE* pData, int iDataLen, DWORD dwCallbackData1, DWORD dwCallbackData2)
  71. {
  72. if(NULL == pData || iDataLen <= 0)
  73. return SSL_DATA_RECV_FAILED;
  74. llhttp_t* llhttp = (llhttp_t*)dwCallbackData1; // 来自SSL通信的用户自定义数据,此案例中为llhttp解析器
  75. HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data; // 来自llhttp的用户自定义数据
  76. pHttpParams->i64TotalReaded += iDataLen; // 计算一共读取的数据
  77. // 将接收到的数据写入文件,这是原始数据,包含响应头
  78. // 数据内容可能是chunked,因此需要进一步解析
  79. DWORD dwWrited = 0;
  80. ::WriteFile(pHttpParams->hFile, pData, iDataLen, &dwWrited, NULL);
  81. // 调用llhttp进行解析
  82. int iRet = llhttp_execute(llhttp, (const char*)pData, iDataLen);
  83. if(HPE_OK != iRet)
  84. return SSL_DATA_RECV_FAILED; // 通知SSL层:业务层发生错误,SSLRecv函数将返回
  85. // 将数据缓存到pHttpParams->szResponseHeader
  86. if(0 == pHttpParams->iResponseHeaderLen)
  87. {
  88. if(pHttpParams->i64TotalReaded > MAX_RESPONSE_HEADER_LEN)
  89. {
  90. int iTotalReaded = int(pHttpParams->i64TotalReaded);
  91. int iPreReaded = iTotalReaded - iDataLen; // 之前读取的长度
  92. if(iPreReaded < MAX_RESPONSE_HEADER_LEN)
  93. memcpy(pHttpParams->szResponseHeader+iPreReaded, pData, MAX_RESPONSE_HEADER_LEN-iPreReaded);
  94. pHttpParams->iResponseHeaderLen = MAX_RESPONSE_HEADER_LEN;
  95. }
  96. else
  97. {
  98. int iTotalReaded = int(pHttpParams->i64TotalReaded);
  99. memcpy(pHttpParams->szResponseHeader+iTotalReaded-iDataLen, pData, iDataLen);
  100. pHttpParams->iResponseHeaderLen = iTotalReaded;
  101. }
  102. }
  103. // 计算HTTP响应头的长度
  104. if(!pHttpParams->bHeaderComplete)
  105. {
  106. // 缓冲区已满但没发现头,说明响应头太大超过8K,防止恶意攻击
  107. if(MAX_RESPONSE_HEADER_LEN == pHttpParams->iResponseHeaderLen)
  108. {
  109. printf("Too large HTTP response header.\r\n");
  110. return SSL_DATA_RECV_FAILED;
  111. }
  112. }
  113. else
  114. {
  115. // 如果没有解析HTTP响应头,则进行解析
  116. if(!pHttpParams->bResponseParsed)
  117. {
  118. // 查找"\r\n\r\n"
  119. char* pszResponseHeader = pHttpParams->szResponseHeader;
  120. char* pszFind = strstr(pszResponseHeader, END_RESPONSE_HEADER);
  121. int iPos = pszFind - pszResponseHeader;
  122. pHttpParams->iResponseHeaderLen = iPos + 4; // 计算真实的响应头长度,包含4字节的"\r\n\r\n"
  123. *(pszResponseHeader+pHttpParams->iResponseHeaderLen) = 0;
  124. pHttpParams->bResponseParsed = TRUE;
  125. pHttpParams->iStatusCode = llhttp->status_code;
  126. // 解析HTTP响应头
  127. ParseResponseHeader(pHttpParams);
  128. // 获取Content-Length长度
  129. string strValue;
  130. if(GetValueByKey(pHttpParams, "Content-Length", strValue))
  131. {
  132. pHttpParams->i64ContentLen = ::_atoi64(strValue.c_str());
  133. }
  134. else
  135. {
  136. pHttpParams->i64ContentLen = -1; // 没有Content-Length字段
  137. }
  138. // 获取Transfer-Encoding编码方式,是否为chunked分块传输
  139. pHttpParams->bChunked = FALSE;
  140. if(GetValueByKey(pHttpParams, "Transfer-Encoding", strValue))
  141. {
  142. if(0 == _stricmp(strValue.c_str(), "chunked"))
  143. pHttpParams->bChunked = TRUE;
  144. }
  145. // HTTP response头中既没有Content-Length字段,也没有Chunked字段,因此无法明确后续内容大小
  146. if(pHttpParams->i64ContentLen < 0 && !pHttpParams->bChunked)
  147. return SSL_DATA_RECV_FAILED;
  148. }
  149. }
  150. // 业务层数据全部读取完毕
  151. if(pHttpParams->bMessageComplete)
  152. {
  153. // 关闭文件
  154. return SSL_DATA_RECV_FINISHED; // 通知SSL层:数据接收完毕,SSLRecv函数将返回TRUE
  155. }
  156. return SSL_DATA_RECV_STILL; // 通知SSL层:继续接收数据,SSLRecv函数将继续接收服务器数据
  157. }
  158. // HTTPS协议测试
  159. int _tmain(int argc, _TCHAR* argv[])
  160. {
  161. // 加载sslite.dll
  162. CSSLWrap sslWrap;
  163. if(!sslWrap.Load())
  164. {
  165. printf("Load sslite.dll failed!\r\n");
  166. return -1;
  167. }
  168. printf("Load sslite.dll successfully!\r\n");
  169. // 获取ISSLSession接口
  170. ISSLSession* pSSLSession = sslWrap.GetSSLSession();
  171. //const char* pszServer = "www.sina.com.cn";
  172. //const char* pszServer = "www.baidu.com";
  173. const char* pszServer = "www.163.com"; // chunked
  174. int iRet = 0;
  175. // 建立SSL会话,也可以调用SSLConnect后再调用SSLHandShake来实现SSL会话
  176. if(!pSSLSession->SSLEstablish(pszServer, 443, iRet))
  177. {
  178. if(SSL_RET_CONNECT == iRet)
  179. {
  180. printf("Connect %s failed!\r\n", pszServer);
  181. }
  182. else if(SSL_RET_HANDSHAKE == iRet)
  183. {
  184. printf("SSL handshake failed!\r\n");
  185. }
  186. return -1;
  187. }
  188. // 建立连接后,显示当前的加密套件名称和ECC(椭圆加密)的组名称
  189. printf("SSL Session Established.\r\n");
  190. printf("Cipher Name: %s\r\n", pSSLSession->SSLGetCipherName());
  191. printf("ECC Group Name: %s\r\n", pSSLSession->SSLGetECGroupName());
  192. printf("Start HTTP communication.......\r\n\r\n");
  193. // 发送HTTP请求
  194. string strRequest;
  195. strRequest = "GET / HTTP/1.1\r\n";
  196. strRequest += "Accept: */*\r\n";
  197. strRequest += "Connection: Close\r\n";
  198. //strRequest += "Accept-Encoding: gzip; br\r\n"; // 不支持压缩
  199. strRequest += "Host: ";
  200. strRequest += pszServer;
  201. strRequest += "\r\n\r\n";
  202. if(!pSSLSession->SSLSend((BYTE*)strRequest.c_str(), strRequest.length()))
  203. {
  204. printf("ERROR: SSLSend.\r\n");
  205. return -1;
  206. }
  207. /*
  208. 接收HTTP响应数据
  209. 1、iBuffSize将返回实际接收到的数据大小;
  210. 2、如果接收的数据大于输入缓存arrBuff的尺寸,SSLRecv只会填满arrBuff缓存,
  211. 后续数据将被丢弃。
  212. 3、OnSSLHttpDataNotify,回调函数,业务层需要在回调函数中处理具体的业务数据,
  213. 在本例中,使用开源的llhttp处理HTTP响应数据,如解析HTTP响应头,获取
  214. Content-Length字段大小或chunk,从而判断出后续要接收实际数据的尺寸。
  215. 从而在llhttp的回调函数中通知上层用户。
  216. OnSSLHttpDataNotify返回值如下:
  217. 3.1、SSL_DATA_RECV_STILL:业务层数据尚未读完,SSLRecv内部需要继续读取;
  218. 3.2、SSL_DATA_RECV_FAILED:业务层出现错误,SSLRecv函数将返回FALSE;
  219. 3.3、SSL_DATA_RECV_FINISHED:业务层数据处理完毕,SSLRecv函数将返回TRUE;
  220. 本例中需要判断Content-Length来决定,业务层数据是否读取完毕。
  221. 注:node.js中使用llhttp进行http数据解析,从而大幅提升解析效率
  222. */
  223. // 构造llhttp解析器,用于解析HTTP返回的响应数据
  224. llhttp_t llhttp_parser;
  225. llhttp_settings_t settings;
  226. llhttp_settings_init(&settings);
  227. settings.on_headers_complete = on_llhttp_headers_complete; // http响应头已接收完毕通知
  228. settings.on_message_complete = on_llhttp_message_complete; // http响应消息接收完毕
  229. settings.on_body = on_llhttp_body; // http除响应头外的消息体数据
  230. llhttp_init(&llhttp_parser, HTTP_RESPONSE, &settings);
  231. HTTP_PARAMS http_params;
  232. llhttp_parser.data = (void*)&http_params; // 用户自定义数据
  233. BYTE arrBuff[1024] = {0};
  234. int iBuffSize = 1024;
  235. // 将读取到的所有响应内容保存到文件中,SSL层上抛的数据
  236. const char* pszPathFile = "C:/TestSSLHttp.html";
  237. http_params.hFile = ::CreateFile(pszPathFile, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
  238. FILE_ATTRIBUTE_NORMAL, NULL);
  239. if(INVALID_HANDLE_VALUE == http_params.hFile)
  240. {
  241. printf("ERROR: CreateFile \"%s\".\r\n", pszPathFile);
  242. return -1;
  243. }
  244. // 将读取到的Body内容保存到文件中,llhttp处理后的真实body数据
  245. const char* pszPathFileBody = "C:/TestSSLHttp_body.html";
  246. http_params.hFileBody = ::CreateFile(pszPathFileBody, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
  247. FILE_ATTRIBUTE_NORMAL, NULL);
  248. if(INVALID_HANDLE_VALUE == http_params.hFileBody)
  249. {
  250. printf("ERROR: CreateFile \"%s\".\r\n", pszPathFileBody);
  251. return -1;
  252. }
  253. BOOL bRet = pSSLSession->SSLRecv(arrBuff, iBuffSize, OnSSLHttpDataNotify, (DWORD)&llhttp_parser, 0);
  254. if(!bRet)
  255. {
  256. printf("ERROR: SSLRecv.\r\n");
  257. }
  258. ::CloseHandle(http_params.hFile);
  259. ::CloseHandle(http_params.hFileBody);
  260. printf("\r\n====================HTTP Response Header====================\r\n");
  261. printf("%s", http_params.szResponseHeader);
  262. printf("\r\n====================HTTP Response Save To File====================\r\n");
  263. printf("Write all response data to file: \"%s\"\r\n", pszPathFile);
  264. printf("Write body data to file: \"%s\"\r\n", pszPathFileBody);
  265. printf("\r\n====================HTTP Response Finished====================\r\n");
  266. if(!http_params.bChunked)
  267. {
  268. printf("Total Readed = %I64u\r\nResponse Header Length = %d\r\nContent Length = %I64u\r\nContent-Length = %I64u\r\nBody Length=%I64u\r\n",
  269. http_params.i64TotalReaded, http_params.iResponseHeaderLen,
  270. http_params.i64TotalReaded-http_params.iResponseHeaderLen,
  271. http_params.i64ContentLen, http_params.i64BodyLen);
  272. }
  273. else
  274. {
  275. printf("Total Readed = %I64u\r\nResponse Header Length = %d\r\nContent Length = %I64u\r\nTransfer-Encoding = chunked\r\nBody Length = %I64u\r\n",
  276. http_params.i64TotalReaded, http_params.iResponseHeaderLen,
  277. http_params.i64TotalReaded-http_params.iResponseHeaderLen,
  278. http_params.i64BodyLen);
  279. }
  280. // !!释放ISSLSession接口
  281. sslWrap.ReleaseSSLSession(pSSLSession);
  282. printf("\r\nPress any key exit.....\r\n");
  283. getchar();
  284. return 0;
  285. }
  286. //=============================以下为公共函数=============================
  287. // 字符串去除头尾的空格
  288. void StrTrim(char* pszSrc)
  289. {
  290. if(NULL == pszSrc)
  291. return;
  292. int i = 0, j = 0;
  293. // 找到第一个非' '字符
  294. while (pszSrc[j] == ' ') {
  295. ++j;
  296. }
  297. // 如果字符串全为空
  298. if (pszSrc[j] == 0) {
  299. pszSrc[0] = 0;
  300. return;
  301. }
  302. int iIdx = j; // 记录第一个非空字符位置
  303. int iStop = 0;
  304. while (pszSrc[j] != 0)
  305. {
  306. if (pszSrc[j] == ' ' && iStop == 0) {
  307. iStop = j; // 记录后面遇到的一个空字符
  308. }
  309. else if (pszSrc[j] != ' ' && iStop != 0) {
  310. iStop = 0;
  311. }
  312. // 将当前非空字符拷贝到以0为开始的新位置
  313. pszSrc[i++] = pszSrc[j++];
  314. }
  315. if (iStop > 0) {
  316. pszSrc[iStop - iIdx] = 0;
  317. }
  318. else if (j != i) {
  319. pszSrc[i] = 0;
  320. }
  321. }
  322. // 解析HTTP 响应头
  323. BOOL ParseResponseHeader(HTTP_PARAMS* pHttpParams)
  324. {
  325. if(NULL == pHttpParams)
  326. return FALSE;
  327. int iLen = strlen(pHttpParams->szResponseHeader);
  328. char* pszResponseHeader = new char[iLen+1];
  329. strcpy(pszResponseHeader, pHttpParams->szResponseHeader);
  330. // 逐行解析
  331. int iPos = 0;
  332. char* pszKeyValue = pszResponseHeader;
  333. char* pszFind = strstr(pszKeyValue, CRLF);
  334. while(pszFind)
  335. {
  336. iPos = pszFind-pszKeyValue;
  337. *(pszKeyValue+iPos) = 0;
  338. if(0 == strlen(pszKeyValue))
  339. break;
  340. // 查找":",并解析key:Value,存放于mapHeader中,便于后续使用
  341. char* pszColon = strstr(pszKeyValue, ":");
  342. if(pszColon)
  343. {
  344. int iPosColon = pszColon - pszKeyValue;
  345. *(pszKeyValue+iPosColon) = 0;
  346. char* pszKey = pszKeyValue;
  347. char* pszValue = pszKeyValue + iPosColon + 1; // SKip Colon
  348. // 去除头尾空格
  349. StrTrim(pszKey);
  350. StrTrim(pszValue);
  351. // 保存到map中
  352. string strKey = pszKey;
  353. string strValue = pszValue;
  354. map<string, string>::iterator iter = pHttpParams->mapHeader.find(strKey);
  355. if(iter == pHttpParams->mapHeader.end())
  356. {
  357. pHttpParams->mapHeader.insert(map<string, string>::value_type(strKey, strValue));
  358. }
  359. else
  360. {
  361. iter->second += ";";
  362. iter->second += strValue;
  363. }
  364. }
  365. // 查找下一行
  366. pszKeyValue = pszKeyValue + iPos + 2; // Skip "\r\n"
  367. pszFind = strstr(pszKeyValue, CRLF);
  368. }
  369. delete pszResponseHeader;
  370. return TRUE;
  371. }
  372. // 根据关键字获取对应的值
  373. BOOL GetValueByKey(HTTP_PARAMS* pHttpParams, string strKey, string& strValue)
  374. {
  375. // 下面方法回出现由于key关键字的大小写不一,导致无法检索到
  376. //map<string, string>::iterator iter = pHttpParams->mapHeader.find(strKey);
  377. //if(iter == pHttpParams->mapHeader.end())
  378. //{
  379. //return FALSE;
  380. //}
  381. //strValue = iter->second;
  382. //return TRUE;
  383. map<string, string>::iterator iter;
  384. for(iter = pHttpParams->mapHeader.begin(); iter != pHttpParams->mapHeader.end(); ++iter)
  385. {
  386. if(0 == _stricmp(iter->first.c_str(), strKey.c_str()))
  387. {
  388. strValue = iter->second;
  389. return TRUE;
  390. }
  391. }
  392. return FALSE;
  393. }

 

原文链接:http://www.cnblogs.com/softlee/p/14866278.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号