1. 程式人生 > >實現HTTP協議Get、Post和檔案上傳功能——使用libcurl介面實現

實現HTTP協議Get、Post和檔案上傳功能——使用libcurl介面實現

        之前我們已經詳細介紹了WinHttp介面如何實現Http的相關功能。本文我將主要講解如何使用libcurl庫去實現相關功能。(轉載請指明出於breaksoftware的csdn部落格)

        libcurl在http://curl.haxx.se/libcurl/有詳細的介紹,有興趣的朋友可以去讀下。本文我只是從實際使用的角度講解其中的一些功能。

        libcurl中主要有兩個介面型別:CURL和CURLM。CURL又稱easy interface,它介面簡單、使用方便,但是它是一個同步介面,我們不能使用它去實現非同步的功能——比如下載中斷——其實也是有辦法的(比如對寫回調做點手腳)。相應的,CURLM又稱multi interface,它是非同步的。可以想下,我們使用easy interface實現一個HTTP請求過程,如果某天我們需要將其改成multi interface介面的,似乎需要對所有介面都要做調整。其實不然,libcurl使用一種優雅的方式去解決這個問題——multi interface只是若干個easy interface的集合。我們只要把easy interface指標加入到multi interface中即可。

CURLMcode curl_multi_add_handle(CURLM *multi_handle, CURL *easy_handle);

        本文將使用multi interface作為最外層的管理者,具體下載功能交給easy interface。在使用easy interface之前,我們需要對其初始化

初始化

初始化easy interface

