LoginSignup
26
12

More than 5 years have passed since last update.

C++とcurlでHTTP通信処理作ってみた

Last updated at Posted at 2018-12-11

グレンジ Advent Calendar 2018 12日目担当の mad_khaki です。
クライアントサイドを中心にサーバ / データ分析 等もやってます。

cocos2d-x (C++) から curl を使って並列に通信リクエストを投げる必要があったのでまとめました

サポーターズでもよく発表しています
【サポーターズCoLab勉強会】C++とcurlでHTTP通信処理作ってみた

環境

  • macOS Mojave 10.14.1
  • Xcode Version 10.0 (10A255)
  • curl 7.54.0

curlの導入

リンクするライブラリにlibcurlを追加するだけ

lib.png

brewでinstallしてコンパイル時に-lcurlでも可

brew install curl

curlの使い方サンプル

基本的には
1. curl/curl.hのインクルード
2. curl_easy_initでcurlインスタンスの初期化
3. curl_easy_setoptで必要なパラメータを設定
4. curl_easy_performで通信実行
5. curl_easy_cleanupで後始末
の5ステップ

GoogleのトップページをDLして、tmp.htmlに書き出すサンプル

#include <iostream>
#include <fstream>
#include <vector>
#include <curl/curl.h>

size_t onReceive(char* ptr, size_t size, size_t nmemb, void* stream) {
    // streamはCURLOPT_WRITEDATAで指定したバッファへのポインタ
    std::vector<char>* recvBuffer = (std::vector<char>*)stream;
    const size_t sizes = size * nmemb;
    recvBuffer->insert(recvBuffer->end(), (char*)ptr, (char*)ptr + sizes);
    return sizes;
}

int main(int argc, const char * argv[]) {
    // curlのセットアップ
    CURL *curl = curl_easy_init();
    if (curl == nullptr) {
        curl_easy_cleanup(curl);
        return 1;
    }
    // レスポンスデータの格納先
    std::vector<char> responseData;
    // 接続先URL
    curl_easy_setopt(curl, CURLOPT_URL, "https://www.google.com");
    // サーバのSSL証明書の検証をしない
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    // レスポンスのコールバック
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, onReceive);
    // 書き込みバッファを指定
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData);

    // 通信実行
    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        curl_easy_cleanup(curl);
        return 1;
    }

    // 後始末
    curl_easy_cleanup(curl);

    std::ofstream ofs;
    ofs.open("tmp.html", std::ios::out);
    if (ofs.is_open()) {
        ofs << responseData.data() << std::endl;
    }
    return 0;
}

実現したいこと

メインスレッドとは別のスレッドで並列にzipファイルをDLし解凍する
graph.png

通信部分

    bool processGetTask(const std::string& url,
                        const std::vector<std::string>& headers,
                        std::vector<char>* stream,
                        long* responseCode,
                        char* errorBuffer) {
        CURLRaii curl;
        bool success = curl.init(url, headers, stream, errorBuffer)
        // ヘッダのLocationを辿る (リダイレクト対応)
        && curl.setOption(CURLOPT_FOLLOWLOCATION, true)
        && curl.perform(responseCode);

        return success;
    }

    void execute(RequestData data) {
        std::vector<char> responseData;
        responseData.reserve(data.getSize());
        long responseCode = -1;
        char errorBuffer[256];

        bool success = ::processGetTask(data.getURL(), {}, &responseData, &responseCode, errorBuffer);

        if (success) {
            // 成功時の処理
        } else {
            // 失敗時の処理
        }
    }

namespace Downloader {
    void start(RequestData data) {
        std::thread t(std::bind(::execute, data));
        t.detach();
    }
}

RequestDataにはURL以外にも解凍前サイズや解凍後サイズなどを持たせておけば、「正常にDL出来たか」「正常に解凍できたか」などを検証したりもできる

 

GETで実装しているが、POSTなら以下のオプションを使う

    curl.setOption(CURLOPT_POST, 1); // POST通信を行う
    curl.setOption(CURLOPT_POSTFIELDS, RequestData.c_str()); // POSTで送信するデータ
    curl.setOption(CURLOPT_POSTFIELDSIZE, RequestData.size()); // POSTで送信するデータ量

curlのラッパー

RAII : Resource Acquisition Is Initialization
RAII - Wikipedia

日本語では「リソースの確保は初期化時に」、「リソースの取得と初期化」など)は、資源(リソース)の確保と解放を、クラス型の変数の初期化と破棄処理に結び付けるというプログラミングのテクニックで、特にC++とD言語で一般的である。

本題ではないけど、RAIIを使って自分以外の人がcurlを使いたい場合に配慮してみた

class CURLRaii {
private:
    CURL* _curl;
    curl_slist* _headers; // curl_slist は ヘッダ用構造体。連結リスト

public:
    CURLRaii()
    : _curl(curl_easy_init())
    , _headers(nullptr)
    {
    }

    ~CURLRaii() {
        if (_curl != nullptr) {
            curl_easy_cleanup(_curl);
        }
        if (_headers != nullptr) {
            curl_slist_free_all(_headers);
        }
    }

    template <class T>
    bool setOption(CURLoption option, T data) {
        return CURLE_OK == curl_easy_setopt(_curl, option, data);
    }

    bool init(const std::string& url, const std::vector<std::string>& headers, void* stream, char* errorBuffer) {
        if (_curl == nullptr) {
            return false;
        }
        if (!configureCURL(errorBuffer)) {
            return false;
        }

        if(!headers.empty()) {
            for (const auto& elem : headers) {
                _headers = curl_slist_append(_headers, elem.c_str());
            }
            // HTTPヘッダフィールドの設定
            if (!setOption(CURLOPT_HTTPHEADER, _headers)) {
                return false;
            }
        }

        return true;
    }

    bool perform(long* responseCode) {
        if (CURLE_OK != curl_easy_perform(_curl)) {
            return false;
        }
        CURLcode code = curl_easy_getinfo(_curl, CURLINFO_RESPONSE_CODE, responseCode);
        if (code == CURLE_OK && *responseCode == 200) {
            return true;
        }
        return false;
    }

    bool configureCURL(char* errorBuffer) {
        //  〜 setOptionでいろいろ設定する 〜
        return true;
    }
};

使い方

Downloader::start(data);

この1行でサブスレッドで通信〜解凍を行うことができる。
3スレッド並列で実行したければ、3回呼び出せば良い。

その他

勉強会を見に来てくださった方が記事書いてくれていたので、ご紹介
【Mac】C++でcurllib使ってリクエストして、レスポンスをpicojsonでパースしてターミナルに表示する

26
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
12