原始碼下載地址
案例開發環境:VS2010
本案例未使用openssl庫,內部提供了sslite.dll庫進行TLS會話,該庫提供了ISSLSession介面用于建立SSL會話,
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會通過回呼函式將資料拋給呼叫層,
以下是原始碼,注釋很多,就不一一解釋了,
class ISSLSession{
public:
virtual ~ISSLSession(){}
// 建立連接
virtual BOOL SSLConnect(LPCTSTR lpszServer, WORD wPort) = 0;
// SSL握手,只有呼叫SSLHandShake成功后,方可進行業務層通信
virtual BOOL SSLHandShake() = 0;
// 建立SSL會話,該函式=SSLConnect()+SSLHandShake()
virtual BOOL SSLEstablish(LPCTSTR lpszServer, WORD wPort, int& iErrCode) = 0;
// 發送資料,該資料為業務層原始資料,無需加密
virtual BOOL SSLSend(const BYTE* pData, int iDataLen) = 0;
// 接收資料,上拋資料已在底層進行解密
virtual BOOL SSLRecv(BYTE* pRecvBuff, int& iRecvBuffSize, LPFN_OnSSLDataNotify pfnOnSSLDataNotify, DWORD dwCallBackData1, DWORD dwCallBackData2) = 0;
// 加密資料,SSLSend函式在發送資料時會呼叫該函式
virtual BOOL SSLEncrypt(const BYTE* pData, int iDataLen, BYTE* pOut, int& iOutLen) = 0;
// 解密資料,SSLRecv在上拋資料時會呼叫該函式
virtual BOOL SSLDecrypt(const BYTE* pData, int iDataLen, BYTE* pOut, int& iOutLen) = 0;
// 關閉SSL連接
virtual BOOL SSLClose() = 0;
// 獲取當前SSL通信的加密套件ID
virtual WORD SSLGetCipherID() = 0;
// 獲取當前SSL通信的加密套件名稱,如:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
virtual const char* SSLGetCipherName() = 0;
// 獲取當前交換秘鑰中使用的ECC group id,如:23
virtual WORD SSLGetECGroupID() = 0;
// 獲取當前交換秘鑰中使用的ECC group name,如:secp256r1
virtual const char* SSLGetECGroupName() = 0;
// 回傳套接字
virtual SOCKET SSLGetSocket() = 0;
};
#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;
}
**
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/286858.html
標籤:其他
上一篇:【電力電子技術課程設計】單相交流調壓電路Simulink仿真
下一篇:平衡放大器優缺點
