Add Cookbook other topics (draft)

This commit is contained in:
yhirose
2026-04-10 19:02:44 -04:00
parent 361b753f19
commit 529dafdee3
29 changed files with 2124 additions and 26 deletions

View File

@@ -0,0 +1,87 @@
---
title: "E01. SSEサーバーを実装する"
order: 47
status: "draft"
---
Server-Sent EventsSSEは、サーバーからクライアントへイベントを一方向にプッシュするためのシンプルなプロトコルです。長時間の接続を保ったまま、サーバーが好きなタイミングでデータを送れます。WebSocketより軽量で、HTTPの範囲で完結するのが魅力です。
cpp-httplibにはSSE専用のサーバーAPIはありませんが、`set_chunked_content_provider()``text/event-stream`を組み合わせれば実装できます。
## 基本のSSEサーバー
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[](size_t offset, httplib::DataSink &sink) {
std::string message = "data: hello\n\n";
sink.write(message.data(), message.size());
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
});
});
```
ポイントは3つです。
1. Content-Typeを`text/event-stream`にする
2. メッセージは`data: <内容>\n\n`の形式で書く(`\n\n`で1イベントの区切り
3. `sink.write()`で送るたびに、クライアントが受け取る
接続が生きている限り、プロバイダラムダが繰り返し呼ばれ続けます。
## イベントを送り続ける例
サーバーの現在時刻を1秒ごとに送るシンプルな例です。
```cpp
svr.Get("/time", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[&req](size_t offset, httplib::DataSink &sink) {
if (req.is_connection_closed()) {
sink.done();
return true;
}
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
std::string msg = "data: " + std::string(std::ctime(&t)) + "\n";
sink.write(msg.data(), msg.size());
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
});
});
```
クライアントが切断したら`sink.done()`で終了します。詳しくはS16. クライアントが切断したか検出するを参照してください。
## コメント行でハートビート
`:`で始まる行はSSEのコメントで、クライアントは無視しますが、**接続を生かしておく**役割があります。プロキシやロードバランサが無通信接続を切ってしまうのを防げます。
```cpp
// 30秒ごとにハートビート
if (tick_count % 30 == 0) {
std::string ping = ": ping\n\n";
sink.write(ping.data(), ping.size());
}
```
## スレッドプールとの関係
SSEは接続がつなぎっぱなしなので、1クライアントあたり1ワーカースレッドを消費します。同時接続数が多くなりそうなら、スレッドプールを動的スケーリングにしておきましょう。
```cpp
svr.new_task_queue = [] {
return new httplib::ThreadPool(8, 128);
};
```
詳しくはS21. マルチスレッド数を設定するを参照してください。
> **Note:** `data:`の後ろに改行が含まれる場合、各行の先頭に`data: `を付けて複数の`data:`行として送ります。SSEの仕様で決まっているフォーマットです。
> イベント名を使い分けたい場合はE02. SSEでイベント名を使い分けるを、クライアント側はE04. SSEをクライアントで受信するを参照してください。

View File

@@ -0,0 +1,84 @@
---
title: "E02. SSEでイベント名を使い分ける"
order: 48
status: "draft"
---
SSEでは、1本のストリームで複数の種類のイベントを送れます。`event:`フィールドで名前を付けると、クライアント側で種類ごとに別々のハンドラを呼べます。チャットの「新規メッセージ」「入室」「退室」のような場面で便利です。
## イベント名付きで送る
```cpp
auto send_event = [](httplib::DataSink &sink,
const std::string &event,
const std::string &data) {
std::string msg = "event: " + event + "\n"
+ "data: " + data + "\n\n";
sink.write(msg.data(), msg.size());
};
svr.Get("/chat/stream", [&](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[&, send_event](size_t offset, httplib::DataSink &sink) {
send_event(sink, "message", "Hello!");
std::this_thread::sleep_for(std::chrono::seconds(2));
send_event(sink, "join", "alice");
std::this_thread::sleep_for(std::chrono::seconds(2));
send_event(sink, "leave", "bob");
std::this_thread::sleep_for(std::chrono::seconds(2));
return true;
});
});
```
1メッセージは`event:``data:` → 空行、の形式です。`event:`を書かないと、クライアント側ではデフォルトの`"message"`イベントとして扱われます。
## IDを付けて再接続に備える
`id:`フィールドを一緒に送ると、クライアントが切断→再接続したときに`Last-Event-ID`ヘッダーで「どこまで受け取ったか」を教えてくれます。
```cpp
auto send_event = [](httplib::DataSink &sink,
const std::string &event,
const std::string &data,
const std::string &id) {
std::string msg = "id: " + id + "\n"
+ "event: " + event + "\n"
+ "data: " + data + "\n\n";
sink.write(msg.data(), msg.size());
};
send_event(sink, "message", "Hello!", "42");
```
IDの付け方は自由です。連番でもUUIDでも、サーバー側で重複せず順序が追えるものを選びましょう。再接続の詳細はE03. SSEの再接続を処理するを参照してください。
## JSONをdataに乗せる
構造化されたデータを送りたいときは、`data:`の中身をJSONにするのが定番です。
```cpp
nlohmann::json payload = {
{"user", "alice"},
{"text", "Hello!"},
};
send_event(sink, "message", payload.dump(), "42");
```
クライアント側では受け取った`data`をそのままJSONパースすれば、元のオブジェクトに戻せます。
## データに改行が含まれる場合
`data:`の値に改行が入るときは、各行の先頭に`data: `を付けて複数行に分けて送ります。
```cpp
std::string msg = "data: line1\n"
"data: line2\n"
"data: line3\n\n";
sink.write(msg.data(), msg.size());
```
クライアント側では、これらが改行でつながった1つの`data`として復元されます。
> **Note:** `event:`を使うとクライアント側のハンドリングがきれいになりますが、ブラウザのDevToolsで見たときに種類別で識別しやすくなるというメリットもあります。デバッグ時に効いてきます。

View File

@@ -0,0 +1,85 @@
---
title: "E03. SSEの再接続を処理する"
order: 49
status: "draft"
---
SSE接続はネットワークの都合で切れることがあります。クライアントは自動的に再接続を試みるので、サーバー側では「再接続してきたクライアントに、途中から配信を再開する」仕組みを用意しておくと親切です。
## `Last-Event-ID`を受け取る
クライアントが再接続すると、最後に受け取ったイベントのIDを`Last-Event-ID`ヘッダーに入れて送ってきます。サーバー側ではこれを読んで、その続きから配信を再開できます。
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
auto last_id = req.get_header_value("Last-Event-ID");
int start = last_id.empty() ? 0 : std::stoi(last_id) + 1;
res.set_chunked_content_provider(
"text/event-stream",
[start](size_t offset, httplib::DataSink &sink) mutable {
static int next_id = 0;
if (next_id < start) { next_id = start; }
std::string msg = "id: " + std::to_string(next_id) + "\n"
+ "data: event " + std::to_string(next_id) + "\n\n";
sink.write(msg.data(), msg.size());
++next_id;
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
});
});
```
初回接続では`Last-Event-ID`が無いので`0`から送り始め、再接続時は続きのIDから再開します。イベントの保存はサーバー側の責任なので、直近のイベントをキャッシュしておく必要があります。
## 再接続間隔を指定する
`retry:`フィールドを送ると、クライアント側の再接続間隔を指定できます。単位はミリ秒です。
```cpp
std::string msg = "retry: 5000\n\n"; // 5秒後に再接続
sink.write(msg.data(), msg.size());
```
通常は最初に1回送っておけば十分です。混雑時やサーバーメンテナンス時に、リトライ間隔を長めに指定して負荷を減らすといった使い方もできます。
## イベントのバッファリング
再接続のために、直近のイベントをサーバー側でバッファしておく実装が必要です。
```cpp
struct EventBuffer {
std::mutex mu;
std::deque<std::pair<int, std::string>> events; // {id, data}
int next_id = 0;
void push(const std::string &data) {
std::lock_guard<std::mutex> lock(mu);
events.push_back({next_id++, data});
if (events.size() > 1000) { events.pop_front(); }
}
std::vector<std::pair<int, std::string>> since(int id) {
std::lock_guard<std::mutex> lock(mu);
std::vector<std::pair<int, std::string>> out;
for (const auto &e : events) {
if (e.first >= id) { out.push_back(e); }
}
return out;
}
};
```
再接続してきたクライアントに`since(last_id)`で未送信分をまとめて送ると、取りこぼしを防げます。
## 保存期間のバランス
バッファをどれだけ持つかは、メモリと「どれだけさかのぼって再送できるか」のトレードオフです。用途によって決めましょう。
- リアルタイムチャット: 数分〜数十分
- 通知: 直近のN件
- 取引データ: 永続化して、必要ならDBから取得
> **Warning:** `Last-Event-ID`はクライアントが送ってくる値なので、サーバー側で信用しすぎないようにしましょう。数値として読むなら範囲チェックを、文字列ならサニタイズを忘れずに。

