curl_multi異步高併發服務實現

自己開發了一個股票軟件,功能很強大,需要的點擊下面的鏈接獲取:

https://www.cnblogs.com/bclshuai/p/11380657.html

curl_multi 異步高併發服務實現

目錄

1      介紹... 1

2      curl_multi異步實現... 1

2.1          curl_multi_poll方式實現異步curl 2

2.1.1     函數調用步驟... 2

2.1.2     實現方案... 2

2.1.3     遇到的問題... 3

2.1.4     curl_multi_poll異步服務封裝類實例... 3

2.2          multi_socket實現異步curl 4

 

1        介紹

https://curl.se/libcurl/c/libcurl-multi.html

libcurl-easy方式是阻塞執行請求,當請求數量過大,或者需要高併發請求時,同步阻塞模式就會顯示出性能瓶頸,執行效率低,延時嚴重,CPU佔用率高,程序阻塞卡頓。所以採用異步方式,可以實現高併發請求的應用場景,異步可以在單線程中同時執行多個請求,等待curl文件標誌或者自定義文件標誌發生變化時,處理請求結果,支持在幾千個平行連接上請求數據,基於事件處理結果。

2        curl_multi異步實現

異步請求有兩種方式,同步多線程調用會出現CPU佔用率過高的情況,導致界面卡死。

有兩種方式

(1)    老方式select來判斷請求返回結果

(2)    multi_socket。

2.1  curl_multi_poll方式實現異步curl

2.1.1          函數調用步驟

(1)    curl_multi_init初始化一個multi handle

(2)    curl_easy_init 初始化一個easy handle

(3)    curl_easy_setopt 給easyhandle設置各種參數

(4)    curl_multi_add_handle添加到multihandle

(5)    curl_multi_perform異步執行請求,每次執行返回對列中正在運行的數量,爲0時,表示執行結束,結束並不意味着所有的請求都成功了,也可能執行失敗了。所以需要循環執行該函數。爲了減少循環執行的CPU佔用率,可以使用curl_multi_poll函數或者curl_multi_fdset配合select函數來判斷是否有結果返回,通知讀取數據,減少CPU佔用。curl_multi_timeout可以爲select提供一個合適的超時時間。

(6)    curl_multi_info_read 讀取返回結果消息隊列中的消息,重複調用,直到消息隊列爲空。返回數據中有個easy handle 用來標識是哪個請求。

(7)    curl_multi_remove_handle 將執行結束的easyhandle從multihandle中移除,表示multihandle不再管理此easyhand,可以銷燬釋放,也可以修改請求連接url和參數,重新加入,複用連接。

(8)    curl_easy_cleanup 執行結束後,先清除easy handle

(9)    curl_multi_cleanup 執行這個函數,清除multi handle

2.1.2          實現方案

(1)    實現一個服務,程序調用服務的添加任務接口addTask不斷的加入任務。

(2)    創建一個線程1不斷的從任務隊列中取出任務,分配easy handle,給easy_hand設置url等參數。然後添加到multihandle上,去執行請求;並將easyhand和任務之間用map保存起來,表示正在進行的任務;

(3)    創建線程2不斷的select或者curl_multi_poll或者curl_multi_wait或者查看multihandle的狀態,看是否有數據返回,有數據返回則讀取數據。curl_multi_poll和curl_multi_wait比select更好,可以解決連接上限爲1024個的問題。curl_multi_poll和curl_multi_wait區別有兩個,一個是curl_multi_poll在被等待的時間內,可以調用curl_multi_wakeup激活,curl_multi_poll會加速返回。而curl_multi_wait無法被激活,只能等到有事件觸發,或者超時返回。另外一個區別是如果沒有文件描述符可以等待,curl_multi_wait會立刻返回,而curl_multi_poll一定要等到超時時間才能返回。

(4)    讀取數據會返回easyhand,用easyhand去map中查找對應的任務;然後根據不同的任務屬性去處理數據,調用回調函數,將數據返回給程序。

 

2.1.3          遇到的問題

(1)    出現崩潰,可能是多線程調用libcurl接口的原因;

(2)    curl_multi_add_handle添加easyhand返回失敗,errocode:8,CURLM_RECURSIVE_API_CALL,錯誤原因是從回調內部調用API函數。沒有找到解決辦法,可能和多線程調用有關。設置超時時間curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30); 超時設置爲0時會出現崩潰,設置成0表示不超時;並且設置不發出信號,curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);解決問題。

