Open11

cprのHTTPクライアント機能を使ってみる

togetoge

libcurlのC++ラッパーであるcprを使って、HTTPクライアントを実装してみようと思います。
普段はcpp-httplibやBoost.Beastを使っているのですがもう一つの選択肢にできれば嬉しいです。

私が捉えている3つの特徴はそれぞれこんな感じです。

ライブラリ名 概要 HTTP/2 HTTP/3 非同期
cpp-httplib 導入が手軽で、使い方も簡単
Boost.Beast Boostの一部だけれどheader onlyなので導入は比較的楽。非同期対応が充実。
cpr libcurlに依存していて、何でもできる。cprもコンパイル必要なので導入が手間
togetoge

libcurlもcprも色々と依存ライブラリがあるので、conanパッケージでインストールします。
C++のパッケージマネージャー嫌いな人、vcpkgみたいな他のパッケージマネージャー使っている人ごめんなさい。

togetoge

conanのメインのパッケージリポジトリであるconan-center-indexにはcprのパッケージがあるので、これを使います。

conanfile.txt
[requires]
cpr/1.10.1

[options]
libcurl/*:with_nghttp2=True

[generators]
CMakeToolchain
CMakeDeps

cprだけrequireしているのに、optionsでlibcurlのオプションを指定しています。
libcurlがHTTP/2に対応していないと、cprもHTTP/2で通信できないためです。
…分かりづらいですね。

togetoge

conanfile.txtが存在する同じディレクトリで以下のコマンドを実行します。

conan install . -of build --build=missing
togetoge

同じディレクトリにCMakeLists.txtも作成します。
cprは0.10.0からC++17を必要としてきたのでこれも有効にします。

CMakeLists.txt
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の部分で表示が乱れるのでプレーンテキストにしています。

togetoge

まずは簡単なGETリクエストから。
一行でさくっと書けるのはいいですね。

test01.cpp
#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に対応していないのです。

今後に期待ですね。

togetoge

ちなみにHTTP/1.1に強制することもできます。
もちろんVERSION_1_0を指定すればHTTP/1.0にもできます。

test02.cpp
#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
togetoge

今まではレスポンスヘッダはraw_headerで確認してきましたが、headerを使えばstd::mapに保存された解析後の情報を確認できます。

test03.cpp
#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潔癖症なら、次のようにするんでしょうか??

test04.cpp
#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の構築が行われているみたいなので、ちょっと効率が気になります。
せめてパースをしないフラグとかで制御できるといいのですが、そういう実装にはなってないですね。

togetoge

cpr::Getですが、これは簡単な関数になっています。

api.h
template <typename... Ts>
Response Get(Ts&&... ts) {
    Session session;
    priv::set_option(session, std::forward<Ts>(ts)...);
    return session.Get();
}

実際にはcpr::Sessionに対する処理なのを綺麗に隠蔽してくれているんですね。
cpr::Sessionを自分で使うなら次のような感じになります。

test05.cpp
#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を使ってみようと思います。

togetoge

話はそれますが、cpr::Get()の実装、勉強になりました。
cpr::Urlcpr::HttpVersionなんかの薄いクラスと、それらのクラスを引数にとるcpr::Session::setOption()を用意しています。
これでcpr::Get()に順序を気にせず必要な制御用の情報を渡せるの、綺麗ですね。

session.h
    // 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);
togetoge

GETにパラメータを渡す場合にはcpr::Session::SetParameters()を利用します。
ここからのサンプルはHTTPリクエストの壁打ちができるサイト httpbin を利用しています。
cprのドキュメントで知ったのですが、このサイトはクライアントのテストをするのに便利ですね。
他のプログラミング言語の学習でも積極的に使っていきたい。

test06.cpp
#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"
}