首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

C++ libcurl 實戰:構建高性能 HTTP 客戶端抓取工具

     編輯:關於C++

libcurl 簡介與應用場景

libcurl 是功能強大、支持協議豐富(HTTP/HTTPS/FTP/SMTP 等)的 C 語言網絡傳輸庫。在 C++ 項目中,它常被用於實現網頁抓取、API 調用、文件下載/上傳、爬蟲開發等場景。本文通過一個抓取天貓/微博數據的實戰案例,詳解核心 API 使用、回調處理、超時控制及錯誤處理最佳實踐。

核心代碼實現(現代 C++ 風格重構)

#include <iostream>
#include <string>
#include <curl/curl.h>
#include <memory>
#include <stdexcept>
#include <functional>

using namespace std;

// 寫入回調函數:將接收到的數據追加到 string
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
    size_t total_size = size * nmemb;
    string* str = static_cast<string*>(userp);
    if (str) {
        str->append(static_cast<char*>(contents), total_size);
    }
    return total_size; // 返回實際處理的字節數
}

// 進度回調(可選):用於顯示下載進度
static int progress_callback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, 
                             curl_off_t ultotal, curl_off_t ulnow) {
    if (dltotal > 0) {
        cout << "\r下載進度: " << (double)dlnow / dltotal * 100 << "%" << flush;
    }
    return 0; // 返回 0 繼續,非 0 取消傳輸
}

// RAII 封裝:自動管理 curl 全局初始化/清理
class CurlGlobalInit {
public:
    CurlGlobalInit() { curl_global_init(CURL_GLOBAL_DEFAULT); }
    ~CurlGlobalInit() { curl_global_cleanup(); }
    // 禁用拷貝
    CurlGlobalInit(const CurlGlobalInit&) = delete;
    CurlGlobalInit& operator=(const CurlGlobalInit&) = delete;
};

// HTTP 客戶端封裝類
class HttpClient {
public:
    HttpClient() : curl_(curl_easy_init()) {
        if (!curl_) throw runtime_error("curl_easy_init 失敗");
        // 設置默認選項
        curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, write_callback);
        curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1L);      // 多線程環境下禁用信號
        curl_easy_setopt(curl_, CURLOPT_FOLLOWLOCATION, 1L); // 支持重定向
        curl_easy_setopt(curl_, CURLOPT_MAXREDIRS, 10L);     // 最大重定向次數
        curl_easy_setopt(curl_, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; CppHttpClient/1.0)");
    }

    ~HttpClient() {
        if (curl_) curl_easy_cleanup(curl_);
    }

    // 禁用拷貝,支持移動
    HttpClient(const HttpClient&) = delete;
    HttpClient& operator=(const HttpClient&) = delete;
    HttpClient(HttpClient&& other) noexcept : curl_(other.curl_) { other.curl_ = nullptr; }
    HttpClient& operator=(HttpClient&& other) noexcept {
        if (this != &other) {
            if (curl_) curl_easy_cleanup(curl_);
            curl_ = other.curl_;
            other.curl_ = nullptr;
        }
        return *this;
    }

    // 設置超時(毫秒)
    void set_timeout(long connect_ms, long total_ms) {
        curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT_MS, connect_ms);
        curl_easy_setopt(curl_, CURLOPT_TIMEOUT_MS, total_ms);
    }

    // 設置請求頭
    void set_headers(const vector<string>& headers) {
        headers_list_ = nullptr;
        for (const auto& h : headers) {
            headers_list_ = curl_slist_append(headers_list_, h.c_str());
        }
        curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers_list_);
    }

    // 執行 GET 請求
    string get(const string& url) {
        string response;
        CURLcode res = perform_request(url, response, nullptr, 0);
        if (res != CURLE_OK) {
            throw runtime_error("GET 請求失敗: " + string(curl_easy_strerror(res)));
        }
        return response;
    }

    // 執行 POST 請求
    string post(const string& url, const string& post_data) {
        string response;
        CURLcode res = perform_request(url, response, post_data.c_str(), post_data.size());
        if (res != CURLE_OK) {
            throw runtime_error("POST 請求失敗: " + string(curl_easy_strerror(res)));
        }
        return response;
    }

    // 獲取 HTTP 響應碼
    long get_response_code() const {
        long code = 0;
        curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &code);
        return code;
    }

    // 獲取有效 URL(處理重定向後)
    string get_effective_url() const {
        char* url = nullptr;
        curl_easy_getinfo(curl_, CURLINFO_EFFECTIVE_URL, &url);
        return url ? string(url) : "";
    }

private:
    CURL* curl_;
    struct curl_slist* headers_list_ = nullptr;

    // 通用請求執行邏輯
    CURLcode perform_request(const string& url, string& response, 
                             const char* post_data, size_t post_len) {
        response.clear();
        
        curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response);
        
        if (post_data && post_len > 0) {
            curl_easy_setopt(curl_, CURLOPT_POST, 1L);
            curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, post_data);
            curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, (long)post_len);
        } else {
            curl_easy_setopt(curl_, CURLOPT_POST, 0L);
        }
        
        // 可選:啟用進度回調
        // curl_easy_setopt(curl_, CURLOPT_NOPROGRESS, 0L);
        // curl_easy_setopt(curl_, CURLOPT_PROGRESSFUNCTION, progress_callback);
        
        return curl_easy_perform(curl_);
    }
};

