グレンジ 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を追加するだけ
brewでinstallしてコンパイル時に-lcurl
でも可
brew install curl
curlの使い方サンプル
基本的には
-
curl/curl.h
のインクルード -
curl_easy_init
でcurlインスタンスの初期化 -
curl_easy_setopt
で必要なパラメータを設定 -
curl_easy_perform
で通信実行 -
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し解凍する
通信部分
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でパースしてターミナルに表示する