源碼下載地址
案例開發環境: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會通過回調函數將數據拋給調用層。
以下是部分源碼截圖,注釋很多,就不一一解釋了。
#define END_RESPONSE_HEADER "\r\n\r\n" #define CRLF "\r\n" // 用於保存http響應的解析的相關參數 #define MAX_RESPONSE_HEADER_LEN 8196 // 響應頭最大為8K typedef struct http_params_st{ BOOL bHeaderComplete; // 響應頭數據是否接收完畢 BOOL bMessageComplete; // 響應數據是否接收完畢 BOOL bChunked; // 傳輸方式是否為分塊傳輸 int iStatusCode; // HTTP響應碼 __int64 i64TotalReaded; // 一共讀取的數據 __int64 i64ContentLen; // Content-Length長度(響應頭中解析出的"Content-Length"字段) __int64 i64BodyLen; // 實際的body數據長度 char szResponseHeader[MAX_RESPONSE_HEADER_LEN]; // 緩存HTTP響應頭 int iResponseHeaderLen; // 響應頭的長度 BOOL bResponseParsed; // 響應頭是否已解析 HANDLE hFile; // 文件句柄,用於保存接收到的所有響應數據(原始數據) HANDLE hFileBody; // 文件句柄,僅保存body數據 map<string, string> mapHeader; // 響應頭中key=value對 http_params_st(){ iStatusCode = 0; bHeaderComplete = FALSE; bMessageComplete = FALSE; bChunked = FALSE; i64TotalReaded = 0; i64ContentLen = 0; i64BodyLen = 0; memset(szResponseHeader, 0, MAX_RESPONSE_HEADER_LEN); bResponseParsed = FALSE; iResponseHeaderLen = 0; hFile = NULL; hFileBody = NULL; } }HTTP_PARAMS; // 字符串去除頭尾的空格 extern void StrTrim(char* pszSrc); // 解析HTTP 響應頭 extern BOOL ParseResponseHeader(HTTP_PARAMS* pHttpParams); // 根據關鍵字獲取對應的值 extern BOOL GetValueByKey(HTTP_PARAMS* pHttpParams, string strKey, string& strValue); //=============================以下llhttp的回調函數============================= // HTTP響應頭讀取完畢 static int on_llhttp_headers_complete(llhttp_t* llhttp) { HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data; pHttpParams->bHeaderComplete = TRUE; return HPE_OK; } // HTTP響應讀取完畢 static int on_llhttp_message_complete(llhttp_t* llhttp) { HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data; pHttpParams->bMessageComplete = TRUE; return HPE_OK; } // llhttp上拋的body數據 static int on_llhttp_body(llhttp_t* llhttp, const char *at, size_t length) { HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data; pHttpParams->i64BodyLen += length; if(INVALID_HANDLE_VALUE != pHttpParams->hFileBody && NULL != pHttpParams->hFileBody) { DWORD dwWrited = 0; ::WriteFile(pHttpParams->hFileBody, at, length, &dwWrited, NULL); } return HPE_OK; } //=============================以下為SSL層返回的業務數據============================= static int OnSSLHttpDataNotify(const BYTE* pData, int iDataLen, DWORD dwCallbackData1, DWORD dwCallbackData2) { if(NULL == pData || iDataLen <= 0) return SSL_DATA_RECV_FAILED; llhttp_t* llhttp = (llhttp_t*)dwCallbackData1; // 來自SSL通信的用戶自定義數據,此案例中為llhttp解析器 HTTP_PARAMS* pHttpParams = (HTTP_PARAMS*)llhttp->data; // 來自llhttp的用戶自定義數據 pHttpParams->i64TotalReaded += iDataLen; // 計算一共讀取的數據 // 將接收到的數據寫入文件,這是原始數據,包含響應頭 // 數據內容可能是chunked,因此需要進一步解析 DWORD dwWrited = 0; ::WriteFile(pHttpParams->hFile, pData, iDataLen, &dwWrited, NULL); // 調用llhttp進行解析 int iRet = llhttp_execute(llhttp, (const char*)pData, iDataLen); if(HPE_OK != iRet) return SSL_DATA_RECV_FAILED; // 通知SSL層:業務層發生錯誤,SSLRecv函數將返回 // 將數據緩存到pHttpParams->szResponseHeader if(0 == pHttpParams->iResponseHeaderLen) { if(pHttpParams->i64TotalReaded > MAX_RESPONSE_HEADER_LEN) { int iTotalReaded = int(pHttpParams->i64TotalReaded); int iPreReaded = iTotalReaded - iDataLen; // 之前讀取的長度 if(iPreReaded < MAX_RESPONSE_HEADER_LEN) memcpy(pHttpParams->szResponseHeader+iPreReaded, pData, MAX_RESPONSE_HEADER_LEN-iPreReaded); pHttpParams->iResponseHeaderLen = MAX_RESPONSE_HEADER_LEN; } else { int iTotalReaded = int(pHttpParams->i64TotalReaded); memcpy(pHttpParams->szResponseHeader+iTotalReaded-iDataLen, pData, iDataLen); pHttpParams->iResponseHeaderLen = iTotalReaded; } } // 計算HTTP響應頭的長度 if(!pHttpParams->bHeaderComplete) { // 緩沖區已滿但沒發現頭,說明響應頭太大超過8K,防止惡意攻擊 if(MAX_RESPONSE_HEADER_LEN == pHttpParams->iResponseHeaderLen) { printf("Too large HTTP response header.\r\n"); return SSL_DATA_RECV_FAILED; } } else { // 如果沒有解析HTTP響應頭,則進行解析 if(!pHttpParams->bResponseParsed) { // 查找"\r\n\r\n" char* pszResponseHeader = pHttpParams->szResponseHeader; char* pszFind = strstr(pszResponseHeader, END_RESPONSE_HEADER); int iPos = pszFind - pszResponseHeader; pHttpParams->iResponseHeaderLen = iPos + 4; // 計算真實的響應頭長度,包含4字節的"\r\n\r\n" *(pszResponseHeader+pHttpParams->iResponseHeaderLen) = 0; pHttpParams->bResponseParsed = TRUE; pHttpParams->iStatusCode = llhttp->status_code; // 解析HTTP響應頭 ParseResponseHeader(pHttpParams); // 獲取Content-Length長度 string strValue; if(GetValueByKey(pHttpParams, "Content-Length", strValue)) { pHttpParams->i64ContentLen = ::_atoi64(strValue.c_str()); } else { pHttpParams->i64ContentLen = -1; // 沒有Content-Length字段 } // 獲取Transfer-Encoding編碼方式,是否為chunked分塊傳輸 pHttpParams->bChunked = FALSE; if(GetValueByKey(pHttpParams, "Transfer-Encoding", strValue)) { if(0 == _stricmp(strValue.c_str(), "chunked")) pHttpParams->bChunked = TRUE; } // HTTP response頭中既沒有Content-Length字段,也沒有Chunked字段,因此無法明確后續內容大小 if(pHttpParams->i64ContentLen < 0 && !pHttpParams->bChunked) return SSL_DATA_RECV_FAILED; } } // 業務層數據全部讀取完畢 if(pHttpParams->bMessageComplete) { // 關閉文件 return SSL_DATA_RECV_FINISHED; // 通知SSL層:數據接收完畢,SSLRecv函數將返回TRUE } return SSL_DATA_RECV_STILL; // 通知SSL層:繼續接收數據,SSLRecv函數將繼續接收服務器數據 } // HTTPS協議測試 int _tmain(int argc, _TCHAR* argv[]) { // 加載sslite.dll CSSLWrap sslWrap; if(!sslWrap.Load()) { printf("Load sslite.dll failed!\r\n"); return -1; } printf("Load sslite.dll successfully!\r\n"); // 獲取ISSLSession接口 ISSLSession* pSSLSession = sslWrap.GetSSLSession(); //const char* pszServer = "www.sina.com.cn"; //const char* pszServer = "www.baidu.com"; const char* pszServer = "www.163.com"; // chunked int iRet = 0; // 建立SSL會話,也可以調用SSLConnect后再調用SSLHandShake來實現SSL會話 if(!pSSLSession->SSLEstablish(pszServer, 443, iRet)) { if(SSL_RET_CONNECT == iRet) { printf("Connect %s failed!\r\n", pszServer); } else if(SSL_RET_HANDSHAKE == iRet) { printf("SSL handshake failed!\r\n"); } return -1; } // 建立連接后,顯示當前的加密套件名稱和ECC(橢圓加密)的組名稱 printf("SSL Session Established.\r\n"); printf("Cipher Name: %s\r\n", pSSLSession->SSLGetCipherName()); printf("ECC Group Name: %s\r\n", pSSLSession->SSLGetECGroupName()); printf("Start HTTP communication.......\r\n\r\n"); // 發送HTTP請求 string strRequest; strRequest = "GET / HTTP/1.1\r\n"; strRequest += "Accept: */*\r\n"; strRequest += "Connection: Close\r\n"; //strRequest += "Accept-Encoding: gzip; br\r\n"; // 不支持壓縮 strRequest += "Host: "; strRequest += pszServer; strRequest += "\r\n\r\n"; if(!pSSLSession->SSLSend((BYTE*)strRequest.c_str(), strRequest.length())) { printf("ERROR: SSLSend.\r\n"); return -1; } /* 接收HTTP響應數據 1、iBuffSize將返回實際接收到的數據大小; 2、如果接收的數據大於輸入緩存arrBuff的尺寸,SSLRecv只會填滿arrBuff緩存, 后續數據將被丟棄。 3、OnSSLHttpDataNotify,回調函數,業務層需要在回調函數中處理具體的業務數據, 在本例中,使用開源的llhttp處理HTTP響應數據,如解析HTTP響應頭,獲取 Content-Length字段大小或chunk,從而判斷出后續要接收實際數據的尺寸。 從而在llhttp的回調函數中通知上層用戶。 OnSSLHttpDataNotify返回值如下: 3.1、SSL_DATA_RECV_STILL:業務層數據尚未讀完,SSLRecv內部需要繼續讀取; 3.2、SSL_DATA_RECV_FAILED:業務層出現錯誤,SSLRecv函數將返回FALSE; 3.3、SSL_DATA_RECV_FINISHED:業務層數據處理完畢,SSLRecv函數將返回TRUE; 本例中需要判斷Content-Length來決定,業務層數據是否讀取完畢。 注:node.js中使用llhttp進行http數據解析,從而大幅提升解析效率 */ // 構造llhttp解析器,用於解析HTTP返回的響應數據 llhttp_t llhttp_parser; llhttp_settings_t settings; llhttp_settings_init(&settings); settings.on_headers_complete = on_llhttp_headers_complete; // http響應頭已接收完畢通知 settings.on_message_complete = on_llhttp_message_complete; // http響應消息接收完畢 settings.on_body = on_llhttp_body; // http除響應頭外的消息體數據 llhttp_init(&llhttp_parser, HTTP_RESPONSE, &settings); HTTP_PARAMS http_params; llhttp_parser.data = (void*)&http_params; // 用戶自定義數據 BYTE arrBuff[1024] = {0}; int iBuffSize = 1024; // 將讀取到的所有響應內容保存到文件中,SSL層上拋的數據 const char* pszPathFile = "C:/TestSSLHttp.html"; http_params.hFile = ::CreateFile(pszPathFile, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if(INVALID_HANDLE_VALUE == http_params.hFile) { printf("ERROR: CreateFile \"%s\".\r\n", pszPathFile); return -1; } // 將讀取到的Body內容保存到文件中,llhttp處理后的真實body數據 const char* pszPathFileBody = "C:/TestSSLHttp_body.html"; http_params.hFileBody = ::CreateFile(pszPathFileBody, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if(INVALID_HANDLE_VALUE == http_params.hFileBody) { printf("ERROR: CreateFile \"%s\".\r\n", pszPathFileBody); return -1; } BOOL bRet = pSSLSession->SSLRecv(arrBuff, iBuffSize, OnSSLHttpDataNotify, (DWORD)&llhttp_parser, 0); if(!bRet) { printf("ERROR: SSLRecv.\r\n"); } ::CloseHandle(http_params.hFile); ::CloseHandle(http_params.hFileBody); printf("\r\n====================HTTP Response Header====================\r\n"); printf("%s", http_params.szResponseHeader); printf("\r\n====================HTTP Response Save To File====================\r\n"); printf("Write all response data to file: \"%s\"\r\n", pszPathFile); printf("Write body data to file: \"%s\"\r\n", pszPathFileBody); printf("\r\n====================HTTP Response Finished====================\r\n"); if(!http_params.bChunked) { printf("Total Readed = %I64u\r\nResponse Header Length = %d\r\nContent Length = %I64u\r\nContent-Length = %I64u\r\nBody Length=%I64u\r\n", http_params.i64TotalReaded, http_params.iResponseHeaderLen, http_params.i64TotalReaded-http_params.iResponseHeaderLen, http_params.i64ContentLen, http_params.i64BodyLen); } else { printf("Total Readed = %I64u\r\nResponse Header Length = %d\r\nContent Length = %I64u\r\nTransfer-Encoding = chunked\r\nBody Length = %I64u\r\n", http_params.i64TotalReaded, http_params.iResponseHeaderLen, http_params.i64TotalReaded-http_params.iResponseHeaderLen, http_params.i64BodyLen); } // !!釋放ISSLSession接口 sslWrap.ReleaseSSLSession(pSSLSession); printf("\r\nPress any key exit.....\r\n"); getchar(); return 0; } //=============================以下為公共函數============================= // 字符串去除頭尾的空格 void StrTrim(char* pszSrc) { if(NULL == pszSrc) return; int i = 0, j = 0; // 找到第一個非' '字符 while (pszSrc[j] == ' ') { ++j; } // 如果字符串全為空 if (pszSrc[j] == 0) { pszSrc[0] = 0; return; } int iIdx = j; // 記錄第一個非空字符位置 int iStop = 0; while (pszSrc[j] != 0) { if (pszSrc[j] == ' ' && iStop == 0) { iStop = j; // 記錄后面遇到的一個空字符 } else if (pszSrc[j] != ' ' && iStop != 0) { iStop = 0; } // 將當前非空字符拷貝到以0為開始的新位置 pszSrc[i++] = pszSrc[j++]; } if (iStop > 0) { pszSrc[iStop - iIdx] = 0; } else if (j != i) { pszSrc[i] = 0; } } // 解析HTTP 響應頭 BOOL ParseResponseHeader(HTTP_PARAMS* pHttpParams) { if(NULL == pHttpParams) return FALSE; int iLen = strlen(pHttpParams->szResponseHeader); char* pszResponseHeader = new char[iLen+1]; strcpy(pszResponseHeader, pHttpParams->szResponseHeader); // 逐行解析 int iPos = 0; char* pszKeyValue = pszResponseHeader; char* pszFind = strstr(pszKeyValue, CRLF); while(pszFind) { iPos = pszFind-pszKeyValue; *(pszKeyValue+iPos) = 0; if(0 == strlen(pszKeyValue)) break; // 查找":",並解析key:Value,存放於mapHeader中,便於后續使用 char* pszColon = strstr(pszKeyValue, ":"); if(pszColon) { int iPosColon = pszColon - pszKeyValue; *(pszKeyValue+iPosColon) = 0; char* pszKey = pszKeyValue; char* pszValue = pszKeyValue + iPosColon + 1; // SKip Colon // 去除頭尾空格 StrTrim(pszKey); StrTrim(pszValue); // 保存到map中 string strKey = pszKey; string strValue = pszValue; map<string, string>::iterator iter = pHttpParams->mapHeader.find(strKey); if(iter == pHttpParams->mapHeader.end()) { pHttpParams->mapHeader.insert(map<string, string>::value_type(strKey, strValue)); } else { iter->second += ";"; iter->second += strValue; } } // 查找下一行 pszKeyValue = pszKeyValue + iPos + 2; // Skip "\r\n" pszFind = strstr(pszKeyValue, CRLF); } delete pszResponseHeader; return TRUE; } // 根據關鍵字獲取對應的值 BOOL GetValueByKey(HTTP_PARAMS* pHttpParams, string strKey, string& strValue) { // 下面方法回出現由於key關鍵字的大小寫不一,導致無法檢索到 //map<string, string>::iterator iter = pHttpParams->mapHeader.find(strKey); //if(iter == pHttpParams->mapHeader.end()) //{ //return FALSE; //} //strValue = iter->second; //return TRUE; map<string, string>::iterator iter; for(iter = pHttpParams->mapHeader.begin(); iter != pHttpParams->mapHeader.end(); ++iter) { if(0 == _stricmp(iter->first.c_str(), strKey.c_str())) { strValue = iter->second; return TRUE; } } return FALSE; }