基於SSL(TLS)的HTTPS網頁下載——如何編寫健壯的可靠的網頁下載

源碼下載地址
案例開發環境:VS2010
本案例未使用openssl庫,內部提供了sslite.dll庫進行TLS會話,該庫提供了ISSLSession接口用於創建SSL會話。下載的是網易(www.163.com)的主頁。程序執行後會打印SSL會話的加密套件名稱和Http響應頭,並在C盤根目錄下輸出「TestSSLHttp.html」和「TestSSLHttp_body.html」兩個文件。前者是服務器響應的原始文件即包含了響應頭,後者是響應數據文件(本案例中爲主頁HTML)。html

HTTP協議很簡單,寫個簡單的socket程序經過GET命令就能把網頁給down下來。但接收大的網絡資源就複雜多了。什麼時候解析、如何解析完整的HTTP響應頭,就是個頭疼問題。由於你不能期望一次recv就能接收完全部響應數據,也不能期望服務器先發送完HTTP響應頭,而後再發送響應數據(有多是二者一併發送的)。只有把HTTP響應頭完全解析了,咱們才能知道後續接收的Body數據有多大,什麼時候才能接收完畢。node

好比經過響應頭的"Content-Length"字段,才能知道後續Body的大小。這個大小可能超過了你以前開闢的接收數據緩存區大小。固然你能夠在得知Body大小後,從新開闢一個與"Content-Length"同樣大小的緩存區。但這樣作顯然是不明智的,好比你get的是一部4K高清藍光小電影,藍光電影不必定能get到,藍屏電腦倒有可能get到。。。。。。web

遇到服務器明確給出"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會經過回調函數將數據拋給調用層。socket

如下是部分源碼截圖,註釋不少,就不一一解釋了。函數

#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響應數據
     一、iBuffSize將返回實際接收到的數據大小;
     二、若是接收的數據大於輸入緩存arrBuff的尺寸,SSLRecv只會填滿arrBuff緩存,
        後續數據將被丟棄。
     三、OnSSLHttpDataNotify,回調函數,業務層須要在回調函數中處理具體的業務數據,
        在本例中,使用開源的llhttp處理HTTP響應數據,如解析HTTP響應頭,獲取
        Content-Length字段大小或chunk,從而判斷出後續要接收實際數據的尺寸。
        從而在llhttp的回調函數中通知上層用戶。

        OnSSLHttpDataNotify返回值以下:
        3.一、SSL_DATA_RECV_STILL:業務層數據還沒有讀完,SSLRecv內部須要繼續讀取;
        3.二、SSL_DATA_RECV_FAILED:業務層出現錯誤,SSLRecv函數將返回FALSE;
        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;
}

 

相關文章
相關標籤/搜索