mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-04-12 03:38:30 +00:00
Add Cookbook other topics (draft)
This commit is contained in:
87
docs-src/pages/en/cookbook/e01-sse-server.md
Normal file
87
docs-src/pages/en/cookbook/e01-sse-server.md
Normal file
@@ -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: <content>\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.
|
||||
84
docs-src/pages/en/cookbook/e02-sse-event-names.md
Normal file
84
docs-src/pages/en/cookbook/e02-sse-event-names.md
Normal file
@@ -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.
|
||||
85
docs-src/pages/en/cookbook/e03-sse-reconnect.md
Normal file
85
docs-src/pages/en/cookbook/e03-sse-reconnect.md
Normal file
@@ -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<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;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
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.
|
||||
99
docs-src/pages/en/cookbook/e04-sse-client.md
Normal file
99
docs-src/pages/en/cookbook/e04-sse-client.md
Normal file
@@ -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.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(); // 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.
|
||||
@@ -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)
|
||||
|
||||
49
docs-src/pages/en/cookbook/t01-tls-backends.md
Normal file
49
docs-src/pages/en/cookbook/t01-tls-backends.md
Normal file
@@ -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 <httplib.h>
|
||||
```
|
||||
|
||||
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`.
|
||||
53
docs-src/pages/en/cookbook/t02-cert-verification.md
Normal file
53
docs-src/pages/en/cookbook/t02-cert-verification.md
Normal file
@@ -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.
|
||||
78
docs-src/pages/en/cookbook/t03-ssl-server.md
Normal file
78
docs-src/pages/en/cookbook/t03-ssl-server.md
Normal file
@@ -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 <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);
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
70
docs-src/pages/en/cookbook/t04-mtls.md
Normal file
70
docs-src/pages/en/cookbook/t04-mtls.md
Normal file
@@ -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.
|
||||
88
docs-src/pages/en/cookbook/t05-peer-cert.md
Normal file
88
docs-src/pages/en/cookbook/t05-peer-cert.md
Normal file
@@ -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.
|
||||
88
docs-src/pages/en/cookbook/w01-websocket-echo.md
Normal file
88
docs-src/pages/en/cookbook/w01-websocket-echo.md
Normal file
@@ -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 <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); // 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 <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();
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
60
docs-src/pages/en/cookbook/w02-websocket-ping.md
Normal file
60
docs-src/pages/en/cookbook/w02-websocket-ping.md
Normal file
@@ -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.
|
||||
91
docs-src/pages/en/cookbook/w03-websocket-close.md
Normal file
91
docs-src/pages/en/cookbook/w03-websocket-close.md
Normal file
@@ -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.
|
||||
85
docs-src/pages/en/cookbook/w04-websocket-binary.md
Normal file
85
docs-src/pages/en/cookbook/w04-websocket-binary.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user