之前我们已经详细介绍了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之前,我们需要对其初始化
bool CHttpRequestByCurl::Prepare() {
bool bSuc = false;
do {
if (!m_pCurlEasy) {
m_pCurlEasy = curl_easy_init();
}
if (!m_pCurlEasy) {
break;
}
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返回值是一致的——并不是说类型一致——而是返回值的定义一致。这就是统一成标准接口的好处。
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);
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;
}
if (!m_strAgent.empty()) {
easycode = curl_easy_setopt(m_pCurlEasy, CURLOPT_USERAGENT, m_strAgent.c_str());
CHECKCURLEASY_EROORBREAK(easycode);
}
if ( ePost == GetType() ) {
easycode = ModifyEasyCurl(m_pCurlEasy, m_Params);
CHECKCURLEASY_EROORBREAK(easycode);
}
之后我们将讲解ModifyEasyCurl的实现。我们先把整个调用过程将完。
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。
对于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