(3)    當使用的easyhand太多時,200個,會出現錯誤碼CURLE_COULDNT_CONNECT ,7的錯誤,意識無法連接()到主機或代理。connect refused。連接數量太多,需要創建太多socket連接,而服務器端創建的連接數量有限,導致失敗。

(4)    一次向multihand添加1000條任務,curl_multi_perform執行返回任務從1000降到0後,並不是所有分任務都執行完了,讀取的數據也就600條左右,需要多次調用curl_multi_info_read去讀取數據。

 

2.1.4          curl_multi_poll異步服務封裝類實例

本代碼實例採用curl_multi_poll實現異步消息的等待,比select性能更加,去除了select上限1024的束縛。把異步請求調用過程封裝成一個服務形式,所有的異步請求都可以發給服務去執行,然後通過回調函數返回結果。其中easyhand做成一個連接池的形式,可以重複使用,並且可以複用連接,提高了請求性能。代碼實例中運用到了C++11新特性的內容,包括智能指針、std::thread、std::move等。

(1)    封裝類頭文件

 

#pragma once

#include"curl.h"
#include <mutex>
#include <condition_variable>
#include"BaseDefine.h"


class CurlSelectMulti
{
public:
    CurlSelectMulti();
    ~CurlSelectMulti();
    //全局初始化
    static void GlobalInit();
    //全局反初始化
    void GlobleFint();
    //初始化
    int init();
    //反初始化
    void finit();
    //添加任務到隊列
    void addTask(shared_ptr<Task>& task);
    
private:
    //處理任務,循環從對列中獲取數據,添加到muitihand
    void dealTask();
    //檢查是否有任務完成
    void handTaskResult();
    //讀取已完成的任務進行解析
    void readTaskResult();
    //從easyhand隊列中獲取easyhand,沒有則新建一個
    CURL* GetCurl();
    //新建一個easyhand
    CURL* CreateCurl();
    //將使用完的easyhand放入隊列
    void PutCurl(CURL* curl);
    //將任務添加到mulitihand,執行任務
    void addTaskToMultiRequest(list<shared_ptr<Task>>& listTask);
    //給easyhand設置參數
    int setTaskParameter(CURL* easyhand, shared_ptr<Task>& task);
    bool m_bDebug=false;
    CURL* m_pMultiHand=nullptr;//多操作句柄
    list<shared_ptr<Task>> m_listTask;//任務列表
    mutex m_taskMutex;//任務列表的鎖
    mutex m_easyHandMutex;//easyhand隊列的鎖
    list<CURL*>m_listEasyHand;// easyhand隊列
    bool m_bRunning = true;//線程控制函數
    thread m_taskAddThread;//投遞任務的的線程
    thread m_taskHandThread;//判斷任務狀態, 處理任務的線程
    condition_variable m_conVarTask;
    map<CURL*, std::shared_ptr<Task>> m_mapRuningTask;//正在執行的任務
    mutex m_runningTaskMutex;
    mutex m_curlApiMutex;//多線程調用curl的接口時會出現崩潰,這裏加個鎖
    int m_curlnum = 0;//
    int m_successnum=0;
    int m_failednum = 0;
    int m_addmultFailed;
};

 

(2)    封裝類源文件

 

#include "stdafx.h"
#include "CurlSelectMulti.h"

static int OnDebug(CURL *, curl_infotype itype, char * pData, size_t size, void *)
{
    if (itype == CURLINFO_TEXT)
    {
        //printf("[TEXT]%s\n", pData);
    }
    else if (itype == CURLINFO_HEADER_IN)
    {
        printf("[HEADER_IN]%s\n", pData);
    }
    else if (itype == CURLINFO_HEADER_OUT)
    {
        printf("[HEADER_OUT]%s\n", pData);
    }
    else if (itype == CURLINFO_DATA_IN)
    {
        printf("[DATA_IN]%s\n", pData);
    }
    else if (itype == CURLINFO_DATA_OUT)
    {
        printf("[DATA_OUT]%s\n", pData);
    }
    return 0;
}