View File

@@ -0,0 +1,99 @@
---
title: "E04. SSEをクライアントで受信する"
order: 50
status: "draft"
---
cpp-httplibには`sse::SSEClient`という専用のクラスが用意されています。自動再接続、イベント名別のハンドラ、`Last-Event-ID`の管理まで面倒を見てくれるので、SSEを受信するときはこれを使うのが一番ラクです。
## 基本の使い方
```cpp
#include <httplib.h>
httplib::Client cli("http://localhost:8080");
httplib::sse::SSEClient sse(cli, "/events");
sse.on_message([](const httplib::sse::SSEMessage &msg) {
std::cout << "data: " << msg.data << std::endl;
});
sse.start(); // ブロッキング
```
`Client`と接続先パスを渡して`SSEClient`を作り、`on_message()`でコールバックを登録します。`start()`を呼ぶとイベントループが走り、接続が切れると自動で再接続を試みます。
## イベント名で分岐する
サーバー側で`event:`を付けて送られてくる場合は、`on_event()`で名前ごとにハンドラを登録できます。
```cpp
sse.on_event("message", [](const auto &msg) {
std::cout << "chat: " << msg.data << std::endl;
});
sse.on_event("join", [](const auto &msg) {
std::cout << msg.data << " joined" << std::endl;
});
sse.on_event("leave", [](const auto &msg) {
std::cout << msg.data << " left" << std::endl;
});
```
`on_message()`は、名前なし(デフォルトの`message`イベント)を受け取る汎用ハンドラとして使えます。
## 接続イベントとエラーハンドリング
```cpp
sse.on_open([] {
std::cout << "connected" << std::endl;
});
sse.on_error([](httplib::Error err) {
std::cerr << "error: " << httplib::to_string(err) << std::endl;
});
```
接続確立時やエラー発生時にもフックを挟めます。エラーハンドラが呼ばれても、`SSEClient`は内部で再接続を試みます。
## 非同期で動かす
メインスレッドを塞ぎたくない場合は`start_async()`を使います。
```cpp
sse.start_async();
// メインスレッドは別の仕事を続ける
do_other_work();
// 終わったら止める
sse.stop();
```
`start_async()`は裏でスレッドを立ち上げてイベントループを回します。`stop()`でクリーンに止められます。
## 再接続の設定
再接続間隔や最大試行回数を調整できます。
```cpp
sse.set_reconnect_interval(5000); // 5秒
sse.set_max_reconnect_attempts(10); // 10回まで0=無制限)
```
サーバー側で`retry:`フィールドを送っていると、そちらが優先されます。
## Last-Event-IDの自動管理
`SSEClient`は受信したイベントの`id`を内部で保持していて、再接続時に`Last-Event-ID`ヘッダーとして送ってくれます。この挙動はサーバー側で`id:`付きイベントを送っていれば自動で有効になります。
```cpp
std::cout << "last id: " << sse.last_event_id() << std::endl;
```
現在のIDは`last_event_id()`で参照できます。
> **Note:** SSEClientの`start()`はブロッキングなので、単発のツールならそのまま使えますが、GUIアプリやサーバーに組み込むときは`start_async()` + `stop()`の組み合わせが基本です。
> サーバー側の実装はE01. SSEサーバーを実装するを参照してください。