bool CHttpRequestByCurl::Prepare() {
	bool bSuc = false;
	do {
		if (!m_pCurlEasy) {
			m_pCurlEasy = curl_easy_init();
		}
		if (!m_pCurlEasy) {
			break;
		}

初始化multi interface

            if (!m_pCurlMulti){
                m_pCurlMulti = curl_multi_init();
            }
            if (!m_pCurlMulti) {
                break;
            }

設定

設定過程回撥

        過程回撥用於體現資料下載了多少或者上傳了多少

		CURLcode easycode;
		easycode = curl_easy_setopt( m_pCurlEasy, CURLOPT_NOPROGRESS, 0 );
		CHECKCURLEASY_EROORBREAK(easycode);
		easycode = curl_easy_setopt(m_pCurlEasy, CURLOPT_PROGRESSFUNCTION, progresscallback);
		CHECKCURLEASY_EROORBREAK(easycode);
		easycode = curl_easy_setopt( m_pCurlEasy, CURLOPT_PROGRESSDATA, this );
		CHECKCURLEASY_EROORBREAK(easycode);

        設定CURLOPT_NOPROGRESS代表我們需要使用過程回撥這個功能。設定CURLOPT_PROGRESSFUNCTION為progresscallback是設定回撥函式的指標,我們將通過靜態函式progresscallback反饋過程狀態。注意一下這兒,因為libcurl是一個C語言API庫,所以它沒有類的概念,這個將影響之後我們對各種靜態回撥函式的設定。此處要求progresscallback是一個靜態函式——它也沒有this指標,但是libcurl設計的非常好,它留了一個使用者自定義引數供我們使用,這樣我們便可以將物件的this指標通過CURLOPT_PROGRESSDATA傳過去。

	int CHttpRequestByCurl::progresscallback( void *clientp, double dltotal, double dlnow, double ultotal, double ulnow ) {
		if (clientp) {
			CHttpRequestByCurl* pThis = (CHttpRequestByCurl*)clientp;
			return pThis->ProcessCallback(dltotal, dlnow);
		}
		else {
			return -1;
		}
	}

    int CHttpRequestByCurl::ProcessCallback( double dltotal, double dlnow ) {
        if ( m_CallBack ) {
            const DWORD dwMaxEslapeTime = 500;
            std::ostringstream os;
            os << (unsigned long)dlnow;
            std::string strSize = os.str();

            std::ostringstream ostotal;
            ostotal << (unsigned long)dltotal;
            std::string strContentSize = ostotal.str();
            DWORD dwTickCount = GetTickCount();
            if ( ( 0 != ((unsigned long)dltotal)) && ( strSize == strContentSize || dwTickCount - m_dwLastCallBackTime > dwMaxEslapeTime ) ) {
                m_dwLastCallBackTime = dwTickCount;
                m_CallBack( strContentSize, strSize );
            }
        }
        return 0;
    }

        此處progresscallback只是一個代理功能——它是靜態的,它去呼叫clientp傳過來的this指標所指向物件的ProcessCallback成員函式。之後我們的其他回撥函式也是類似的,比如寫結果的回撥設定

設定寫結果回撥

		easycode = curl_easy_setopt(m_pCurlEasy, CURLOPT_WRITEFUNCTION, writefilecallback);
		CHECKCURLEASY_EROORBREAK(easycode);
		easycode = curl_easy_setopt(m_pCurlEasy, CURLOPT_WRITEDATA, this);
		CHECKCURLEASY_EROORBREAK(easycode);
	size_t CHttpRequestByCurl::writefilecallback( void *buffer, size_t size, size_t nmemb, void *stream ) {
		if (stream) {
			 CHttpRequestByCurl* pThis = (CHttpRequestByCurl*)stream;
			 return pThis->WriteFileCallBack(buffer, size, nmemb);
		}
		else {
			return size * nmemb;
		}
	}

    size_t CHttpRequestByCurl::WriteFileCallBack( void *buffer, size_t size, size_t nmemb ) {
        if (!m_pCurlEasy) {
            return 0;
        }

        int nResponse = 0;
        CURLcode easycode = curl_easy_getinfo(m_pCurlEasy, CURLINFO_RESPONSE_CODE, &nResponse);
        if ( CURLE_OK != easycode || nResponse >= 400 ) {
            return 0;
        }

        return Write(buffer, size, nmemb);
    }

        在WriteFileCallBack函式中,我們使用curl_easy_getinfo判斷了easy interface的返回值,這是為了解決接收返回結果時伺服器中斷的問題。

設定讀回撥

        讀回撥我們並沒有傳遞this指標過去。

            easycode = curl_easy_setopt( m_pCurlEasy,  CURLOPT_READFUNCTION,  read_callback);
            CHECKCURLEASY_EROORBREAK(easycode);

        我們看下回調就明白了

    size_t CHttpRequestByCurl::read_callback( void *ptr, size_t size, size_t nmemb, void *stream ) {
       return ((ToolsInterface::LPIMemFileOperation)(stream))->MFRead(ptr, size, nmemb);
    }

        這次使用者自定義指標指向了一個IMemFileOperation物件指標,它是在之後的其他步奏裡傳遞過來的。這兒有個非常有意思的地方——即MFRead的返回值和libcurl要求的read_callback返回值是一致的——並不是說型別一致——而是返回值的定義一致。這就是統一成標準介面的好處。

設定URL

            easycode = curl_easy_setopt(m_pCurlEasy, CURLOPT_URL, m_strUrl.c_str());
            CHECKCURLEASY_EROORBREAK(easycode);

設定超時時間

            easycode = curl_easy_setopt(m_pCurlEasy, CURLOPT_TIMEOUT_MS, m_nTimeout);
            CHECKCURLEASY_EROORBREAK(easycode);

設定Http頭

            for ( ToolsInterface::ListStrCIter it = m_listHeaders.begin(); it != m_listHeaders.end(); it++ ) {
                m_pHeaderlist = curl_slist_append(m_pHeaderlist, it->c_str());
            }
            if (m_pHeaderlist) {
                curl_easy_setopt(m_pCurlEasy, CURLOPT_HTTPHEADER, m_pHeaderlist);
            }

        這兒需要注意的是m_pHeaderlist在整個請求完畢後需要釋放

		if (m_pHeaderlist) {
			curl_slist_free_all (m_pHeaderlist);
			m_pHeaderlist = NULL;
		}

設定Agent

            if (!m_strAgent.empty()) {
                easycode = curl_easy_setopt(m_pCurlEasy, CURLOPT_USERAGENT, m_strAgent.c_str());
                CHECKCURLEASY_EROORBREAK(easycode);
            }

設定Post引數

            if ( ePost == GetType() ) {
                easycode = ModifyEasyCurl(m_pCurlEasy, m_Params);
                CHECKCURLEASY_EROORBREAK(easycode);
            }

        之後我們將講解ModifyEasyCurl的實現。我們先把整個呼叫過程將完。

將easy interface加入到multi interface

            CURLMcode multicode = curl_multi_add_handle( m_pCurlMulti, m_pCurlEasy );
            CHECKCURLMULTI_EROORBREAK(multicode);

            bSuc = true;
      } while (0);
      return bSuc;
}

