libcurl,多線程,gzip,共享DNS

轉載

http://hi.baidu.com/jjxiaoyan/item/e17b9ec3e31b93d4964452d8


libcurl是一個不錯的socket庫,而且又是開源的。如果僅僅是簡單的HTTP請求,那麼只需要幾行代碼就能輕鬆實現。不過要用libcurl實現高效、高頻率的HTTP請求就需要對libcurl有深入的瞭解纔行。如果閱讀英文無障礙的話,那麼libcurl自帶的示例程序和幫助文檔就是最好的老師。

一、多線程HTTP請求

libcurl提供多線程和異步請求來實現大批量HTTP請求,可參見multithread.c和multi-app.c兩個示例程序。這兩種批量HTTP請求的方式在測試環境下都能正常運行,但使用異步請求總是會出現問題,於是將目標轉向多線程請求。

多線程HTTP請求要注意的幾個問題:

1. 千萬不要在多線程之間共享同一個CURL對象

在libcurl中,第一步要做的就是使用curl_easy_int函數來初始化一個CURL對象,每個CURL對應一個HTTP連接。於是,在批量請求時爲了省去每次進行HTTP連接的時間,會對多個HTTP請求使用同一個CURL對象。這在非多線程狀態下是不會出問題的,但在多線程下則會出問題。具體原因未知,網上查找到的資料對此解釋不太詳細。

所以我們需要爲每一個線程建立一個CURL對象:

void threadfunc( void *p )
{
    CURL *curl;
    
    curl = curl_easy_init();
    ...
    ...
    ...
    curl_easy_cleanup( curl );
}

2. 避免多個線程中同時調用curl_global_init函數

在多線程環境下,應在主線程中使用curl_global_init和curl_global_cleanup函數。

第一次調用 curl_easy_init()時,curl_easy_init 會調用 curl_global_init,在單線程環境下,這不是問題。但是多線程下就不行了,因爲curl_global_init不是線程安全的。在多個線程中調用curl_easy_int,然後如果兩個線程同時發現curl_global_init還沒有被調用,同時調用 curl_global_init,悲劇就發生了。這種情況發生的概率很小,但可能性是存在的。

int main()
{
    curl_global_init( CURL_GLOBAL_ALL );

    /* 創建多線程代碼 */
    ...
    ...

    curl_global_cleanup();
    return 0;
}

3. 域名解析的設定

引用:

libcurl 有個很好的特性,它甚至可以控制域名解析的超時。但是在默認情況下,它是使用alarm + siglongjmp 實現的。用alarm在多線程下做超時,本身就幾乎不可能。如果只是使用alarm,並不會導致程序崩潰,但是,再加上siglongjmp,就要命了(程序崩潰的很可怕,core中幾乎看不出有用信息),因爲其需要一個sigjmp_buf型的全局變量,多線程修改它。(通常情況下,可以每個線程一個 sigjmp_buf 型的變量,這種情況下,多線程中使用 siglongjmp 是沒有問題的,但是libcurl只有一個全局變量,所有的線程都會用)。 具體是類似 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L) 的超時設置(發生在域名解析階段),導致alarm的使用,如前所述,這在多線程中是有衝突的。解決方式是禁用掉alarm這種超時, curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L)。 這樣,多線程中使用超時就安全了。但是域名解析就沒了超時機制,碰到很慢的域名解析,也很麻煩。文檔的建議是 Consider building libcurl with c-ares support to enable asynchronous DNS lookups, which enables nice timeouts for name resolves without signals. c-ares 是異步的 DNS 解決方案。 參考:http://gcoder.blogbus.com/logs/54871550.html 

4. DNS共享

參考文章:http://blog.csdn.NET/colinw/article/details/6534025

由於每個CURL對象都會連接一次服務器,如果發送1000次HTTP請求都連接到同一服務器,libcurl就會返回大量連接錯誤和接收錯誤,爲此使用DNS共享是很有必要的。