View File

@@ -75,22 +75,22 @@ status: "draft"
## TLS / セキュリティ
- T01. OpenSSL・mbedTLS・wolfSSLの選択指針(ビルド時の`#define`の違い)
- T02. SSL証明書の検証を制御する無効化・カスタムCA・カスタムコールバック
- T03. SSL/TLSサーバーを立ち上げる(証明書・秘密鍵の設定)
- T04. mTLS(クライアント証明書による相互認証)を設定する
- T05. サーバー側でピア証明書を参照する`req.peer_cert()` / SNI
- [T01. OpenSSL・mbedTLS・wolfSSLの選択指針](t01-tls-backends)
- [T02. SSL証明書の検証を制御する](t02-cert-verification)
- [T03. SSL/TLSサーバーを立ち上げる](t03-ssl-server)
- [T04. mTLSを設定する](t04-mtls)
- [T05. サーバー側でピア証明書を参照する](t05-peer-cert)
## SSE
- E01. SSEサーバーを実装する
- E02. SSEでイベント名を使い分ける
- E03. SSEの再接続を処理する`Last-Event-ID`
- E04. SSEをクライアントで受信する
- [E01. SSEサーバーを実装する](e01-sse-server)
- [E02. SSEでイベント名を使い分ける](e02-sse-event-names)
- [E03. SSEの再接続を処理する](e03-sse-reconnect)
- [E04. SSEをクライアントで受信する](e04-sse-client)
## WebSocket
- W01. WebSocketエコーサーバークライアントを実装する
- W02. ハートビートを設定する`set_websocket_ping_interval`
- W03. 接続クローズをハンドリングする
- W04. バイナリフレームを送受信する
- [W01. WebSocketエコーサーバークライアントを実装する](w01-websocket-echo)
- [W02. ハートビートを設定する](w02-websocket-ping)
- [W03. 接続クローズをハンドリングする](w03-websocket-close)
- [W04. バイナリフレームを送受信する](w04-websocket-binary)

View File

@@ -0,0 +1,64 @@
---
title: "S22. Unix domain socketで通信する"
order: 41
status: "draft"
---
ネットワーク経由ではなく、同じホスト内のプロセスとだけ通信したいときは、Unix domain socketを使えます。TCPのオーバーヘッドがなく、ファイルシステムのパーミッションで簡単にアクセス制御できるので、ローカルのIPCや、リバースプロキシの背後に置くサービスでよく使われます。
## サーバー側
```cpp
httplib::Server svr;
svr.set_address_family(AF_UNIX);
svr.Get("/", [](const auto &, auto &res) {
res.set_content("hello from unix socket", "text/plain");
});
svr.listen("/tmp/httplib.sock", 80);
```
`set_address_family(AF_UNIX)`を呼んでから、`listen()`の第1引数にソケットファイルのパスを渡します。第2引数のポート番号は使われませんが、シグネチャの都合で何か渡す必要があります。
## クライアント側
```cpp
httplib::Client cli("/tmp/httplib.sock");
cli.set_address_family(AF_UNIX);
auto res = cli.Get("/");
if (res) {
std::cout << res->body << std::endl;
}
```
`Client`のコンストラクタにソケットファイルのパスを渡し、`set_address_family(AF_UNIX)`を呼ぶだけです。あとは通常のHTTPリクエストと同じように書けます。
## 使いどころ
- **リバースプロキシとの連携**: nginxがUnix socket経由でバックエンドを呼ぶ構成は、TCPより高速で、ポート管理も不要です
- **ローカル専用API**: 外部からアクセスしないツール間通信
- **コンテナ内IPC**: 同じPodやコンテナ内でのプロセス間通信
- **開発環境**: ポート競合を気にしなくていい
## ソケットファイルの後始末
Unix domain socketはファイルシステム上にファイルを作ります。サーバー終了時に自動では消えないので、必要なら起動前に削除しておきましょう。
```cpp
std::remove("/tmp/httplib.sock");
svr.listen("/tmp/httplib.sock", 80);
```
## パーミッション
ソケットファイルのパーミッションで、どのユーザーからアクセスできるかをコントロールできます。
```cpp
svr.listen("/tmp/httplib.sock", 80);
// 別プロセスや別スレッドで
chmod("/tmp/httplib.sock", 0660); // オーナーとグループのみ
```
> **Warning:** Windowsでも一部バージョンでAF_UNIXがサポートされていますが、実装や挙動がプラットフォームによって違います。クロスプラットフォームで動かす場合は、十分にテストしてから本番に投入してください。

