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

[グレンジ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/grenge) 12日目担当の mad_khaki です。
クライアントサイドを中心にサーバ / データ分析 等もやってます。

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

サポーターズでもよく発表しています
[【サポーターズCoLab勉強会】C++とcurlでHTTP通信処理作ってみた ](|https://supporterzcolab.com/event/595/)

# 環境

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

# curlの導入

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

![lib.png](https://qiita-image-store.s3.amazonaws.com/0/216890/43afcb65-d9be-5851-3de6-ba15d748af61.png)

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

```bash
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に書き出すサンプル

```cpp
#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し解凍する
<img width="462" alt="graph.png" src="https://qiita-image-store.s3.amazonaws.com/0/216890/e67e21b5-8bb1-29b3-2aa9-0d993f301d16.png">


# 通信部分

```cpp
	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なら以下のオプションを使う

```cpp
	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](https://ja.wikipedia.org/wiki/RAII)

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

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

```cpp
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でパースしてターミナルに表示する](https://qiita.com/yumechi/items/7c1fbe3a443108dc07cb)