static size_t OnWriteData(void* buffer, size_t size, size_t nmemb, void* lpVoid)
{
    std::string* str = dynamic_cast<std::string*>((std::string *)lpVoid);
    if (NULL == str || NULL == buffer)
    {
        return -1;
    }

    char* pData = (char*)buffer;
    str->append(pData, size * nmemb);
    return nmemb;
}

CurlSelectMulti::CurlSelectMulti()
{

}


CurlSelectMulti::~CurlSelectMulti()
{
    finit();
}

void CurlSelectMulti::GlobalInit()
{
    curl_global_init(CURL_GLOBAL_ALL);
}
void CurlSelectMulti::GlobleFint()
{
    curl_global_cleanup();
}

int CurlSelectMulti::init()
{    //創建一個multi句柄
    m_pMultiHand = curl_multi_init();
    if (m_pMultiHand == nullptr)
    {
        return false;
    }
    m_bRunning = true;
    m_taskAddThread=std::move(thread(std::bind(&CurlSelectMulti::dealTask, this)));
    m_taskHandThread = std::move(thread(std::bind(&CurlSelectMulti::handTaskResult,this)));
    //m_taskAddThread.join();
    //m_taskHandThread.join();
    return true;
}

void CurlSelectMulti::finit()
{
    //讓線程自動退出
    m_bRunning = false;
    //通知不在等待
    m_conVarTask.notify_all();
    //清除所有的easycurl
    while (m_listEasyHand.size()>0)
    {
        auto it = move(m_listEasyHand.front());
        curl_multi_remove_handle(m_pMultiHand,it);
        curl_easy_cleanup(it);
        m_listEasyHand.pop_front();
    }
    //清除multihand
    if (m_pMultiHand != nullptr)
    {
        curl_multi_cleanup(m_pMultiHand);
        m_pMultiHand = nullptr;
    }

}

void CurlSelectMulti::addTask(shared_ptr<Task>& task)
{
    if (m_listTask.size() > 5000)
    {
        //printf("task is full size %d ,abord task %d", m_listTask.size(),task->taskid);
        return;
    }
    unique_lock<mutex> lk(m_taskMutex);
    m_listTask.push_back(task);
    //m_conVarTask.notify_one();//通知有任務添加
    lk.unlock();
}

CURL* CurlSelectMulti::GetCurl()
{
    CURL* curl = NULL;
    m_easyHandMutex.lock();
    if (m_listEasyHand.size()>0)
    {

        curl = m_listEasyHand.front();
        m_listEasyHand.pop_front();

    }
    m_easyHandMutex.unlock();
    if (curl == NULL)
    {
        curl = CreateCurl();
    }
    return curl;
}

CURL* CurlSelectMulti::CreateCurl()
{
    
    if (m_curlnum >100)//數量太多會出現連接失敗的error
    {
        return NULL;
    }
    m_curlnum++;
    printf("curl num %d", m_curlnum);
    CURL* curl = curl_easy_init();
    if (NULL == curl)
    {
        return NULL;
    }
    if (m_bDebug)
    {
        curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
        curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, OnDebug);
    }
    //curl_easy_setopt(curl, CURLOPT_URL, strUrl.c_str());
    curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);
    //curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);
    /* enable TCP keep-alive for this transfer */
    curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L);
    /* keep-alive idle time to 120 seconds */
    curl_easy_setopt(curl, CURLOPT_TCP_KEEPIDLE, 300L);
    /* interval time between keep-alive probes: 60 seconds */
    curl_easy_setopt(curl, CURLOPT_TCP_KEEPINTVL, 200L);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 100);
    //支持重定向
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
    //保持會話,不用反覆創建連接,據說可以提高效率
    curl_easy_setopt(curl, CURLOPT_COOKIESESSION, 1);
    //設置共享dns cache功能,據說能提高性能
    curl_share_setopt(curl, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
    //不驗證主機名稱
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    //不驗證對端的證書
    //curl_easy_setopt(curl, CURLOPT_CAINFO, c->msg._caPath.c_str());
    /**
    * 當多個線程都使用超時處理的時候,同時主線程中有sleep或是wait等操作。
    * 如果不設置這個選項,libcurl將會發信號打斷這個wait從而導致程序退出。
    */
    curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 100);
    return curl;
}

void CurlSelectMulti::PutCurl(CURL* curl)
{

    m_easyHandMutex.lock();
    m_listEasyHand.push_back(curl);
    m_easyHandMutex.unlock();
    //m_conVarEasyHand.notify_all();
}