View File

@@ -0,0 +1,49 @@
---
title: "T01. OpenSSL・mbedTLS・wolfSSLの選択指針"
order: 42
status: "draft"
---
cpp-httplibはTLSの実装を自前では持たず、3つのバックエンドの中から1つを選んで使います。ビルド時に有効にするマクロで切り替えます。
| バックエンド | マクロ | 特徴 |
| --- | --- | --- |
| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | 最も広く使われている、機能が豊富 |
| mbedTLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | 軽量、組み込み向け |
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | 組み込み向け、商用サポートあり |
## ビルド時の指定
`httplib.h`をインクルードする前に、使いたいバックエンドのマクロを定義します。
```cpp
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <httplib.h>
```
リンク時にそのバックエンドのライブラリ(`libssl``libcrypto``libmbedtls``libwolfssl`など)も必要です。
## どれを選ぶか
**迷ったらOpenSSL**
機能が一番豊富で、情報も多いです。通常のサーバー用途やLinuxデスクトップ向けなら、まずこれで始めて問題ありません。
**バイナリサイズを抑えたい、組み込みで使う**
mbedTLSかwolfSSLが向いています。OpenSSLよりずっとコンパクトで、メモリ制約のあるデバイスでも動きます。
**商用サポートが必要**
wolfSSLには商用ライセンスとサポートがあります。製品に組み込むなら選択肢に入ります。
## 複数バックエンドを切り替えたい場合
ビルドバリアントとして切り分けて、同じソースをマクロ切替でコンパイルし直すのが一般的です。APIの違いは、cpp-httplib側でかなり吸収してくれますが、完全に同じ挙動ではないのでテストは必須です。
## どのバックエンドでも使えるAPI
証明書の検証制御、SSLServerの立ち上げ、ピア証明書の取得などは、どのバックエンドでも同じAPIで呼べます。
- T02. SSL証明書の検証を制御する
- T03. SSL/TLSサーバーを立ち上げる
- T05. サーバー側でピア証明書を参照する
> **Note:** macOSでは、OpenSSL系のバックエンドを使う場合、システムのキーチェーンからルート証明書を自動で読む設定`CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`)がデフォルトで有効です。無効にしたい場合は`CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`を定義してください。

View File

@@ -0,0 +1,53 @@
---
title: "T02. SSL証明書の検証を制御する"
order: 43
status: "draft"
---
HTTPSクライアントは、デフォルトでサーバー証明書を検証します。OSのルート証明書ストアを使って、証明書チェーンの有効性とホスト名の一致を確認します。この挙動を変えたいときに使うAPIを紹介します。
## 独自のCA証明書を指定する
社内認証局CAで署名された証明書を使うサーバーに接続するときは、`set_ca_cert_path()`でCA証明書を指定します。
```cpp
httplib::Client cli("https://internal.example.com");
cli.set_ca_cert_path("/etc/ssl/certs/internal-ca.pem");
auto res = cli.Get("/");
```
第1引数がCA証明書ファイル、第2引数がCA証明書ディレクトリ省略可です。OpenSSLバックエンドなら、`set_ca_cert_store()``X509_STORE*`を直接渡すこともできます。
## 証明書検証を無効にする(非推奨)
開発用のサーバーや自己署名証明書にアクセスしたいときは、検証を無効にできます。
```cpp
httplib::Client cli("https://self-signed.example.com");
cli.enable_server_certificate_verification(false);
auto res = cli.Get("/");
```
これだけで、証明書チェーンの検証がスキップされます。
> **Warning:** 証明書検証を無効にすると、中間者攻撃MITMを防げなくなります。本番環境では**絶対に使わない**でください。開発やテスト以外で無効化する必要が出たら、「もう一度やり方を間違えていないか確認する」という癖をつけましょう。
## ホスト名検証だけを無効にする
証明書チェーンは検証したいけれど、ホスト名の一致だけスキップしたい、という中間的な設定もあります。証明書のCN/SANとリクエスト先のホスト名が食い違うサーバーにアクセスするときに使います。
```cpp
cli.enable_server_hostname_verification(false);
```
証明書そのものは有効かどうか検証するので、「検証完全無効」よりは少し安全です。ただ、これも本番ではおすすめしません。
## OSの証明書ストアをそのまま使う
多くのLinuxディストリビューションでは、`/etc/ssl/certs/ca-certificates.crt`などにルート証明書がまとまっています。cpp-httplibは起動時にOSのデフォルトストアを自動で読みにいくので、普通のサーバーならとくに設定不要です。
> mbedTLSやwolfSSLバックエンドでも同じAPIが使えます。バックエンドの選び方はT01. OpenSSL・mbedTLS・wolfSSLの選択指針を参照してください。
> 失敗したときの詳細を調べる方法はC18. SSLエラーをハンドリングするを参照してください。