執行

    EDownloadRet CHttpRequestByCurl::Curl_Multi_Select(CURLM* pMultiCurl)
    {
        EDownloadRet ERet = EContinue;

        do {
            struct timeval timeout;
            fd_set fdread;
            fd_set fdwrite;
            fd_set fdexcep;

            CURLMcode multicode;
            long curl_timeo = -1;

            /* set a suitable timeout to fail on */ 
            timeout.tv_sec = 30; /* 30 seconds */ 
            timeout.tv_usec = 0;
            multicode = curl_multi_timeout(pMultiCurl, &curl_timeo);
            if ( CURLM_OK == multicode && curl_timeo >= 0 ) {
                timeout.tv_sec = curl_timeo / 1000;
                if (timeout.tv_sec > 1) {
                    timeout.tv_sec = 0;
                } 
                else {
                    timeout.tv_usec = (curl_timeo % 1000) * 1000;
                }
            }

            int nMaxFd = -1;

            while ( -1 == nMaxFd ) {

                FD_ZERO(&fdread);
                FD_ZERO(&fdwrite);
                FD_ZERO(&fdexcep);

                multicode = curl_multi_fdset( m_pCurlMulti, &fdread, &fdwrite, &fdexcep, &nMaxFd );
                CHECKCURLMULTI_EROORBREAK(multicode);
                if ( -1 != nMaxFd ) {
                    break;
                }
                else {
                    if (WAIT_TIMEOUT != WaitForSingleObject(m_hStop, 100)) {
                        ERet = EInterrupt;
                        break;
                    }
                    int nRunning = 0;
                    CURLMcode multicode = curl_multi_perform( m_pCurlMulti, &nRunning );
                    CHECKCURLMULTI_EROORBREAK(multicode);
                }
            }

            if ( EContinue == ERet ) {
                int nSelectRet = select( nMaxFd + 1, &fdread, &fdwrite, &fdexcep, &timeout );

                if ( -1 == nSelectRet ){
                    ERet = EFailed;
                }
            }
            if ( EInterrupt == ERet ) {
                break;
            }
        } while (0);

        return ERet;
    }

    DWORD CHttpRequestByCurl::StartRequest() {
        Init();
        EDownloadRet eDownloadRet = ESuc;
        do {
            if (!Prepare()) {
                break;
            }

            int nRunning = -1;
            while( CURLM_CALL_MULTI_PERFORM == curl_multi_perform(m_pCurlMulti, &nRunning) ) {
                if (WAIT_TIMEOUT != WaitForSingleObject(m_hStop, 10)) {
                    eDownloadRet = EInterrupt;
                    break;
                }
            }

            if ( EInterrupt == eDownloadRet ) {
                break;
            }

            while(0 != nRunning) {
                EDownloadRet nSelectRet = Curl_Multi_Select(m_pCurlMulti);
                if ( EFailed == nSelectRet || EInterrupt == nSelectRet || ENetError == nSelectRet ) {
                    eDownloadRet = nSelectRet;
                    break;
                }
                else {
                    CURLMcode multicode = curl_multi_perform(m_pCurlMulti, &nRunning);
                    if (CURLM_CALL_MULTI_PERFORM == multicode) {
                        if (WAIT_TIMEOUT != WaitForSingleObject(m_hStop, 10)) {
                            eDownloadRet = EInterrupt;
                            break;
                        }
                    }
                    else if ( CURLM_OK == multicode ) {
                    }
                    else {
                        if (WAIT_TIMEOUT != WaitForSingleObject(m_hStop, 100)) {
                            eDownloadRet = EInterrupt;
                        }
                        break;
                    }
                }

                if ( EInterrupt == eDownloadRet ) {
                    break;
                }
            } // while

            if ( EInterrupt == eDownloadRet ) {
                break;
            }

            int msgs_left;  
            CURLMsg*  msg;  
            while((msg = curl_multi_info_read(m_pCurlMulti, &msgs_left))) {  
                if (CURLMSG_DONE == msg->msg) { 
                    if ( CURLE_OK != msg->data.result ) {
                        eDownloadRet = EFailed;
                    }
                }
                else {
                    eDownloadRet = EFailed;
                }
            }
			
        } while (0);

        Unint();

        m_bSuc = ( ESuc == eDownloadRet ) ? true : false;
        return eDownloadRet;
    }

        可以見得執行的主要過程就是不停的呼叫curl_multi_perform。