void set_share_handle(CURL* curl_handle)
{
    static CURLSH* share_handle = NULL;
    if (!share_handle)
    {
        share_handle = curl_share_init();
    curl_share_setopt(share_handle, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
    }
    curl_easy_setopt(curl_handle, CURLOPT_SHARE, share_handle);
    curl_easy_setopt(curl_handle, CURLOPT_DNS_CACHE_TIMEOUT, 60 * 5);
}

void threadfunc( void *p )
{
    CURL *curl;
    
    set_share_handle( curl );
    curl = curl_easy_init();
    ...
    ...
    ...
    curl_easy_cleanup( curl );
}

二、接收gzip數據及解壓縮gzip

假如一個網頁有180KB大小,使用gzip算法壓縮後可能就只有60KB大小。目前絕大部分網站都支持gzip,這樣用戶向網站請求獲取的數據是gzip格式的,下載會用戶電腦後再由瀏覽器對gzip數據進行解壓,這樣可以大大提高網站的瀏覽速度。

要讓libcurl接受gzip編碼很簡單,只需要加入一行代碼:

curl_easy_setopt(curl, CURLOPT_ENCODING, "gzip");

關鍵的問題是如何解壓縮gzip數據,這需要用到zlib庫。下面是從網上找的一個解壓gzip數據的函數:

/* HTTP gzip decompress */
/* 參數1.壓縮數據 2.數據長度 3.解壓數據 4.解壓後長度 */
int CHttp::httpgzdecompress(Byte *zdata, uLong nzdata, Byte *data, uLong *ndata)
{
    int err = 0;
    z_stream d_stream = {0}; /* decompression stream */
    static char dummy_head[2] = 
    {
        0x8 + 0x7 * 0x10,
        (((0x8 + 0x7 * 0x10) * 0x100 + 30) / 31 * 31) & 0xFF,
    };
    d_stream.zalloc = (alloc_func)0;
    d_stream.zfree = (free_func)0;
    d_stream.opaque = (voidpf)0;
    d_stream.next_in  = zdata;
    d_stream.avail_in = 0;
    d_stream.next_out = data;
    if(inflateInit2(&d_stream, 47) != Z_OK) return -1;
    while (d_stream.total_out < *ndata && d_stream.total_in < nzdata) 
    {
        d_stream.avail_in = d_stream.avail_out = 1; /* force small buffers */
        if((err = inflate(&d_stream, Z_NO_FLUSH)) == Z_STREAM_END) break;
        if(err != Z_OK )
        {
            if(err == Z_DATA_ERROR)
            {
                d_stream.next_in = (Bytef*) dummy_head;
                d_stream.avail_in = sizeof(dummy_head);
                if((err = inflate(&d_stream, Z_NO_FLUSH)) != Z_OK) 
                {
                    return -1;
                }
            }
            else return -1;
        }
    }
    if(inflateEnd(&d_stream) != Z_OK) return -1;
    *ndata = d_stream.total_out;
    return 0;
}

使用示例:

//解壓縮buffer中的數據
int ndesize = 1024000;//此處長度需要足夠大以容納解壓縮後數據
char *szdebuffer = new char[ndesize];
memset( szdebuffer, 0, ndesize ); 
int err; //錯誤變量的定義
/* 執行httpgzdecompress後,會在ndesize中保存解壓後的數據長度 */
err = httpgzdecompress( ( Byte* ) szbuffer.c_str(), ( uLong ) szbuffer.size(), ( Byte* ) szdebuffer, ( uLong* ) &ndesize );

if ( err == Z_OK )
{
    /* 成功解壓 */
}

參考文章:

安裝zlib
http://www.linuxidc.com/Linux/2012-06/61982p2.htm

gzip的壓縮與解壓縮
http://www.cppblog.com/woaidongmao/archive/2011/06/05/148089.html

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