View File

@@ -0,0 +1,78 @@
---
title: "T03. SSL/TLSサーバーを立ち上げる"
order: 44
status: "draft"
---
HTTPSサーバーを立ち上げるには、`httplib::Server`の代わりに`httplib::SSLServer`を使います。サーバー証明書と秘密鍵をコンストラクタに渡せば、あとは`Server`とまったく同じように使えます。
## 基本の使い方
```cpp
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <httplib.h>
int main() {
httplib::SSLServer svr("cert.pem", "key.pem");
svr.Get("/", [](const auto &req, auto &res) {
res.set_content("hello over TLS", "text/plain");
});
svr.listen("0.0.0.0", 443);
}
```
コンストラクタにサーバー証明書PEM形式と秘密鍵のファイルパスを渡します。これだけでTLS対応のサーバーが立ちます。ハンドラの登録も`listen()`の呼び方も、通常の`Server`と同じです。
## 秘密鍵がパスワード保護されている場合
第5引数に秘密鍵のパスワードを渡せます。
```cpp
httplib::SSLServer svr("cert.pem", "key.pem",
nullptr, nullptr, "password");
```
第3、第4引数はクライアント証明書検証用mTLS、T04参照なので、今は`nullptr`を指定します。
## メモリ上のPEMから立ち上げる
ファイルではなくメモリ上のPEMデータから起動したいときは、`PemMemory`構造体を使います。
```cpp
httplib::SSLServer::PemMemory pem{};
pem.cert_pem = cert_data.data();
pem.cert_pem_len = cert_data.size();
pem.key_pem = key_data.data();
pem.key_pem_len = key_data.size();
httplib::SSLServer svr(pem);
```
環境変数やシークレットマネージャから証明書を取得する場合に便利です。
## 証明書の更新
証明書の有効期限が切れる前に、サーバーを再起動せずに新しい証明書に差し替えたいことがあります。`update_certs_pem()`が使えます。
```cpp
svr.update_certs_pem(new_cert_pem, new_key_pem);
```
既存の接続はそのまま、これから確立する接続は新しい証明書で動きます。
## 証明書の準備
テスト用の自己署名証明書は、OpenSSLのコマンドで作れます。
```sh
openssl req -x509 -newkey rsa:2048 -days 365 -nodes \
-keyout key.pem -out cert.pem -subj "/CN=localhost"
```
本番では、Let's Encryptや社内CAから発行された証明書を使いましょう。
> **Warning:** HTTPSサーバーを443番ポートで立ち上げるにはroot権限が必要です。安全に立ち上げる方法はS18. `listen_after_bind`で起動順序を制御するの「特権降格」を参照してください。
> クライアント証明書による相互認証mTLSはT04. mTLSを設定するを参照してください。

View File

@@ -0,0 +1,70 @@
---
title: "T04. mTLSを設定する"
order: 45
status: "draft"
---
通常のTLSはサーバー証明書だけを検証しますが、**mTLS**mutual TLS、相互TLSではクライアントも証明書を提示し、サーバーがそれを検証します。API間通信のゼロトラスト化や、社内システムの認証でよく使われるパターンです。
## サーバー側の設定
`SSLServer`のコンストラクタ第3、第4引数に、クライアント証明書を検証するためのCA証明書を渡します。
```cpp
httplib::SSLServer svr(
"server-cert.pem", // サーバー証明書
"server-key.pem", // サーバー秘密鍵
"client-ca.pem", // クライアント証明書を検証するCA
nullptr // CAディレクトリ省略
);
svr.Get("/", [](const httplib::Request &req, httplib::Response &res) {
res.set_content("authenticated", "text/plain");
});
svr.listen("0.0.0.0", 443);
```
この設定だと、クライアント証明書が`client-ca.pem`で署名されていない接続はハンドシェイクの段階で拒否されます。ハンドラまで到達した時点で、クライアントはすでに認証済みです。
## メモリ上のPEMで設定する
```cpp
httplib::SSLServer::PemMemory pem{};
pem.cert_pem = server_cert.data();
pem.cert_pem_len = server_cert.size();
pem.key_pem = server_key.data();
pem.key_pem_len = server_key.size();
pem.client_ca_pem = client_ca.data();
pem.client_ca_pem_len = client_ca.size();
httplib::SSLServer svr(pem);
```
環境変数やシークレットマネージャから読み込む場合はこちらが便利です。
## クライアント側の設定
クライアント側では、`SSLClient`のコンストラクタにクライアント証明書と秘密鍵を渡します。
```cpp
httplib::SSLClient cli("api.example.com", 443,
"client-cert.pem",
"client-key.pem");
auto res = cli.Get("/");
```
`Client`ではなく`SSLClient`を直接使う点に注意してください。秘密鍵にパスワードがある場合は第5引数で渡せます。
## ハンドラからクライアント情報を取得する
ハンドラの中で、どのクライアントが接続してきたかを確認したいときは`req.peer_cert()`を使います。詳しくはT05. サーバー側でピア証明書を参照するを参照してください。
## 用途
- **マイクロサービス間通信**: サービスごとに証明書を発行して、証明書で認証する
- **IoTデバイスの管理**: デバイスに証明書を焼き込み、APIへのアクセス制御に使う
- **社内VPNの代替**: 公開されているエンドポイントに証明書認証をかけて、社内リソースへ安全にアクセスさせる
> **Note:** クライアント証明書の発行と失効管理は、普通のパスワード認証より運用コストが高いです。内部PKIを回すか、ACMELet's Encryptなど系のツールで自動化する体制が必要です。

