实现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

你可能感兴趣的:(网络通信)