cprのHTTPクライアント機能を使ってみる
libcurlのC++ラッパーであるcprを使って、HTTPクライアントを実装してみようと思います。
普段はcpp-httplibやBoost.Beastを使っているのですがもう一つの選択肢にできれば嬉しいです。
私が捉えている3つの特徴はそれぞれこんな感じです。
ライブラリ名 | 概要 | HTTP/2 | HTTP/3 | 非同期 |
---|---|---|---|---|
cpp-httplib | 導入が手軽で、使い方も簡単 | |||
Boost.Beast | Boostの一部だけれどheader onlyなので導入は比較的楽。非同期対応が充実。 | ◎ | ||
cpr | libcurlに依存していて、何でもできる。cprもコンパイル必要なので導入が手間 | ○ | ○ | ○ |
libcurlもcprも色々と依存ライブラリがあるので、conanパッケージでインストールします。
C++のパッケージマネージャー嫌いな人、vcpkgみたいな他のパッケージマネージャー使っている人ごめんなさい。
conanのメインのパッケージリポジトリであるconan-center-indexにはcprのパッケージがあるので、これを使います。
[requires]
cpr/1.10.1
[options]
libcurl/*:with_nghttp2=True
[generators]
CMakeToolchain
CMakeDeps
cprだけrequireしているのに、optionsでlibcurlのオプションを指定しています。
libcurlがHTTP/2に対応していないと、cprもHTTP/2で通信できないためです。
…分かりづらいですね。
conanfile.txtが存在する同じディレクトリで以下のコマンドを実行します。
conan install . -of build --build=missing
同じディレクトリにCMakeLists.txtも作成します。
cprは0.10.0からC++17を必要としてきたのでこれも有効にします。
cmake_minimum_required(VERSION 3.9)
project(study_ada LANGUAGES CXX)
include(CheckIPOSupported)
check_ipo_supported(RESULT ipo_supported OUTPUT error)
if(ipo_supported)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(ada REQUIRED CONFIG)
find_package(cpr REQUIRED CONFIG)
foreach(idx RANGE 1 100)
if(idx LESS 10)
set(idx "0${idx}")
endif()
if(EXISTS "${CMAKE_SOURCE_DIR}/test${idx}.cpp")
add_executable(test${idx} test${idx}.cpp)
target_link_libraries(test${idx} PRIVATE cpr::cpr)
target_compile_features(test${idx} PRIVATE cxx_std_17)
endif()
endforeach()
※cmakeのハイライトさせたいのですが、なんかcpr::cpr
の部分で表示が乱れるのでプレーンテキストにしています。
まずは簡単なGETリクエストから。
一行でさくっと書けるのはいいですね。
#include <iostream>
#include "cpr/cpr.h"
int main() {
auto const r = cpr::Get(cpr::Url{"https://www.google.com/"});
std::cout << r.status_code << "\n\n";
std::cout << r.raw_header << "\n\n";
std::cout << r.text.size() << '\n';
return 0;
}
200
HTTP/2 200
date: Wed, 10 May 2023 12:47:40 GMT
expires: -1
cache-control: private, max-age=0
content-type: text/html; charset=ISO-8859-1
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-SPf5gh1KHzAMLUP9kNqgNg' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
content-encoding: gzip
server: gws
content-length: 6869
x-xss-protection: 0
x-frame-options: SAMEORIGIN
set-cookie: 1P_JAR=2023-05-10-12; expires=Fri, 09-Jun-2023 12:47:40 GMT; path=/; domain=.google.com; Secure
set-cookie: AEC=AUEFqZdzgju945zm4W3b7wB0GK2OB39qYsJTUeL5lnJal9R8KZ0ecUui7sE; expires=Mon, 06-Nov-2023 12:47:40 GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax
set-cookie: NID=511=ERz4RUeUHRLDHwi3C1awpGfYMpOZ-hVqvzGCWJ5LDBbi-6j-yDTb4Sd0VMzW67CRDZ6mMYyFpaSb9FVpYD0DgaPITdHbSy15-HDSch4S2rst5PLl0TZNMm7-4qOJw5JGxNrvYkbM_l2ma3g30acZLaZDMXHBViMarrMET7nGfqY; expires=Thu, 09-Nov-2023 12:47:40 GMT; path=/; domain=.google.com; HttpOnly
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
15967
ちゃんとnghttp2が有効になったlibcurlを使っているのでHTTP/2で通信できていることが分かります。
libcurlもcprもHTTP/3に対応しているので、HTTP/3ライブラリが有効になっていればHTTP/3が使われたと思いますが、現時点でconan-center-indexのlibcurlはHTTP/3に対応していないのです。
今後に期待ですね。
ちなみにHTTP/1.1に強制することもできます。
もちろんVERSION_1_0
を指定すればHTTP/1.0にもできます。
#include <iostream>
#include "cpr/cpr.h"
int main() {
auto const r = cpr::Get(cpr::Url{"https://www.google.com/"}, cpr::HttpVersion{cpr::HttpVersionCode::VERSION_1_1});
std::cout << r.status_code << "\n\n";
std::cout << r.raw_header << "\n\n";
std::cout << r.text.size() << '\n';
return 0;
}
200
HTTP/1.1 200 OK
Date: Wed, 10 May 2023 12:55:22 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
Content-Security-Policy-Report-Only: object-src 'none';base-uri 'self';script-src 'nonce-pR1XPw27Uf-i2COasB6f-g' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info."
Content-Encoding: gzip
Server: gws
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
Set-Cookie: 1P_JAR=2023-05-10-12; expires=Fri, 09-Jun-2023 12:55:22 GMT; path=/; domain=.google.com; Secure
Set-Cookie: AEC=AUEFqZfKShcY3G1d2chWKbYyDcf6-eqBbUJmwkzYBBZXibHUB3hK9z7ewA; expires=Mon, 06-Nov-2023 12:55:22 GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax
Set-Cookie: NID=511=YsVjyvPFpNRAzVitocEFhTOMCiHPZqjmMo2KQDPodS-ark74WQIi1kykMgpToUnSjA5-xEG_vnDlw5mp_FtQTGWcuBOSjgz24EdChf0XW61cny4-OVTzTKpFx7OAX9hsIZfgX8C6xEOTv1T38YE7-U6vr0fVQU9nkwdomYvqmB8; expires=Thu, 09-Nov-2023 12:55:22 GMT; path=/; domain=.google.com; HttpOnly
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Transfer-Encoding: chunked
15969
今まではレスポンスヘッダはraw_header
で確認してきましたが、header
を使えばstd::map
に保存された解析後の情報を確認できます。
#include <iostream>
#include "cpr/cpr.h"
int main() {
auto r = cpr::Get(cpr::Url{"https://www.google.com/"});
std::cout << r.header["Content-Type"] << '\n';
return 0;
}
text/html; charset=ISO-8859-1
operator[]'を使っているので
rから
const`を外しています。
const潔癖症なら、次のようにするんでしょうか??
#include <iostream>
#include "cpr/cpr.h"
int main() {
auto const r = cpr::Get(cpr::Url{"https://www.google.com/"});
std::cout << r.header.at("Content-Type") << '\n';
return 0;
}
いずれにせよResponseを作るたびに強制的にレスポンスヘッダのパースとstd::mapの構築が行われているみたいなので、ちょっと効率が気になります。
せめてパースをしないフラグとかで制御できるといいのですが、そういう実装にはなってないですね。
cpr::Get
ですが、これは簡単な関数になっています。
template <typename... Ts>
Response Get(Ts&&... ts) {
Session session;
priv::set_option(session, std::forward<Ts>(ts)...);
return session.Get();
}
実際にはcpr::Session
に対する処理なのを綺麗に隠蔽してくれているんですね。
cpr::Session
を自分で使うなら次のような感じになります。
#include <iostream>
#include "cpr/cpr.h"
int main() {
auto session = cpr::Session{};
session.SetUrl("https://www.google.com/");
auto const response = session.Get();
std::cout << response.raw_header << '\n';
return 0;
}
KeepAliveとかどういう動作になるのか気になるので、これ以降はcpr::Session
を使ってみようと思います。
話はそれますが、cpr::Get()
の実装、勉強になりました。
cpr::Url
やcpr::HttpVersion
なんかの薄いクラスと、それらのクラスを引数にとるcpr::Session::setOption()
を用意しています。
これでcpr::Get()
に順序を気にせず必要な制御用の情報を渡せるの、綺麗ですね。
// Used in templated functions
void SetOption(const Url& url);
void SetOption(const Parameters& parameters);
void SetOption(Parameters&& parameters);
void SetOption(const Header& header);
void SetOption(const Timeout& timeout);
void SetOption(const ConnectTimeout& timeout);
void SetOption(const Authentication& auth);
GETにパラメータを渡す場合にはcpr::Session::SetParameters()
を利用します。
ここからのサンプルはHTTPリクエストの壁打ちができるサイト httpbin を利用しています。
cprのドキュメントで知ったのですが、このサイトはクライアントのテストをするのに便利ですね。
他のプログラミング言語の学習でも積極的に使っていきたい。
#include <iostream>
#include "cpr/cpr.h"
int main() {
auto session = cpr::Session{};
session.SetUrl("https://www.httpbin.org/get");
session.SetParameters({{"hello", "world"}});
auto const response = session.Get();
std::cout << response.elapsed << '\n';
std::cout << response.url << '\n';
std::cout << response.text << '\n';
return 0;
}
実行結果は次のようになります。
elapsed
はレスポンスを受信するまでのTATを秒単位で返してくれるみたいですね。
いたれりつくせりですが、別にいらない時もあるんだけどなぁ。
1.7994
https://www.httpbin.org/get?hello=world
{
"args": {
"hello": "world"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "deflate, gzip",
"Host": "www.httpbin.org",
"User-Agent": "curl/7.87.0",
"X-Amzn-Trace-Id": "Root=1-645d153d-5e37619e17e5df64076e732d"
},
"origin": "xx.xx.xx.xx",
"url": "https://www.httpbin.org/get?hello=world"
}