View File

@@ -0,0 +1,88 @@
---
title: "T05. サーバー側でピア証明書を参照する"
order: 46
status: "draft"
---
mTLS構成では、接続してきたクライアントの証明書をハンドラの中で読めます。証明書のCNCommon NameやSANSubject Alternative Nameを取り出して、ユーザーを特定したり、ログに残したりできます。
## 基本の使い方
```cpp
svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
auto cert = req.peer_cert();
if (!cert) {
res.status = 401;
res.set_content("no client certificate", "text/plain");
return;
}
auto cn = cert.subject_cn();
res.set_content("hello, " + cn, "text/plain");
});
```
`req.peer_cert()``tls::PeerCert`オブジェクトを返します。`bool`に変換できるので、まずは証明書の有無を確認してから使います。
## 取り出せる情報
`PeerCert`からは、以下の情報を取得できます。
```cpp
auto cert = req.peer_cert();
std::string cn = cert.subject_cn(); // CN
std::string issuer = cert.issuer_name(); // 発行者
std::string serial = cert.serial(); // シリアル番号
time_t not_before, not_after;
cert.validity(not_before, not_after); // 有効期間
auto sans = cert.sans(); // SAN一覧
for (const auto &san : sans) {
std::cout << san.value << std::endl;
}
```
ホスト名がSANに含まれるかを確認するヘルパーもあります。
```cpp
if (cert.check_hostname("alice.corp.example.com")) {
// 一致
}
```
## 証明書ベースの認可
CNやSANを使って、ルート単位でアクセス制御できます。
```cpp
svr.set_pre_request_handler(
[](const httplib::Request &req, httplib::Response &res) {
auto cert = req.peer_cert();
if (!cert) {
res.status = 401;
return httplib::Server::HandlerResponse::Handled;
}
if (req.matched_route.rfind("/admin", 0) == 0) {
auto cn = cert.subject_cn();
if (!is_admin_cn(cn)) {
res.status = 403;
return httplib::Server::HandlerResponse::Handled;
}
}
return httplib::Server::HandlerResponse::Unhandled;
});
```
Pre-requestハンドラと組み合わせれば、共通の認可ロジックを一箇所にまとめられます。詳しくはS11. Pre-request handlerでルート単位の認証を行うを参照してください。
## SNIServer Name Indication
クライアントが指定してきたサーバー名は、cpp-httplibが自動で処理します。同じサーバーで複数のドメインをホストする場合にSNIが使われますが、通常はハンドラ側で意識する必要はありません。
> **Warning:** `req.peer_cert()`は、mTLSが有効で、かつクライアントが証明書を提示した場合のみ有効な値を返します。通常のTLS接続では空の`PeerCert`が返ります。使う前に必ず`bool`チェックしてください。
> mTLSの設定方法はT04. mTLSを設定するを参照してください。

View File

@@ -0,0 +1,88 @@
---
title: "W01. WebSocketエコーサーバークライアントを実装する"
order: 51
status: "draft"
---
WebSocketは、クライアントとサーバーの間で**双方向**にメッセージをやり取りするためのプロトコルです。cpp-httplibはサーバーとクライアントの両方のAPIを提供しています。まずは一番シンプルなエコーサーバーから見てみましょう。
## サーバー: エコーサーバー
```cpp
#include <httplib.h>
int main() {
httplib::Server svr;
svr.WebSocket("/echo", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
std::string msg;
while (ws.is_open()) {
auto result = ws.read(msg);
if (result == httplib::ws::ReadResult::Fail) {
break;
}
ws.send(msg); // 受け取った内容をそのまま返す
}
});
svr.listen("0.0.0.0", 8080);
}
```
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
`read()`の返り値は`ReadResult`列挙値で、次の3種類です。
- `ReadResult::Text`: テキストメッセージを受信
- `ReadResult::Binary`: バイナリメッセージを受信
- `ReadResult::Fail`: エラー、または接続が閉じた
## クライアント: エコーを叩く
```cpp
#include <httplib.h>
int main() {
httplib::ws::WebSocketClient cli("ws://localhost:8080/echo");
if (!cli.connect()) {
std::cerr << "failed to connect" << std::endl;
return 1;
}
cli.send("Hello, WebSocket!");
std::string msg;
if (cli.read(msg) != httplib::ws::ReadResult::Fail) {
std::cout << "received: " << msg << std::endl;
}
cli.close();
}
```
URLには`ws://`(平文)または`wss://`TLSを指定します。`connect()`でハンドシェイクを行い、あとは`send()``read()`でサーバーと同じAPIでやり取りできます。
## テキストとバイナリの送り分け
`send()`には2つのオーバーロードがあり、テキストとバイナリで使い分けられます。
```cpp
ws.send("Hello"); // テキストフレーム
ws.send(binary_data, binary_data_size); // バイナリフレーム
```
`std::string`を受け取るオーバーロードはテキスト、`const char*`とサイズを受け取るオーバーロードはバイナリとして送られます。詳しくはW04. バイナリフレームを送受信するを参照してください。
## スレッドとの関係
WebSocket接続はハンドラが終わるまで生き続けるので、1接続につきワーカースレッドを1つ占有します。同時接続数が多い場合は、スレッドプールを動的スケーリングに設定しましょう。
```cpp
svr.new_task_queue = [] {
return new httplib::ThreadPool(8, 128);
};
```
詳細はS21. マルチスレッド数を設定するを参照してください。
> **Note:** HTTPSサーバーの上でWebSocketを動かしたいときは、`httplib::Server`の代わりに`httplib::SSLServer`を使えば、同じ`WebSocket()`ハンドラがそのまま動きます。クライアント側は`wss://`スキームを指定するだけです。