void CurlSelectMulti::dealTask()
{
    while (m_bRunning)
    {
        unique_lock<mutex> lk(m_taskMutex);
        //m_conVarTask.wait(lk);//等待任務添加
        if (m_listTask.size() > 0)
        {
            list<shared_ptr<Task>> listTask;
            listTask.swap(m_listTask);
            lk.unlock();
            addTaskToMultiRequest(listTask);
        }
        else
        {
            lk.unlock();
            Sleep(20);
        }
        
        
        
    }
}

void CurlSelectMulti::handTaskResult()
{
    CURLMcode mc = CURLM_OK; 
    int still_running = 0;
    int ret = 0;
    while (m_bRunning)
    {
        m_curlApiMutex.lock();
        //執行請求,並返回正在執行的請求數量
        mc = curl_multi_perform(m_pMultiHand, &still_running);
        //printf("still running num%d,%d\n", still_running, mc);
        m_curlApiMutex.unlock();
        //等待有任務完成的通知,有結果時立刻返回,沒有結果時1000ms後等待結束返回,ret返回完成的任務數量
        mc = curl_multi_poll(m_pMultiHand, NULL, 0, 1000, &ret);
        if (mc == CURLM_OK)//有任務
        {
            readTaskResult();
        }
        else
        {
            printf("curl_multi_poll error %d", mc);
        }
        //if (still_running>0)//有正在執行的請求任務
        //{
        //    while (still_running>0)
        //    {
        //        
        //        //再次執行curl_multi_perform,更新still_running
        //        mc = curl_multi_perform(m_pMultiHand, &still_running);
        //        //printf("still running num%d\n", still_running);
        //    }
        //    printf("task finish\n");
        //}
        //else//沒有任務,則等一會,避免一直循環,cpu佔用過高
        //{
        //    Sleep(100);
        //}
        
    }
}

void CurlSelectMulti::readTaskResult()
{
    CURLMsg* m = NULL;
    do {
        int msgq = 0;
        m_curlApiMutex.lock();
        m = curl_multi_info_read(m_pMultiHand, &msgq);
        m_curlApiMutex.unlock();
        if (m && (m->msg == CURLMSG_DONE))
        {
            CURL *e = m->easy_handle;
            // 數據處理
            auto it = m_mapRuningTask.find(e);
            if (it != m_mapRuningTask.end())
            {
                if (m->data.result != 0)
                {
                    m_failednum++;
                    printf("request error %d,failednum%d,taskid%d,%s\n", m->data.result, m_failednum, m_mapRuningTask[e]->taskid, m_mapRuningTask[e]->strUrl.c_str());
                }
                else
                {
                    m_successnum++;
                    printf("request success successnum%d,id %d,    \n ",  m_successnum, m_mapRuningTask[e]->taskid);//, , m_mapRuningTask[e]->strResponse.c_str()
                }
                //移除easyhand
                m_curlApiMutex.lock();
                curl_multi_remove_handle(m_pMultiHand, e);
                m_curlApiMutex.unlock();
                
                //從map中移除
                m_runningTaskMutex.lock();
                if (it->second->headers != nullptr)//清除數據
                {
                    curl_slist_free_all(it->second->headers);
                }    
                m_mapRuningTask.erase(it);
                m_runningTaskMutex.unlock();
                //放回對列中重複使用
                PutCurl(e);
                
            }
            else {
                //移除easyhand
                m_curlApiMutex.lock();
                curl_multi_remove_handle(m_pMultiHand, e);
                m_curlApiMutex.unlock();
                printf( "find map key failed" );
                PutCurl(e);
            }
        }
    } while (m);
}

