diff --git a/docs-src/pages/en/cookbook/e01-sse-server.md b/docs-src/pages/en/cookbook/e01-sse-server.md new file mode 100644 index 0000000..2184fb6 --- /dev/null +++ b/docs-src/pages/en/cookbook/e01-sse-server.md @@ -0,0 +1,87 @@ +--- +title: "E01. Implement an SSE Server" +order: 47 +status: "draft" +--- + +Server-Sent Events (SSE) is a simple protocol for pushing events one-way from server to client. The connection stays open, and the server can send data whenever it wants. It's lighter than WebSocket and fits entirely within HTTP — a nice combination. + +cpp-httplib doesn't have a dedicated SSE server API, but you can implement one with `set_chunked_content_provider()` and `text/event-stream`. + +## Basic SSE server + +```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; + }); +}); +``` + +Three things matter here: + +1. Content-Type is `text/event-stream` +2. Messages follow the format `data: \n\n` (the double newline separates events) +3. Each `sink.write()` delivers data to the client + +The provider lambda keeps being called as long as the connection is alive. + +## A continuous stream + +Here's a simple example that sends the current time once per second. + +```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; + }); +}); +``` + +When the client disconnects, call `sink.done()` to stop. Details in S16. Detect When the Client Has Disconnected. + +## Heartbeats via comment lines + +Lines starting with `:` are SSE comments — clients ignore them, but they **keep the connection alive**. Handy for preventing proxies and load balancers from closing idle connections. + +```cpp +// heartbeat every 30 seconds +if (tick_count % 30 == 0) { + std::string ping = ": ping\n\n"; + sink.write(ping.data(), ping.size()); +} +``` + +## Relationship with the thread pool + +SSE connections stay open, so each client holds a worker thread. For lots of concurrent connections, enable dynamic scaling on the thread pool. + +```cpp +svr.new_task_queue = [] { + return new httplib::ThreadPool(8, 128); +}; +``` + +See S21. Configure the Thread Pool. + +> **Note:** When `data:` contains newlines, split it into multiple `data:` lines — one per line. This is how the SSE spec requires multiline data to be transmitted. + +> For event names, see E02. Use Named Events in SSE. For the client side, see E04. Receive SSE on the Client. diff --git a/docs-src/pages/en/cookbook/e02-sse-event-names.md b/docs-src/pages/en/cookbook/e02-sse-event-names.md new file mode 100644 index 0000000..922efdf --- /dev/null +++ b/docs-src/pages/en/cookbook/e02-sse-event-names.md @@ -0,0 +1,84 @@ +--- +title: "E02. Use Named Events in SSE" +order: 48 +status: "draft" +--- + +SSE lets you send multiple kinds of events over the same stream. Give each one a name with the `event:` field, and the client can dispatch to a different handler per type. Great for things like "new message", "user joined", "user left" in a chat app. + +## Send events with names + +```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; + }); +}); +``` + +A message is `event:` → `data:` → blank line. If you omit `event:`, the client treats it as a default `"message"` event. + +## Attach IDs for reconnect + +When you include an `id:` field, the client automatically sends it back as `Last-Event-ID` on reconnect, telling the server "here's how far I got." + +```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"); +``` + +The ID format is up to you. Monotonic counters or UUIDs both work — just pick something unique and orderable on the server side. See E03. Handle SSE Reconnection for details. + +## JSON payloads in data + +For structured data, the usual move is to put JSON in `data:`. + +```cpp +nlohmann::json payload = { + {"user", "alice"}, + {"text", "Hello!"}, +}; +send_event(sink, "message", payload.dump(), "42"); +``` + +On the client, parse the incoming `data` as JSON to get the original object back. + +## Data with newlines + +If the data value contains newlines, split it across multiple `data:` lines. + +```cpp +std::string msg = "data: line1\n" + "data: line2\n" + "data: line3\n\n"; +sink.write(msg.data(), msg.size()); +``` + +On the client side, these come back as a single `data` string with newlines. + +> **Note:** Using `event:` makes client-side dispatch cleaner, but it also helps in the browser DevTools — events are easier to filter by type. That matters more than you'd expect while debugging. diff --git a/docs-src/pages/en/cookbook/e03-sse-reconnect.md b/docs-src/pages/en/cookbook/e03-sse-reconnect.md new file mode 100644 index 0000000..79f2d46 --- /dev/null +++ b/docs-src/pages/en/cookbook/e03-sse-reconnect.md @@ -0,0 +1,85 @@ +--- +title: "E03. Handle SSE Reconnection" +order: 49 +status: "draft" +--- + +SSE connections drop for all sorts of network reasons. Clients automatically try to reconnect, so it's a good idea to make your server resume from where it left off. + +## Read `Last-Event-ID` + +When the client reconnects, it sends the ID of the last event it received in the `Last-Event-ID` header. The server reads that and picks up from the next one. + +```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; + }); +}); +``` + +On the first connect, `Last-Event-ID` is empty, so start from `0`. On reconnect, resume from the next ID. Event history is the server's responsibility — you need to keep recent events around somewhere. + +## Set the reconnect interval + +Sending a `retry:` field tells the client how long to wait before reconnecting, in milliseconds. + +```cpp +std::string msg = "retry: 5000\n\n"; // reconnect after 5 seconds +sink.write(msg.data(), msg.size()); +``` + +Usually you send this once at the start. During peak load or maintenance windows, a longer retry interval helps reduce reconnect storms. + +## Buffer recent events + +To support reconnection, keep a rolling buffer of recent events on the server. + +```cpp +struct EventBuffer { + std::mutex mu; + std::deque> events; // {id, data} + int next_id = 0; + + void push(const std::string &data) { + std::lock_guard lock(mu); + events.push_back({next_id++, data}); + if (events.size() > 1000) { events.pop_front(); } + } + + std::vector> since(int id) { + std::lock_guard lock(mu); + std::vector> out; + for (const auto &e : events) { + if (e.first >= id) { out.push_back(e); } + } + return out; + } +}; +``` + +When a client reconnects, call `since(last_id)` to send any events it missed. + +## How much to keep + +The buffer size is a tradeoff between memory and how far back a client can resume. It depends on the use case: + +- Real-time chat: a few minutes to half an hour +- Notifications: the last N items +- Trading data: persist to a database and pull from there + +> **Warning:** `Last-Event-ID` is a client-provided value — don't trust it blindly. If you read it as a number, validate the range. If it's a string, sanitize it. diff --git a/docs-src/pages/en/cookbook/e04-sse-client.md b/docs-src/pages/en/cookbook/e04-sse-client.md new file mode 100644 index 0000000..5e53c00 --- /dev/null +++ b/docs-src/pages/en/cookbook/e04-sse-client.md @@ -0,0 +1,99 @@ +--- +title: "E04. Receive SSE on the Client" +order: 50 +status: "draft" +--- + +cpp-httplib ships a dedicated `sse::SSEClient` class. It handles auto-reconnect, per-event-name dispatch, and `Last-Event-ID` tracking for you — so receiving SSE is painless. + +## Basic usage + +```cpp +#include + +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(); // blocking +``` + +Build an `SSEClient` with a `Client` and a path, register a callback with `on_message()`, and call `start()`. The event loop kicks in and automatically reconnects if the connection drops. + +## Dispatch by event name + +When the server sends events with an `event:` field, register a handler per name via `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()` serves as a generic fallback for unnamed events (the default `message` type). + +## Connection lifecycle and errors + +```cpp +sse.on_open([] { + std::cout << "connected" << std::endl; +}); + +sse.on_error([](httplib::Error err) { + std::cerr << "error: " << httplib::to_string(err) << std::endl; +}); +``` + +Hook into connection open and error events. Even when the error handler fires, `SSEClient` keeps trying to reconnect in the background. + +## Run asynchronously + +If you don't want to block the main thread, use `start_async()`. + +```cpp +sse.start_async(); + +// main thread continues to do other things +do_other_work(); + +// when you're done, stop it +sse.stop(); +``` + +`start_async()` spawns a background thread to run the event loop. Use `stop()` to shut it down cleanly. + +## Configure reconnection + +You can tune the reconnect interval and maximum retries. + +```cpp +sse.set_reconnect_interval(5000); // 5 seconds +sse.set_max_reconnect_attempts(10); // up to 10 (0 = unlimited) +``` + +If the server sends a `retry:` field, that takes precedence. + +## Automatic Last-Event-ID + +`SSEClient` tracks the `id` of each received event internally and sends it back as `Last-Event-ID` on reconnect. As long as the server sends events with `id:`, this all works automatically. + +```cpp +std::cout << "last id: " << sse.last_event_id() << std::endl; +``` + +Use `last_event_id()` to read the current value. + +> **Note:** `SSEClient::start()` blocks, which is fine for a one-off command-line tool. For GUI apps or embedded in a server, the `start_async()` + `stop()` pair is the usual pattern. + +> For the server side, see E01. Implement an SSE Server. diff --git a/docs-src/pages/en/cookbook/index.md b/docs-src/pages/en/cookbook/index.md index 99dc526..fd58c4c 100644 --- a/docs-src/pages/en/cookbook/index.md +++ b/docs-src/pages/en/cookbook/index.md @@ -75,22 +75,22 @@ A collection of recipes that answer "How do I...?" questions. Each recipe is sel ## TLS / Security -- T01. Choosing between OpenSSL, mbedTLS, and wolfSSL (build-time `#define` differences) -- T02. Control SSL certificate verification (disable, custom CA, custom callback) -- T03. Set up an SSL/TLS server (certificate and private key) -- T04. Configure mTLS (mutual TLS with client certificates) -- T05. Access the peer certificate on the server (`req.peer_cert()` / SNI) +- [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](t01-tls-backends) +- [T02. Control SSL certificate verification](t02-cert-verification) +- [T03. Start an SSL/TLS server](t03-ssl-server) +- [T04. Configure mTLS](t04-mtls) +- [T05. Access the peer certificate on the server](t05-peer-cert) ## SSE -- E01. Implement an SSE server -- E02. Use event names to distinguish event types -- E03. Handle reconnection (`Last-Event-ID`) -- E04. Receive SSE events on the client +- [E01. Implement an SSE server](e01-sse-server) +- [E02. Use named events in SSE](e02-sse-event-names) +- [E03. Handle SSE reconnection](e03-sse-reconnect) +- [E04. Receive SSE on the client](e04-sse-client) ## WebSocket -- W01. Implement a WebSocket echo server and client -- W02. Configure heartbeats (`set_websocket_ping_interval`) -- W03. Handle connection close -- W04. Send and receive binary frames +- [W01. Implement a WebSocket echo server and client](w01-websocket-echo) +- [W02. Set a WebSocket heartbeat](w02-websocket-ping) +- [W03. Handle connection close](w03-websocket-close) +- [W04. Send and receive binary frames](w04-websocket-binary) diff --git a/docs-src/pages/en/cookbook/t01-tls-backends.md b/docs-src/pages/en/cookbook/t01-tls-backends.md new file mode 100644 index 0000000..c3c6c38 --- /dev/null +++ b/docs-src/pages/en/cookbook/t01-tls-backends.md @@ -0,0 +1,49 @@ +--- +title: "T01. Choosing Between OpenSSL, mbedTLS, and wolfSSL" +order: 42 +status: "draft" +--- + +cpp-httplib doesn't ship its own TLS implementation — it uses one of three backends that you pick at build time via a macro. + +| Backend | Macro | Character | +| --- | --- | --- | +| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | Most widely used, richest feature set | +| mbedTLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | Lightweight, aimed at embedded | +| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | Embedded-friendly, commercial support available | + +## Build-time selection + +Define the macro for your chosen backend before including `httplib.h`: + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include +``` + +You'll also need to link against the backend's libraries (`libssl`, `libcrypto`, `libmbedtls`, `libwolfssl`, etc.). + +## Which to pick + +**When in doubt, OpenSSL** +It has the most features and the best documentation. For normal server use or Linux desktop apps, start here — you probably won't need anything else. + +**To shrink binary size or target embedded** +mbedTLS or wolfSSL are a better fit. They're far more compact than OpenSSL and run on memory-constrained devices. + +**When you need commercial support** +wolfSSL offers commercial licensing and support. If you're shipping in a product, it's worth considering. + +## Supporting multiple backends + +The usual approach is to treat each backend as a build variant and recompile the same source with different macros. cpp-httplib smooths over most of the API differences, but the backends are not 100% identical — always test. + +## APIs that work across all backends + +Certificate verification control, standing up an SSLServer, reading the peer certificate — these all share the same API across backends: + +- T02. Control SSL Certificate Verification +- T03. Start an SSL/TLS Server +- T05. Access the Peer Certificate on the Server Side + +> **Note:** On macOS with an OpenSSL-family backend, cpp-httplib automatically loads root certificates from the system keychain (via `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`, on by default). To disable this, define `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`. diff --git a/docs-src/pages/en/cookbook/t02-cert-verification.md b/docs-src/pages/en/cookbook/t02-cert-verification.md new file mode 100644 index 0000000..f89c44a --- /dev/null +++ b/docs-src/pages/en/cookbook/t02-cert-verification.md @@ -0,0 +1,53 @@ +--- +title: "T02. Control SSL Certificate Verification" +order: 43 +status: "draft" +--- + +By default, an HTTPS client verifies the server certificate — it uses the OS root certificate store to check the chain and the hostname. Here are the APIs for changing that behavior. + +## Specify a custom CA certificate + +When connecting to a server whose certificate is signed by an internal CA, use `set_ca_cert_path()`. + +```cpp +httplib::Client cli("https://internal.example.com"); +cli.set_ca_cert_path("/etc/ssl/certs/internal-ca.pem"); + +auto res = cli.Get("/"); +``` + +The first argument is the CA certificate file; the second is an optional CA directory. With the OpenSSL backend, you can also pass an `X509_STORE*` directly via `set_ca_cert_store()`. + +## Disable certificate verification (not recommended) + +For development servers or self-signed certificates, you can skip verification entirely. + +```cpp +httplib::Client cli("https://self-signed.example.com"); +cli.enable_server_certificate_verification(false); + +auto res = cli.Get("/"); +``` + +That's all it takes to disable chain verification. + +> **Warning:** Disabling certificate verification removes protection against man-in-the-middle attacks. **Never do this in production.** If you find yourself needing it outside of dev/test, pause and make sure you're not doing something wrong. + +## Disable hostname verification only + +There's an in-between option: verify the certificate chain, but skip the hostname check. Useful when you need to reach a server whose cert CN/SAN doesn't match the request's hostname. + +```cpp +cli.enable_server_hostname_verification(false); +``` + +The certificate itself is still validated, so this is safer than fully disabling verification — but still not recommended in production. + +## Use the OS cert store as-is + +On most Linux distributions, root certificates live in a single file like `/etc/ssl/certs/ca-certificates.crt`. cpp-httplib reads the OS default store at startup, so for most servers you don't need to configure anything. + +> The same APIs work on the mbedTLS and wolfSSL backends. For choosing between backends, see T01. Choosing Between OpenSSL, mbedTLS, and wolfSSL. + +> For details on diagnosing failures, see C18. Handle SSL Errors. diff --git a/docs-src/pages/en/cookbook/t03-ssl-server.md b/docs-src/pages/en/cookbook/t03-ssl-server.md new file mode 100644 index 0000000..442f532 --- /dev/null +++ b/docs-src/pages/en/cookbook/t03-ssl-server.md @@ -0,0 +1,78 @@ +--- +title: "T03. Start an SSL/TLS Server" +order: 44 +status: "draft" +--- + +To stand up an HTTPS server, use `httplib::SSLServer` instead of `httplib::Server`. Pass a certificate and private key to the constructor, and you get back something that works exactly like `Server`. + +## Basic usage + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include + +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); +} +``` + +Pass the server certificate (PEM format) and private key file paths to the constructor. That's all you need for a TLS-enabled server. Registering handlers and calling `listen()` work the same as with `Server`. + +## Password-protected private keys + +The fifth argument is the private key password. + +```cpp +httplib::SSLServer svr("cert.pem", "key.pem", + nullptr, nullptr, "password"); +``` + +The third and fourth arguments are for client certificate verification (mTLS, see T04). For now, pass `nullptr`. + +## Load PEM data from memory + +When you want to load certs from memory instead of files, use the `PemMemory` struct. + +```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); +``` + +Handy when you pull certificates from environment variables or a secrets manager. + +## Rotate certificates + +Before a certificate expires, you may want to swap it out without restarting the server. That's what `update_certs_pem()` is for. + +```cpp +svr.update_certs_pem(new_cert_pem, new_key_pem); +``` + +Existing connections keep using the old cert; new connections use the new one. + +## Generating a test certificate + +For a throwaway self-signed cert, use the `openssl` CLI. + +```sh +openssl req -x509 -newkey rsa:2048 -days 365 -nodes \ + -keyout key.pem -out cert.pem -subj "/CN=localhost" +``` + +In production, use certificates from Let's Encrypt or your internal CA. + +> **Warning:** Binding an HTTPS server to port 443 requires root. For a safe way to do that, see the privilege-drop pattern in S18. Control Startup Order with `listen_after_bind`. + +> For mutual TLS (client certificates), see T04. Configure mTLS. diff --git a/docs-src/pages/en/cookbook/t04-mtls.md b/docs-src/pages/en/cookbook/t04-mtls.md new file mode 100644 index 0000000..697c72b --- /dev/null +++ b/docs-src/pages/en/cookbook/t04-mtls.md @@ -0,0 +1,70 @@ +--- +title: "T04. Configure mTLS" +order: 45 +status: "draft" +--- + +Regular TLS verifies the server certificate only. **mTLS** (mutual TLS) adds the other direction: the client presents a certificate too, and the server verifies it. It's common for zero-trust API-to-API traffic and internal system authentication. + +## Server side + +Pass the CA used to verify client certificates as the third (and fourth) argument to `SSLServer`. + +```cpp +httplib::SSLServer svr( + "server-cert.pem", // server certificate + "server-key.pem", // server private key + "client-ca.pem", // CA that signs valid client certs + nullptr // CA directory (none) +); + +svr.Get("/", [](const httplib::Request &req, httplib::Response &res) { + res.set_content("authenticated", "text/plain"); +}); + +svr.listen("0.0.0.0", 443); +``` + +With this, any connection whose client certificate isn't signed by `client-ca.pem` is rejected at the handshake. By the time a handler runs, the client is already authenticated. + +## Configure with in-memory 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); +``` + +This is the clean way when you load certificates from environment variables or a secrets manager. + +## Client side + +On the client side, pass the client certificate and key to `SSLClient`. + +```cpp +httplib::SSLClient cli("api.example.com", 443, + "client-cert.pem", + "client-key.pem"); + +auto res = cli.Get("/"); +``` + +Note you're using `SSLClient` directly, not `Client`. If the private key has a password, pass it as the fifth argument. + +## Read client info from a handler + +To see which client connected from inside a handler, use `req.peer_cert()`. Details in T05. Access the Peer Certificate on the Server Side. + +## Use cases + +- **Microservice-to-microservice calls**: Issue a cert per service, use the cert as identity +- **IoT device management**: Burn a cert into each device and use it to gate API access +- **An alternative to internal VPN**: Put cert-based auth in front of public endpoints so internal resources can be reached safely + +> **Note:** Issuing and revoking client certificates is more operational work than password-based auth. You'll need either an internal PKI setup or an automated flow using ACME-family tools. diff --git a/docs-src/pages/en/cookbook/t05-peer-cert.md b/docs-src/pages/en/cookbook/t05-peer-cert.md new file mode 100644 index 0000000..695580e --- /dev/null +++ b/docs-src/pages/en/cookbook/t05-peer-cert.md @@ -0,0 +1,88 @@ +--- +title: "T05. Access the Peer Certificate on the Server Side" +order: 46 +status: "draft" +--- + +In an mTLS setup, you can read the client's certificate from inside a handler. Pull out the CN or SAN to identify the user or log the request. + +## Basic usage + +```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()` returns a `tls::PeerCert`. It's convertible to `bool`, so check whether a cert is present before using it. + +## Available fields + +From a `PeerCert`, you can get: + +```cpp +auto cert = req.peer_cert(); + +std::string cn = cert.subject_cn(); // CN +std::string issuer = cert.issuer_name(); // issuer +std::string serial = cert.serial(); // serial number + +time_t not_before, not_after; +cert.validity(not_before, not_after); // validity period + +auto sans = cert.sans(); // SANs +for (const auto &san : sans) { + std::cout << san.value << std::endl; +} +``` + +There's also a helper to check if a hostname is covered by the SAN list: + +```cpp +if (cert.check_hostname("alice.corp.example.com")) { + // matches +} +``` + +## Cert-based authorization + +You can gate routes by CN or 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; + }); +``` + +Combined with a pre-request handler, you can keep all authorization logic in one place. See S11. Authenticate Per Route with a Pre-Request Handler. + +## SNI (Server Name Indication) + +cpp-httplib handles SNI automatically. If one server hosts multiple domains, SNI is used under the hood — but normally handlers don't need to care. + +> **Warning:** `req.peer_cert()` only returns a meaningful value when mTLS is enabled and the client actually presented a certificate. For plain TLS, you get an empty `PeerCert`. Always do the `bool` check before using it. + +> To set up mTLS, see T04. Configure mTLS. diff --git a/docs-src/pages/en/cookbook/w01-websocket-echo.md b/docs-src/pages/en/cookbook/w01-websocket-echo.md new file mode 100644 index 0000000..9c2141a --- /dev/null +++ b/docs-src/pages/en/cookbook/w01-websocket-echo.md @@ -0,0 +1,88 @@ +--- +title: "W01. Implement a WebSocket Echo Server and Client" +order: 51 +status: "draft" +--- + +WebSocket is a protocol for **two-way** messaging between client and server. cpp-httplib provides APIs for both sides. Let's start with the simplest example: an echo server. + +## Server: echo server + +```cpp +#include + +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); // echo back what we received + } + }); + + svr.listen("0.0.0.0", 8080); +} +``` + +Register a WebSocket handler with `svr.WebSocket()`. By the time the handler runs, the WebSocket handshake is already complete. Inside the loop, just `ws.read()` and `ws.send()` to get a working echo. + +The `read()` return value is a `ReadResult` enum: + +- `ReadResult::Text`: received a text message +- `ReadResult::Binary`: received a binary message +- `ReadResult::Fail`: error, or connection closed + +## Client: talk to the echo server + +```cpp +#include + +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(); +} +``` + +Use a `ws://` (plain) or `wss://` (TLS) URL. Call `connect()` to do the handshake, then `send()` and `read()` work the same as on the server side. + +## Text vs. binary + +`send()` has two overloads that let you choose the frame type. + +```cpp +ws.send("Hello"); // text frame +ws.send(binary_data, binary_data_size); // binary frame +``` + +The `std::string` overload sends as **text**; the `const char*` + size overload sends as **binary**. A bit subtle, but once you know it, it's intuitive. See W04. Send and Receive Binary Frames for details. + +## Thread pool implications + +A WebSocket handler holds its worker thread for the entire life of the connection — one connection per thread. For many concurrent clients, configure a dynamic thread pool. + +```cpp +svr.new_task_queue = [] { + return new httplib::ThreadPool(8, 128); +}; +``` + +See S21. Configure the Thread Pool. + +> **Note:** To run WebSocket over HTTPS, use `httplib::SSLServer` instead of `httplib::Server` — the same `WebSocket()` handler just works. On the client side, use a `wss://` URL. diff --git a/docs-src/pages/en/cookbook/w02-websocket-ping.md b/docs-src/pages/en/cookbook/w02-websocket-ping.md new file mode 100644 index 0000000..6b8c260 --- /dev/null +++ b/docs-src/pages/en/cookbook/w02-websocket-ping.md @@ -0,0 +1,60 @@ +--- +title: "W02. Set a WebSocket Heartbeat" +order: 52 +status: "draft" +--- + +WebSocket connections stay open for a long time, and proxies or load balancers will sometimes drop them for being "idle." To prevent that, you periodically send Ping frames to keep the connection alive. cpp-httplib can do this for you automatically. + +## Server side + +```cpp +svr.set_websocket_ping_interval(30); // ping every 30 seconds + +svr.WebSocket("/chat", [](const auto &req, auto &ws) { + // ... +}); +``` + +Just pass the interval in seconds. Every WebSocket connection this server accepts will be pinged on that interval. + +There's a `std::chrono` overload too. + +```cpp +using namespace std::chrono_literals; +svr.set_websocket_ping_interval(30s); +``` + +## Client side + +The client has the same API. + +```cpp +httplib::ws::WebSocketClient cli("ws://localhost:8080/chat"); +cli.set_websocket_ping_interval(30); +cli.connect(); +``` + +Call it before `connect()`. + +## The default + +The default interval is set by the build-time macro `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`. Usually you won't need to change it, but adjust downward if you're dealing with an aggressive proxy. + +## What about Pong? + +The WebSocket protocol requires that Ping frames are answered with Pong frames. cpp-httplib responds to Pings automatically — you don't need to think about it in application code. + +## Picking an interval + +| Environment | Suggested | +| --- | --- | +| Normal internet | 30–60s | +| Strict proxies (e.g. AWS ALB) | 15–30s | +| Mobile networks | 60s+ (too short drains battery) | + +Too short wastes bandwidth; too long and connections get dropped. As a rule of thumb, target about **half the idle timeout** of whatever's between you and the client. + +> **Warning:** A very short ping interval spawns background work per connection and increases CPU usage. For servers with many connections, keep the interval modest. + +> For handling a closed connection, see W03. Handle Connection Close. diff --git a/docs-src/pages/en/cookbook/w03-websocket-close.md b/docs-src/pages/en/cookbook/w03-websocket-close.md new file mode 100644 index 0000000..0fef2c5 --- /dev/null +++ b/docs-src/pages/en/cookbook/w03-websocket-close.md @@ -0,0 +1,91 @@ +--- +title: "W03. Handle Connection Close" +order: 53 +status: "draft" +--- + +A WebSocket ends when either side closes it explicitly, or when the network drops. Handle close cleanly, and your cleanup and reconnect logic stays tidy. + +## Detect a closed connection + +When `ws.read()` returns `ReadResult::Fail`, the connection is gone — either cleanly or with an error. Break out of the loop and the handler will finish. + +```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 runs once we're out of the loop + cleanup_user_session(req); +}); +``` + +You can also check `ws.is_open()` — it's the same signal from a different angle. + +## Close from the server side + +To close explicitly, call `close()`. + +```cpp +ws.close(httplib::ws::CloseStatus::Normal, "bye"); +``` + +The first argument is the close status; the second is an optional reason. Common `CloseStatus` values: + +| Value | Meaning | +| --- | --- | +| `Normal` (1000) | Normal closure | +| `GoingAway` (1001) | Server is shutting down | +| `ProtocolError` (1002) | Protocol violation detected | +| `UnsupportedData` (1003) | Received data that can't be handled | +| `PolicyViolation` (1008) | Violated a policy | +| `MessageTooBig` (1009) | Message too large | +| `InternalError` (1011) | Server-side error | + +## Close from the client side + +The client API is identical. + +```cpp +cli.close(httplib::ws::CloseStatus::Normal); +``` + +Destroying the client also closes the connection, but calling `close()` explicitly makes the intent clearer. + +## Graceful shutdown + +To notify in-flight clients that the server is going down, use `GoingAway`. + +```cpp +ws.close(httplib::ws::CloseStatus::GoingAway, "server restarting"); +``` + +The client can inspect that status and decide whether to reconnect. + +## Example: a tiny chat with quit + +```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:** On a sudden network drop, `read()` returns `Fail` with no chance to call `close()`. Put your cleanup at the end of the handler, and both paths — clean close and abrupt disconnect — end up in the same place. diff --git a/docs-src/pages/en/cookbook/w04-websocket-binary.md b/docs-src/pages/en/cookbook/w04-websocket-binary.md new file mode 100644 index 0000000..c8c979c --- /dev/null +++ b/docs-src/pages/en/cookbook/w04-websocket-binary.md @@ -0,0 +1,85 @@ +--- +title: "W04. Send and Receive Binary Frames" +order: 54 +status: "draft" +--- + +WebSocket has two frame types: text and binary. JSON and plain text go in text frames; images and raw protocol bytes go in binary. In cpp-httplib, `send()` picks the right type via overload. + +## How to pick a frame type + +```cpp +ws.send(std::string("Hello")); // text +ws.send("Hello", 5); // binary +ws.send(binary_data, binary_data_size); // binary +``` + +The `std::string` overload sends as **text**. The `const char*` + size overload sends as **binary**. A bit subtle, but once you know it, it sticks. + +If you have a `std::string` and want to send it as binary, pass `.data()` and `.size()` explicitly. + +```cpp +std::string raw = build_binary_payload(); +ws.send(raw.data(), raw.size()); // binary frame +``` + +## Detect frame type on receive + +The return value of `ws.read()` tells you whether the received frame was text or binary. + +```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: + // error or closed + break; +} +``` + +Binary frames still come back in a `std::string`, but treat its contents as raw bytes — use `msg.data()` and `msg.size()`. + +## When binary is the right call + +- **Images, video, audio**: No Base64 overhead +- **Custom protocols**: protobuf, MessagePack, or any structured binary format +- **Game networking**: When latency matters +- **Sensor data streams**: Push numeric arrays directly + +## Ping is binary-ish, but hidden + +WebSocket Ping/Pong frames are close cousins of binary frames at the opcode level, but cpp-httplib handles them automatically — you don't touch them. See W02. Set a WebSocket Heartbeat. + +## Example: send an image + +```cpp +// Server: push an image +svr.WebSocket("/image", [](const auto &req, auto &ws) { + auto img = read_image_file("logo.png"); + ws.send(img.data(), img.size()); +}); +``` + +```cpp +// Client: receive and save +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()); +} +``` + +You can mix text and binary in the same connection. A common pattern: JSON for control messages, binary for the actual data — you get efficient handling of metadata and payload both. + +> **Note:** WebSocket frames don't have an infinite size limit. For very large data, chunk it in your application code. cpp-httplib can handle a big frame in one shot, but it does load it all into memory at once. diff --git a/docs-src/pages/ja/cookbook/e01-sse-server.md b/docs-src/pages/ja/cookbook/e01-sse-server.md new file mode 100644 index 0000000..a961697 --- /dev/null +++ b/docs-src/pages/ja/cookbook/e01-sse-server.md @@ -0,0 +1,87 @@ +--- +title: "E01. SSEサーバーを実装する" +order: 47 +status: "draft" +--- + +Server-Sent Events(SSE)は、サーバーからクライアントへイベントを一方向にプッシュするためのシンプルなプロトコルです。長時間の接続を保ったまま、サーバーが好きなタイミングでデータを送れます。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をクライアントで受信するを参照してください。 diff --git a/docs-src/pages/ja/cookbook/e02-sse-event-names.md b/docs-src/pages/ja/cookbook/e02-sse-event-names.md new file mode 100644 index 0000000..26d49b6 --- /dev/null +++ b/docs-src/pages/ja/cookbook/e02-sse-event-names.md @@ -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で見たときに種類別で識別しやすくなるというメリットもあります。デバッグ時に効いてきます。 diff --git a/docs-src/pages/ja/cookbook/e03-sse-reconnect.md b/docs-src/pages/ja/cookbook/e03-sse-reconnect.md new file mode 100644 index 0000000..466fa21 --- /dev/null +++ b/docs-src/pages/ja/cookbook/e03-sse-reconnect.md @@ -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> events; // {id, data} + int next_id = 0; + + void push(const std::string &data) { + std::lock_guard lock(mu); + events.push_back({next_id++, data}); + if (events.size() > 1000) { events.pop_front(); } + } + + std::vector> since(int id) { + std::lock_guard lock(mu); + std::vector> 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`はクライアントが送ってくる値なので、サーバー側で信用しすぎないようにしましょう。数値として読むなら範囲チェックを、文字列ならサニタイズを忘れずに。 diff --git a/docs-src/pages/ja/cookbook/e04-sse-client.md b/docs-src/pages/ja/cookbook/e04-sse-client.md new file mode 100644 index 0000000..03c7568 --- /dev/null +++ b/docs-src/pages/ja/cookbook/e04-sse-client.md @@ -0,0 +1,99 @@ +--- +title: "E04. SSEをクライアントで受信する" +order: 50 +status: "draft" +--- + +cpp-httplibには`sse::SSEClient`という専用のクラスが用意されています。自動再接続、イベント名別のハンドラ、`Last-Event-ID`の管理まで面倒を見てくれるので、SSEを受信するときはこれを使うのが一番ラクです。 + +## 基本の使い方 + +```cpp +#include + +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サーバーを実装するを参照してください。 diff --git a/docs-src/pages/ja/cookbook/index.md b/docs-src/pages/ja/cookbook/index.md index 3dd36bc..01ff253 100644 --- a/docs-src/pages/ja/cookbook/index.md +++ b/docs-src/pages/ja/cookbook/index.md @@ -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) diff --git a/docs-src/pages/ja/cookbook/s22-unix-socket.md b/docs-src/pages/ja/cookbook/s22-unix-socket.md new file mode 100644 index 0000000..998c1ed --- /dev/null +++ b/docs-src/pages/ja/cookbook/s22-unix-socket.md @@ -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がサポートされていますが、実装や挙動がプラットフォームによって違います。クロスプラットフォームで動かす場合は、十分にテストしてから本番に投入してください。 diff --git a/docs-src/pages/ja/cookbook/t01-tls-backends.md b/docs-src/pages/ja/cookbook/t01-tls-backends.md new file mode 100644 index 0000000..46df6ad --- /dev/null +++ b/docs-src/pages/ja/cookbook/t01-tls-backends.md @@ -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 +``` + +リンク時にそのバックエンドのライブラリ(`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`を定義してください。 diff --git a/docs-src/pages/ja/cookbook/t02-cert-verification.md b/docs-src/pages/ja/cookbook/t02-cert-verification.md new file mode 100644 index 0000000..9f07213 --- /dev/null +++ b/docs-src/pages/ja/cookbook/t02-cert-verification.md @@ -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エラーをハンドリングするを参照してください。 diff --git a/docs-src/pages/ja/cookbook/t03-ssl-server.md b/docs-src/pages/ja/cookbook/t03-ssl-server.md new file mode 100644 index 0000000..7346588 --- /dev/null +++ b/docs-src/pages/ja/cookbook/t03-ssl-server.md @@ -0,0 +1,78 @@ +--- +title: "T03. SSL/TLSサーバーを立ち上げる" +order: 44 +status: "draft" +--- + +HTTPSサーバーを立ち上げるには、`httplib::Server`の代わりに`httplib::SSLServer`を使います。サーバー証明書と秘密鍵をコンストラクタに渡せば、あとは`Server`とまったく同じように使えます。 + +## 基本の使い方 + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include + +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を設定するを参照してください。 diff --git a/docs-src/pages/ja/cookbook/t04-mtls.md b/docs-src/pages/ja/cookbook/t04-mtls.md new file mode 100644 index 0000000..b018efb --- /dev/null +++ b/docs-src/pages/ja/cookbook/t04-mtls.md @@ -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を回すか、ACME(Let's Encryptなど)系のツールで自動化する体制が必要です。 diff --git a/docs-src/pages/ja/cookbook/t05-peer-cert.md b/docs-src/pages/ja/cookbook/t05-peer-cert.md new file mode 100644 index 0000000..cef7224 --- /dev/null +++ b/docs-src/pages/ja/cookbook/t05-peer-cert.md @@ -0,0 +1,88 @@ +--- +title: "T05. サーバー側でピア証明書を参照する" +order: 46 +status: "draft" +--- + +mTLS構成では、接続してきたクライアントの証明書をハンドラの中で読めます。証明書のCN(Common Name)やSAN(Subject 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でルート単位の認証を行うを参照してください。 + +## SNI(Server Name Indication) + +クライアントが指定してきたサーバー名は、cpp-httplibが自動で処理します。同じサーバーで複数のドメインをホストする場合にSNIが使われますが、通常はハンドラ側で意識する必要はありません。 + +> **Warning:** `req.peer_cert()`は、mTLSが有効で、かつクライアントが証明書を提示した場合のみ有効な値を返します。通常のTLS接続では空の`PeerCert`が返ります。使う前に必ず`bool`チェックしてください。 + +> mTLSの設定方法はT04. mTLSを設定するを参照してください。 diff --git a/docs-src/pages/ja/cookbook/w01-websocket-echo.md b/docs-src/pages/ja/cookbook/w01-websocket-echo.md new file mode 100644 index 0000000..2174c6e --- /dev/null +++ b/docs-src/pages/ja/cookbook/w01-websocket-echo.md @@ -0,0 +1,88 @@ +--- +title: "W01. WebSocketエコーサーバー/クライアントを実装する" +order: 51 +status: "draft" +--- + +WebSocketは、クライアントとサーバーの間で**双方向**にメッセージをやり取りするためのプロトコルです。cpp-httplibはサーバーとクライアントの両方のAPIを提供しています。まずは一番シンプルなエコーサーバーから見てみましょう。 + +## サーバー: エコーサーバー + +```cpp +#include + +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 + +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://`スキームを指定するだけです。 diff --git a/docs-src/pages/ja/cookbook/w02-websocket-ping.md b/docs-src/pages/ja/cookbook/w02-websocket-ping.md new file mode 100644 index 0000000..9655a40 --- /dev/null +++ b/docs-src/pages/ja/cookbook/w02-websocket-ping.md @@ -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. 接続クローズをハンドリングするを参照してください。 diff --git a/docs-src/pages/ja/cookbook/w03-websocket-close.md b/docs-src/pages/ja/cookbook/w03-websocket-close.md new file mode 100644 index 0000000..a11d5c5 --- /dev/null +++ b/docs-src/pages/ja/cookbook/w03-websocket-close.md @@ -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`を返します。後始末はハンドラ終了時にまとめて行うようにしておくと、どちらのパターンでも対応できます。 diff --git a/docs-src/pages/ja/cookbook/w04-websocket-binary.md b/docs-src/pages/ja/cookbook/w04-websocket-binary.md new file mode 100644 index 0000000..fb5e2b1 --- /dev/null +++ b/docs-src/pages/ja/cookbook/w04-websocket-binary.md @@ -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のデフォルトでは大きなフレームもそのまま処理されますが、メモリを一気に使う点は変わりません。