View File

@@ -0,0 +1,60 @@
---
title: "W02. ハートビートを設定する"
order: 52
status: "draft"
---
WebSocket接続は長時間つなぎっぱなしになるので、プロキシやロードバランサが「アイドルだから」と勝手に切ってしまうことがあります。これを防ぐために、定期的にPingフレームを送って接続を生かしておく仕組みがあります。cpp-httplibでは、指定した間隔で自動的にPingを送ってくれます。
## サーバー側の設定
```cpp
svr.set_websocket_ping_interval(30); // 30秒ごとにPing
svr.WebSocket("/chat", [](const auto &req, auto &ws) {
// ...
});
```
`set_websocket_ping_interval()`に秒数を渡すだけです。このサーバーが受け入れるすべてのWebSocket接続に対して、指定した間隔でPingが送られます。
`std::chrono`の期間を受け取るオーバーロードもあります。
```cpp
using namespace std::chrono_literals;
svr.set_websocket_ping_interval(30s);
```
## クライアント側の設定
クライアント側でも同じAPIがあります。
```cpp
httplib::ws::WebSocketClient cli("ws://localhost:8080/chat");
cli.set_websocket_ping_interval(30);
cli.connect();
```
`connect()`を呼ぶ前に設定しておきましょう。
## デフォルト値
デフォルトのPing間隔は、ビルド時のマクロ`CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`で決まります。通常はそのままで問題ありませんが、特別なプロキシ環境に合わせて短くしたい場合は調整してください。
## PongはどうやってpIngに応答するか
WebSocketプロトコルでは、PingフレームにはPongフレームで応答することが決まっています。cpp-httplibは受信したPingに自動でPongを返すので、アプリケーションコード側で気にする必要はありません。
## Pingの間隔をどう決めるか
| 環境 | 推奨 |
| --- | --- |
| 通常のインターネット接続 | 30〜60秒 |
| 厳しいプロキシAWS ALBなど | 15〜30秒 |
| モバイル回線 | 短すぎるとバッテリーを食う、60秒以上 |
短すぎると無駄なトラフィックになり、長すぎると接続が切れます。だいたい**接続が切れる時間の半分**くらいが目安です。
> **Warning:** Ping間隔を極端に短くすると、WebSocket接続ごとにバックグラウンドでスレッドが走るので、CPU負荷が上がります。接続数が多いサーバーでは控えめな値に設定しましょう。
> 接続が閉じたときの処理はW03. 接続クローズをハンドリングするを参照してください。

View File