void CurlSelectMulti::addTaskToMultiRequest(list<shared_ptr<Task>>& listTask)
{
    while (listTask.empty()==false)
    {
        auto item = listTask.front();
        CURL* easyhand = GetCurl();//獲取easyhand
        if (easyhand == NULL)
        {
            //unique_lock<mutex> lk(m_easyHandMutex);
            //m_conVarEasyHand.wait(lk);//等待有easyhand被放入
            Sleep(1);
            continue;
        }
        //根據任務設置參數,設置url,timeout等參數到easyhand
        //使用智能指針,指向在堆上創建的對象
        /*shared_ptr<Task> task(new Task());
        *task = item;*/
        if (setTaskParameter(easyhand, item) != 0)
        {
            Sleep(2);
            PutCurl(easyhand);
            continue;
        }
        //將設置好參數的easyhand添加到multihand    
        m_curlApiMutex.lock();
        CURLMcode code = curl_multi_add_handle(m_pMultiHand, easyhand);//當任務數量太大時出現添加失敗錯誤碼8,有時還崩潰。
        m_curlApiMutex.unlock();
        if (code!= CURLM_OK)
        {
            m_addmultFailed++;
            string strerror= curl_multi_strerror(code);
            printf("curl_multi_add_handle failed%d,%s\n", m_addmultFailed,  strerror.c_str());
            PutCurl(easyhand);//
            continue;
        }
        //加入成功後將easyhand和task加入map,便於返回結果時通過easyhand去查找任務
        m_runningTaskMutex.lock();
        m_mapRuningTask.insert({ easyhand, item });
        printf("running task size:%d\n", m_mapRuningTask.size());
        m_runningTaskMutex.unlock();
        listTask.pop_front();    
    }
}

int CurlSelectMulti::setTaskParameter(CURL* easyhand, shared_ptr<Task>& task)
{
    CURLcode code = CURLE_OK;
    do 
    {
        if (task->iType == HttpType::HTTP_POST || task->iType == HttpType::HTTPS_POST)
        {
            code = curl_easy_setopt(easyhand, CURLOPT_POST, 1);  //post方法    
            if (code!= CURLE_OK)
            {
                printf("curl_easy_setopt error %d", code);
                break;
            }
            code = curl_easy_setopt(easyhand, CURLOPT_POSTFIELDSIZE, task->strPostContent.size());
            if (code != CURLE_OK)
            {
                printf("curl_easy_setopt error %d", code);
                break;
            }
            code = curl_easy_setopt(easyhand, CURLOPT_POSTFIELDS, task->strPostContent.data());
            if (code != CURLE_OK)
            {
                printf("curl_easy_setopt error %d", code);
                break;
            }
        }
        //設置url
        code = curl_easy_setopt(easyhand, CURLOPT_URL, task->strUrl.c_str());
        if (code != CURLE_OK)
        {
            printf("curl_easy_setopt error %d", code);
            break;
        }
        code = curl_easy_setopt(easyhand, CURLOPT_WRITEDATA, (void *)&(task->strResponse));
        if (code != CURLE_OK)
        {
            printf("curl_easy_setopt error %d", code);
            break;
        }
        
        //設置協議頭
        if (task->headers != nullptr)
        {
            code=curl_easy_setopt(easyhand, CURLOPT_HTTPHEADER, task->headers);
            if (code != CURLE_OK)
            {
                printf("curl_easy_setopt error %d", code);
                break;
            }
        }    
    } while (0);
    
    if (code!=CURLE_OK)//清除掉無效的easyhand
    {
        printf("setTaskParameter error");
        PutCurl(easyhand);
        return -1;
    }
    return 0;
}

 

(3)    服務封裝類使用實例

 

// CurlMultiServer.cpp : 定義控制檯應用程序的入口點。
//

#include "stdafx.h"
#include <iostream>
#include <fstream>
#include <thread>
#include "CurlSelectMulti.h"
#include "CurlSocketMulti.h"

using namespace std;

int main()
{
    CurlSelectMulti m_multServer;
    m_multServer.GlobalInit();
    m_multServer.init();
    
    
    int taskid=0;
    int time = 0;
    //while (true)
    {
        for (int i = 0; i < 10000; i++)
        {
            shared_ptr<Task> task = std::make_shared<Task>();
            task->iType = HttpType::HTTPS_GET;
            task->strUrl = "";
            taskid++;
            task->taskid = taskid;
            m_multServer.addTask(task);     
        }
        //Sleep(1000);
        //printf("using time %ds,task num %d",(time++) * 1, time * 100);
    }
    getchar();
    return 0;
}

(4)    性能測試

在如下電腦配置條件下,請求了某個http請求1萬次,所用時間14秒,平均一次1.4毫秒。每秒714次請求調用。

 

 

 

 

 

 

另外還有multi_socket和事件libevent結合的方式,且聽下回分解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章