// 使用示例
int main() {
    // 全局初始化(進程級,一次即可)
    CurlGlobalInit global_init;
    
    try {
        HttpClient client;
        client.set_timeout(3000, 10000); // 連接 3s,總超時 10s
        
        // 設置自定義請求頭(模擬瀏覽器)
        client.set_headers({
            "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8",
            "Accept-Encoding: gzip, deflate, br",
            "Connection: keep-alive"
        });
        
        // 抓取示例頁面
        string url = "https://book.weibo.com";
        cout << "正在請求: " << url << endl;
        
        string html = client.get(url);
        
        cout << "HTTP 狀態碼: " << client.get_response_code() << endl;
        cout << "最終 URL: " << client.get_effective_url() << endl;
        cout << "響應長度: " << html.length() << " 字節" << endl;
        cout << "前 500 字符:" << endl << html.substr(0, 500) << endl;
        
    } catch (const exception& e) {
        cerr << "錯誤: " << e.what() << endl;
        return -1;
    }
    
    return 0;
}

編譯與鏈接指南

Linux/macOS (g++/clang++)

# 安裝開發包
sudo apt-get install libcurl4-openssl-dev  # Ubuntu/Debian
# 或
brew install curl  # macOS

# 編譯
g++ -std=c++17 -O2 -o http_client http_client.cpp -lcurl

# 運行
./http_client

Windows (MSVC / MinGW)

# vcpkg 安裝
vcpkg install curl:x64-windows

# CMakeLists.txt 片段
find_package(CURL REQUIRED)
target_link_libraries(your_target PRIVATE ${CURL_LIBRARIES})

核心 API 深度解析

1. 易用介面 vs 多介面

  • 易用介面:curl_easy_* 系列,同步阻塞,適合簡單請求,本文採用此模式。
  • 多介面:curl_multi_* 系列,異步非阻塞,支持並發數千連接,適合高性能爬蟲/下載器。

2. 關鍵選項詳解

選項用途推薦值
CURLOPT_WRITEFUNCTION數據寫入回調自定義回調(追加到 string/vector)
CURLOPT_WRITEDATA回調的 userp 參數指向 std::string* 或 vector<char>*
CURLOPT_NOSIGNAL禁用 SIGALRM 超時機制1L(多線程必須)
CURLOPT_CONNECTTIMEOUT_MS連接階段超時3000-5000 ms
CURLOPT_TIMEOUT_MS整個請求超時10000-30000 ms
CURLOPT_FOLLOWLOCATION自動跟隨重定向1L
CURLOPT_SSL_VERIFYPEER驗證 SSL 證書1L(生產環境),0L(測試/自簽名)
CURLOPT_USERAGENT設置 User-Agent自定義標識
CURLOPT_COOKIEJAR/FILECookie 持久化文件路徑

3. 錯誤處理體系

  • curl_easy_perform 返回 CURLcode,使用 curl_easy_strerror 獲取可讀錯誤信息。
  • HTTP 層面錯誤(4xx/5xx)不會導致 CURLE_OK 失敗,需通過 CURLINFO_RESPONSE_CODE 檢查狀態碼。
  • 網絡層錯誤:CURLE_COULDNT_CONNECT、CURLE_OPERATION_TIMEDOUT、CURLE_SSL_CONNECT_ERROR 等。

進階主題:高性能異步爬蟲架構

對於大規模抓取,同步易用介面效率低下。推薦架構:

  1. 生產者-消費者模型:URL 隊列 + 工作線程池。
  2. curl_multi 事件循環:單線程管理數萬並發連接,配合 epoll/kqueue/IOCP。
  3. DNS 緩存與連接復用:啟用 CURLOPT_DNS_CACHE_TIMEOUT、CURLOPT_FORBID_REUSE=0。
  4. 限流與禮貌性:域名級別並發控制、延遲請求、遵守 robots.txt。
  5. 斷點續傳:CURLOPT_RESUME_FROM_LARGE 實現大文件分片下載。

常見坑點與避雷指南

  • 忘記 curl_global_init/cleanup:進程僅調用一次,多線程環境需線程安全初始化(CURL_GLOBAL_SSL)。
  • 回調函數返回值錯誤:必須返回實際處理字節數(size*nmemb),返回較小值會導致傳輸中斷。
  • 指針生命週期:CURLOPT_WRITEDATA 指向的對象必須在 curl_easy_perform 返回前存活。
  • 中文 URL 編碼:需對查詢參數進行 URL Encode(curl_easy_escape)。
  • HTTPS 證書驗證失敗:生產環境務必配置 CA 證書路徑(CURLOPT_CAINFO),不可長期關閉驗證。

總結

libcurl 是 C/C++ 網絡編程的基石。通過 RAII 封裝、合理的超時控制、完善的錯誤處理及異步架構設計,可構建出穩定、高效、可維護的 HTTP 客戶端。本文提供的 HttpClient 類可直接集成到項目中,並作為擴展異步爬蟲、文件下載器、API 网關客戶端的基礎組件。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved