實現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