實現Post、檔案上傳功能

        對於MultiPart格式資料,我們要使用curl_httppost結構體儲存引數

組裝上傳檔案

    CURLcode CPostByCurl::ModifyEasyCurl_File( CURL* pEasyCurl, const FMParam& Param ) {

        Param.value->MFSeek(0L, SEEK_END);
        long valuesize = Param.value->MFTell();
        Param.value->MFSeek(0L, SEEK_SET);

        curl_formadd((curl_httppost**)&m_pFormpost,
            (curl_httppost**)&m_pLastptr,
            CURLFORM_COPYNAME, Param.strkey.c_str(),
            CURLFORM_STREAM, Param.value, 
            CURLFORM_CONTENTSLENGTH, valuesize,
            CURLFORM_FILENAME, Param.fileinfo.szfilename,
            CURLFORM_CONTENTTYPE, "application/octet-stream",
            CURLFORM_END);

        return CURLE_OK;
    }

        我們使用CURLFORM_STREAM標記資料的載體,此處我們傳遞的是一個IMemFileOperation指標,之前我們定義的readcallback回撥將會將該引數作為第一個引數被呼叫。CURLFORM_CONTENTSLENGTH也是個非常重要的引數。如果我們不設定CURLFORM_CONTENTSLENGTH,則傳遞的資料長度是資料起始至\0結尾。所以我們在呼叫curl_formadd之前先計算了資料的長度——檔案的大小。然後指定CURLFORM_FILENAME為伺服器上儲存的檔名。

組裝上傳資料

    CURLcode CPostByCurl::ModifyEasyCurl_Mem( CURL* pEasyCurl, const FMParam& Param ) {
        if (Param.meminfo.bMulti) {
            Param.value->MFSeek(0L, SEEK_END);
            long valuesize = Param.value->MFTell();
            Param.value->MFSeek(0L, SEEK_SET);
            curl_formadd(&m_pFormpost, &m_pLastptr, 
                CURLFORM_COPYNAME, Param.strkey.c_str(), 
                CURLFORM_STREAM, Param.value, 
                CURLFORM_CONTENTSLENGTH, valuesize,
                CURLFORM_CONTENTTYPE, "application/octet-stream",
                CURLFORM_END );
        }
        else {
            if (!m_strCommonPostData.empty()) {
                m_strCommonPostData += "&";
            }
            std::string strpostvalue;
            while(!Param.value->MFEof()) {
                char buffer[1024] = {0};
                size_t size = Param.value->MFRead(buffer, 1, 1024);
                strpostvalue.append(buffer, size);
            }
            m_strCommonPostData += Param.strkey;
            m_strCommonPostData += "=";
            m_strCommonPostData += strpostvalue;
        }
        return CURLE_OK;
    }

        對於需要MultiPart格式傳送的資料,我們傳送的方法和檔案傳送相似——只是少了CURLFORM_FILENAME設定——因為沒有檔名。

        對於普通Post資料,我們使用m_strCommonPostData拼接起來。待之後一併傳送。

設定資料待上傳        

CURLcode CPostByCurl::ModifyEasyCurl( CURL* pEasyCurl, const FMParams& Params ) {
        for (FMParamsCIter it = m_PostParam.begin(); it != m_PostParam.end(); it++ ) {
            if (it->postasfile) {
                ModifyEasyCurl_File(pEasyCurl, *it);
            }
            else {
                ModifyEasyCurl_Mem(pEasyCurl, *it);
            }
        }

        if (m_pFormpost){
            curl_easy_setopt(pEasyCurl, CURLOPT_HTTPPOST, m_pFormpost);
        }

        if (!m_strCommonPostData.empty()) {
            curl_easy_setopt(pEasyCurl, CURLOPT_COPYPOSTFIELDS, m_strCommonPostData.c_str());
        }

	return CURLE_OK;
}

        通過設定CURLOPT_HTTPPOST,我們將MultiPart型資料——包括檔案上傳資料設定好。通過設定CURLOPT_COPYPOSTFIELDS,我們將普通Post型資料設定好。

        Get型請求沒什麼好說的。詳細見之後給的工程原始碼。

        工程原始碼連結:http://pan.baidu.com/s/1i3eUnMt 密碼:hfro