之前我们已经详细介绍了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