@@ -0,0 +1,91 @@
---
title: "W03. 接続クローズをハンドリングする"
order: 53
status: "draft"
---
WebSocket接続は、クライアントかサーバーのどちらかが明示的に閉じるか、ネットワーク障害で切れると終了します。クローズ処理をきちんと書いておくと、リソースの後始末や再接続ロジックがきれいに書けます。
## クローズ状態の検出
`ws.read()``ReadResult::Fail`を返したら、接続が切れたか何らかのエラーが起きたということです。ループを抜けてハンドラから戻れば、そのWebSocket接続の処理は終わります。
```cpp
svr.WebSocket("/chat", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
std::string msg;
while (ws.is_open()) {
auto result = ws.read(msg);
if (result == httplib::ws::ReadResult::Fail) {
std::cout << "disconnected" << std::endl;
break;
}
handle_message(ws, msg);
}
// ここに到達したら後始末
cleanup_user_session(req);
});
```
`ws.is_open()`でも接続状態を確認できます。内部的には同じことを見ています。
## サーバー側から閉じる
サーバー側から明示的にクローズしたいときは、`close()`を呼びます。
```cpp
ws.close(httplib::ws::CloseStatus::Normal, "bye");
```
第1引数にクローズステータス、第2引数に理由任意を渡します。クローズステータスは`CloseStatus`列挙値で、代表的なものはこちらです。
| 値 | 意味 |
| --- | --- |
| `Normal` (1000) | 通常終了 |
| `GoingAway` (1001) | サーバーが終了するため |
| `ProtocolError` (1002) | プロトコル違反を検知 |
| `UnsupportedData` (1003) | 対応していないデータを受信 |
| `PolicyViolation` (1008) | ポリシー違反 |
| `MessageTooBig` (1009) | メッセージが大きすぎる |
| `InternalError` (1011) | サーバー内部エラー |
## クライアント側から閉じる
クライアント側でも同じAPIが使えます。
```cpp
cli.close(httplib::ws::CloseStatus::Normal);
```
`cli`を破棄したときにも自動的にクローズされますが、明示的に`close()`を呼んだほうが意図が伝わりやすいです。
## グレースフルシャットダウン
サーバーを停止するときに接続中のクライアントに「これから止まります」と伝えたい場合は、`GoingAway`を使います。
```cpp
ws.close(httplib::ws::CloseStatus::GoingAway, "server restarting");
```
クライアント側はこのステータスを見て、再接続を試みるかどうかを判断できます。
## サンプル: 簡単なチャット終了
```cpp
svr.WebSocket("/chat", [](const auto &req, auto &ws) {
std::string msg;
while (ws.is_open()) {
if (ws.read(msg) == httplib::ws::ReadResult::Fail) break;
if (msg == "/quit") {
ws.send("goodbye");
ws.close(httplib::ws::CloseStatus::Normal, "user quit");
break;
}
ws.send("echo: " + msg);
}
});
```
> **Note:** ネットワーク障害で突然切断された場合、`close()`を呼ぶ暇もなく`read()`が`Fail`を返します。後始末はハンドラ終了時にまとめて行うようにしておくと、どちらのパターンでも対応できます。

View File

@@ -0,0 +1,85 @@
---
title: "W04. バイナリフレームを送受信する"
order: 54
status: "draft"
---
WebSocketにはテキストフレームとバイナリフレームの2種類があります。JSONやプレーンテキストならテキスト、画像や独自プロトコルの生データならバイナリ、という使い分けです。cpp-httplibの`send()`は、オーバーロードで両者を自動的に切り替えます。
## 送り分けの仕組み
```cpp
ws.send(std::string("Hello")); // テキスト
ws.send("Hello", 5); // バイナリ
ws.send(binary_data, binary_data_size); // バイナリ
```
`std::string`を受け取るオーバーロードは**テキスト**、`const char*`とサイズを受け取るオーバーロードは**バイナリ**です。ちょっと紛らわしいですが、覚えてしまえば直感的です。
文字列をバイナリとして送りたい場合は、`.data()``.size()`を明示的に渡します。
```cpp
std::string raw = build_binary_payload();
ws.send(raw.data(), raw.size()); // バイナリフレーム
```
## 受信時の判別
`ws.read()`の返り値で、受信したフレームがテキストかバイナリかを判別できます。
```cpp
std::string msg;
auto result = ws.read(msg);
switch (result) {
case httplib::ws::ReadResult::Text:
std::cout << "text: " << msg << std::endl;
break;
case httplib::ws::ReadResult::Binary:
std::cout << "binary: " << msg.size() << " bytes" << std::endl;
handle_binary(msg.data(), msg.size());
break;
case httplib::ws::ReadResult::Fail:
// エラーまたは切断
break;
}
```
バイナリフレームも`std::string`に入って渡されますが、中身はバイト列なので注意してください。`msg.data()``msg.size()`で生のバイトとして扱えます。
## バイナリを使うべき場面
- **画像・動画・音声**: Base64でエンコードせずにそのまま送れるので、オーバーヘッドがない
- **独自プロトコル**: protobufやMessagePackなどの構造化バイナリフォーマット
- **ゲームのネットワーク通信**: 低レイテンシが求められる場合
- **センサーデータのストリーミング**: 数値列をそのまま送る
## Pingもバイナリフレームの一種
WebSocketのPing/PongフレームもOpcodeレベルではバイナリに近い扱いですが、cpp-httplibが自動で処理するので、アプリケーションコードで意識する必要はありません。W02. ハートビートを設定するを参照してください。
## サンプル: 画像を送る
```cpp
// サーバー側: 画像を送りつける
svr.WebSocket("/image", [](const auto &req, auto &ws) {
auto img = read_image_file("logo.png");
ws.send(img.data(), img.size());
});
```
```cpp
// クライアント側: 受け取ってファイルに保存
httplib::ws::WebSocketClient cli("ws://localhost:8080/image");
cli.connect();
std::string buf;
if (cli.read(buf) == httplib::ws::ReadResult::Binary) {
std::ofstream ofs("received.png", std::ios::binary);
ofs.write(buf.data(), buf.size());
}
```
テキストとバイナリを混ぜて送ることもできます。たとえば「制御メッセージはJSON、データ本体はバイナリ」といったプロトコルを組み立てると、メタデータと生データを効率よく扱えます。
> **Note:** WebSocketのフレームサイズには上限がないわけではありません。巨大なデータを送るときは、アプリケーション側で分割して送るのが安全です。cpp-httplibのデフォルトでは大きなフレームもそのまま処理されますが、メモリを一気に使う点は変わりません。