mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-09-19 03:31:11 +08:00
feat(engineering): add boost-asio-pro skill for async C++ networking
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: "boost-asio-pro"
|
||||
description: "Use when writing or reviewing asynchronous C++ networking code with Boost.Asio or standalone Asio — TCP/UDP servers and clients, SSL/TLS, timers, strands, io_context, co_spawn, awaitable, async_read/async_write, asio::spawn, yield_context, or pre-C++20 completion-handler callbacks."
|
||||
---
|
||||
|
||||
# Boost.Asio / standalone Asio
|
||||
|
||||
## Overview
|
||||
|
||||
Write async C++ networking code that compiles on the *user's* Boost, not the newest one. Asio's API changed shape three times (classic `io_service` → `io_context` → C++20 coroutines) and most Asio code on the internet is from the first era, so **pick the style from the toolchain first**, then follow that style's reference file.
|
||||
|
||||
**References:** [Boost.Asio](https://www.boost.org/doc/libs/latest/doc/html/boost_asio.html) · [standalone Asio](https://think-async.com/Asio/)
|
||||
|
||||
Use this skill whenever async C++ networking code is being written or reviewed — and especially when the target toolchain is old, where coroutine examples simply will not compile. The three worked implementations it references are CI-verified from Boost 1.62 (2016) through 1.90.
|
||||
|
||||
## Step 1: pick the style (do this before writing code)
|
||||
|
||||
Determine the Boost (or Asio) version and the C++ standard actually in use — `find_package(Boost)` output, `dpkg -l libboost-dev`, `brew info boost`, `CMAKE_CXX_STANDARD`, or ask. Do not assume the newest.
|
||||
|
||||
| Boost | C++ std | Style | Read |
|
||||
|-------|---------|-------|------|
|
||||
| ≥ 1.77 | C++20 | Coroutines (`co_await` + `awaitable<T>`) — preferred | [references/coroutines.md](references/coroutines.md) |
|
||||
| ≥ 1.74 | C++11–17 | Completion handlers (callbacks) — the portable baseline | [references/pre-cpp20.md](references/pre-cpp20.md) |
|
||||
| ≥ 1.80 | C++11–17 | Stackful `asio::spawn` + `yield_context` (links Boost.Coroutine — not header-only) | [references/pre-cpp20.md](references/pre-cpp20.md) |
|
||||
| 1.62–1.65 | C++11 | Classic `io_service` / `strand.wrap` / `expires_from_now` | [references/classic-boost.md](references/classic-boost.md) |
|
||||
|
||||
SSL/TLS in any style: [references/ssl.md](references/ssl.md). CMake for any style: [references/build.md](references/build.md).
|
||||
|
||||
`io_context`, `make_strand`, `bind_executor`, `steady_timer`, `signal_set`, `async_read`/`async_write`/`async_read_until`, buffers and `resolver` are **library** features — identical in the coroutine and callback styles. Only the suspension mechanism differs.
|
||||
|
||||
## Step 2: version floors (verified by compiling, not from docs)
|
||||
|
||||
Reach for one of these and the build breaks on older distros:
|
||||
|
||||
| Feature | Floor |
|
||||
|---------|-------|
|
||||
| `experimental/awaitable_operators.hpp` (the `\|\|` / `&&` operators) | **Boost ≥ 1.77** / Asio ≥ 1.20 |
|
||||
| `as_tuple` completion token | **Boost ≥ 1.79** / Asio ≥ 1.21 |
|
||||
| `co_composed` (custom composed ops) | **Boost ≥ 1.85** / Asio ≥ 1.30 |
|
||||
| 3-arg `asio::spawn(ex, fn, token)` | **Boost ≥ 1.80** (older Boost has only `spawn(ex, fn)`) |
|
||||
| `any_io_executor` (`strand<any_io_executor>`, `tcp::socket`'s default executor) | **Boost ≥ 1.74** — the floor for the callback style; below it, use legacy `io_context::strand` |
|
||||
| `io_context`, `make_strand`, `expires_after` | **Boost ≥ 1.66** — below it, classic `io_service` |
|
||||
|
||||
Distro floors that bite: **Debian bookworm ships Boost 1.74** (no `awaitable_operators.hpp` — `#include` fails outright), Ubuntu 20.04 ships 1.71 (no `any_io_executor`), Debian 9 ships 1.62.
|
||||
|
||||
Language, not library: the chrono literals `250ms` / `30s` are **C++14**. For a true C++11 build write `std::chrono::milliseconds(250)`.
|
||||
|
||||
## Step 3: the rules that are actually easy to get wrong
|
||||
|
||||
**A strand does not serialize writes.** A strand serializes handler *execution*, not whole composed operations. Two `async_write`s in flight on the same strand still **interleave bytes on the wire**. Full-duplex (a read loop plus concurrent pushes/replies) needs a per-connection strand **and** an outbound queue with an in-flight flag, so at most one `async_write` exists at a time. This is the single most common wrong answer about Asio.
|
||||
|
||||
**Buffers do not own memory.** `asio::buffer()` is a view. Storage must outlive the operation: coroutine locals are fine across `co_await` in the same frame; in callback style the same data must become a **member**, not a local.
|
||||
|
||||
**Connections must outlive their handlers.** `enable_shared_from_this`, and capture `self` in *every* `co_spawn` / handler — read loop, write loop, and each timer.
|
||||
|
||||
**Frame with composed reads.** `async_read` (fills the buffer exactly) for a length prefix and then the body; never `async_read_some`, which returns short.
|
||||
|
||||
**Wrap `as_tuple`.** Always `as_tuple(use_awaitable)`. Bare `as_tuple` resolves against the operation's default token and compiles in some contexts, fails in others.
|
||||
|
||||
**`async_accept(make_strand(...))` changes two things**: it forces an explicit completion token back on the call, and the accepted socket is `basic_stream_socket<tcp, strand<...>>`, not `tcp::socket`. Take it **by value** or with `auto` — binding it to `tcp::socket&` will not compile.
|
||||
|
||||
**Re-arming a timer resolves the pending wait with `operation_aborted`.** In an idle-timeout loop that is the signal to keep waiting, not an error.
|
||||
|
||||
**GCC needs `-fcoroutines`** for the C++20 style, and header-only Boost needs `BOOST_ERROR_CODE_HEADER_ONLY` defined in exactly one place (CMake).
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Buffer dangling (local goes out of scope during async op) | Ensure buffer lifetime ≥ operation lifetime; coroutine locals or members, not callback locals |
|
||||
| Forgetting `io.run()` | No handlers dispatch without `run()` / `run_one()` |
|
||||
| Concurrent socket access without strand | Wrap in `strand<>` or serialize via one coroutine chain |
|
||||
| Assuming a strand prevents interleaved writes | Add a write queue — see Step 3 |
|
||||
| Using `use_awaitable` where `deferred` suffices | Omit the token (default is `deferred`) unless using `\|\|` / `&&` |
|
||||
| Ignoring short reads/writes | Use composed `async_read` / `async_write` / `async_read_until`, not `async_read_some` |
|
||||
| Not setting `reuse_address` on the acceptor | Set before `bind`/`listen` or restarts hit "address in use" |
|
||||
| SSL operations without a strand | *All* `ssl::stream` ops need strand synchronization |
|
||||
| Blocking inside a handler | Never block in a completion handler |
|
||||
| Accepting a socket with the wrong executor type | See `async_accept(make_strand(...))` in Step 3 |
|
||||
| Requiring the `Boost::system` component | Header-only since 1.74: `Boost::headers` + `BOOST_ERROR_CODE_HEADER_ONLY`. Only classic (pre-1.66) needs the link |
|
||||
| Missing `-fcoroutines` on GCC | Build fails — add `$<$<CXX_COMPILER_ID:GNU>:-fcoroutines>` |
|
||||
| Writing coroutine code for a Boost that predates it | Do Step 1 first |
|
||||
|
||||
## Boost.Asio vs standalone Asio
|
||||
|
||||
Same author, same API — namespace and includes differ.
|
||||
|
||||
| Aspect | Boost.Asio | Standalone Asio |
|
||||
|--------|-----------|-----------------|
|
||||
| Namespace / include | `boost::asio` / `<boost/asio.hpp>` | `asio` / `<asio.hpp>` |
|
||||
| Error code | `boost::system::error_code` | `asio::error_code` (or `std::error_code`) |
|
||||
| Install (brew) | `brew install boost` | `brew install asio` |
|
||||
| CMake | `Boost::headers` | manual include path |
|
||||
| Version (2025) | 1.87–1.90 (with Boost) | 1.30–1.36 (independent) |
|
||||
| Macro prefix | `BOOST_ASIO_` | `ASIO_` |
|
||||
|
||||
Support both with a shim, then use `net::` throughout:
|
||||
```cpp
|
||||
#ifdef USE_STANDALONE_ASIO
|
||||
#include <asio.hpp>
|
||||
namespace net = asio;
|
||||
using error_code = asio::error_code;
|
||||
#else
|
||||
#include <boost/asio.hpp>
|
||||
namespace net = boost::asio;
|
||||
using error_code = boost::system::error_code;
|
||||
#endif
|
||||
namespace ssl = net::ssl;
|
||||
using tcp = net::ip::tcp;
|
||||
```
|
||||
|
||||
## Before you call it done
|
||||
|
||||
Check the code you just wrote against this list:
|
||||
|
||||
- [ ] Style matches the target Boost version and C++ standard (Step 1), and every API used clears its floor (Step 2).
|
||||
- [ ] Every buffer passed to an async op outlives that op — no callback locals, no dangling `string_view`.
|
||||
- [ ] At most one `async_write` per socket in flight, enforced by a queue + flag, if anything writes concurrently with reading.
|
||||
- [ ] Every async chain on a shared object runs on the same strand; `self` captured in every handler and `co_spawn`.
|
||||
- [ ] Framing / delimited reads use composed `async_read` / `async_read_until`.
|
||||
- [ ] Errors are handled, not swallowed: `as_tuple(use_awaitable)` destructured, or the callback's `ec` checked, on every op.
|
||||
- [ ] `operation_aborted` distinguished from real errors wherever a timer is re-armed or an op is cancelled.
|
||||
- [ ] Acceptor sets `reuse_address`; shutdown path closes the acceptor and drains sessions.
|
||||
- [ ] CMake has the standard, `-fcoroutines` for GCC (C++20 only), `BOOST_ERROR_CODE_HEADER_ONLY` in one place, and `Boost::coroutine` only if using stackful `spawn`.
|
||||
- [ ] It compiles. Build it — most of the mistakes above are compile-time, and the version floors are only real once tested.
|
||||
|
||||
## Worked examples
|
||||
|
||||
Three CI-verified implementations of the same full-duplex framed-protocol server, one per style — copy from the one matching Step 1. All three live in the upstream repository and are built by CI on every push.
|
||||
|
||||
- [market-data-feed](https://github.com/alexprivalov/boost-asio-skill/tree/main/examples/market-data-feed) — C++20 coroutines (Boost 1.77+; verified 1.83–1.90)
|
||||
- [market-data-feed-precpp20](https://github.com/alexprivalov/boost-asio-skill/tree/main/examples/market-data-feed-precpp20) — callbacks, C++11-clean (verified Boost 1.74+, incl. Windows/MSVC)
|
||||
- [market-data-feed-classic](https://github.com/alexprivalov/boost-asio-skill/tree/main/examples/market-data-feed-classic) — classic `io_service` (verified back to Boost 1.62 / Debian 9)
|
||||
|
||||
## Official documentation
|
||||
|
||||
- Overview: https://www.boost.org/doc/libs/latest/doc/html/boost_asio/overview.html
|
||||
- Reference: https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference.html
|
||||
- Examples: https://www.boost.org/doc/libs/latest/doc/html/boost_asio/examples.html
|
||||
|
||||
## Cross-References
|
||||
|
||||
- `engineering/docker-development` — the old-Boost verification lanes this skill's floors come from are containerised builds (Debian 9 / bookworm, Fedora).
|
||||
- `engineering/chaos-engineering` — for exercising the failure paths this skill tells you to handle: half-open sockets, idle timeouts, partial frames.
|
||||
- `engineering-team/playwright-pro` — the client-side counterpart when the server built here is driven from browser-based integration tests.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Build Configuration
|
||||
|
||||
|
||||
### Boost.Asio (header-only since Boost 1.74+)
|
||||
|
||||
```cmake
|
||||
find_package(Boost REQUIRED)
|
||||
find_package(OpenSSL REQUIRED) # if using SSL
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
target_link_libraries(myapp PRIVATE
|
||||
Boost::headers # header-only Asio
|
||||
OpenSSL::SSL OpenSSL::Crypto # if using SSL
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
target_compile_features(myapp PRIVATE cxx_std_20)
|
||||
|
||||
# REQUIRED for GCC coroutine support — build will fail without this
|
||||
target_compile_options(myapp PRIVATE
|
||||
$<$<CXX_COMPILER_ID:GNU>:-fcoroutines>
|
||||
)
|
||||
|
||||
# Optional: truly header-only (no Boost.System link needed)
|
||||
target_compile_definitions(myapp PRIVATE BOOST_ERROR_CODE_HEADER_ONLY)
|
||||
```
|
||||
|
||||
### Standalone Asio (always header-only)
|
||||
|
||||
```cmake
|
||||
# Standalone Asio has no CMake config — use pkg-config or manual path
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# If installed via brew:
|
||||
find_path(ASIO_INCLUDE_DIR asio.hpp HINTS /opt/homebrew/include)
|
||||
|
||||
target_include_directories(myapp PRIVATE ${ASIO_INCLUDE_DIR})
|
||||
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto Threads::Threads)
|
||||
target_compile_features(myapp PRIVATE cxx_std_20)
|
||||
target_compile_definitions(myapp PRIVATE ASIO_STANDALONE)
|
||||
|
||||
target_compile_options(myapp PRIVATE
|
||||
$<$<CXX_COMPILER_ID:GNU>:-fcoroutines>
|
||||
)
|
||||
```
|
||||
|
||||
### Dual-mode CMake (supports both)
|
||||
|
||||
```cmake
|
||||
option(USE_STANDALONE_ASIO "Use standalone Asio instead of Boost.Asio" OFF)
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
if(USE_STANDALONE_ASIO)
|
||||
find_path(ASIO_INCLUDE_DIR asio.hpp HINTS /opt/homebrew/include)
|
||||
target_include_directories(myapp PRIVATE ${ASIO_INCLUDE_DIR})
|
||||
target_compile_definitions(myapp PRIVATE USE_STANDALONE_ASIO ASIO_STANDALONE)
|
||||
else()
|
||||
find_package(Boost REQUIRED)
|
||||
target_link_libraries(myapp PRIVATE Boost::headers)
|
||||
target_compile_definitions(myapp PRIVATE BOOST_ERROR_CODE_HEADER_ONLY)
|
||||
endif()
|
||||
|
||||
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto Threads::Threads)
|
||||
target_compile_features(myapp PRIVATE cxx_std_20)
|
||||
target_compile_options(myapp PRIVATE $<$<CXX_COMPILER_ID:GNU>:-fcoroutines>)
|
||||
```
|
||||
|
||||
## Header-Only Usage
|
||||
|
||||
**Boost.Asio:** Asio is header-only by default. The only thing that pulls in a Boost library to link is `boost::system::error_code`'s out-of-line symbols, so for a truly link-free build define **`BOOST_ERROR_CODE_HEADER_ONLY`**. `BOOST_ASIO_HEADER_ONLY` is rarely needed and only relevant if separate compilation was previously enabled; you do **not** normally need both.
|
||||
|
||||
**Define `BOOST_ERROR_CODE_HEADER_ONLY` in exactly ONE place — prefer CMake** (`target_compile_definitions`, as shown above). Defining it in CMake *and* with a source `#define` triggers `-Wmacro-redefined`. So in source, just include — no `#define`:
|
||||
```cpp
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/experimental/awaitable_operators.hpp>
|
||||
```
|
||||
|
||||
**Standalone Asio:**
|
||||
```cpp
|
||||
#include <asio.hpp>
|
||||
#include <asio/ssl.hpp>
|
||||
#include <asio/experimental/awaitable_operators.hpp>
|
||||
// No macros needed — always header-only
|
||||
```
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Classic Boost (pre-1.66, the `io_service` era — verified to 1.62)
|
||||
|
||||
|
||||
To support Boost older than 1.66 (no `io_context`, no `make_strand`, no `any_io_executor`), drop to the classic API — verified building **back to Boost 1.62** (Debian 9) while still compiling on current Boost via a tiny shim:
|
||||
|
||||
| Modern (1.66+) | Classic (pre-1.66) |
|
||||
|----------------|--------------------|
|
||||
| `io_context` | `io_service` |
|
||||
| `make_strand(ex)` / `strand<any_io_executor>` | `io_service::strand strand(io)` |
|
||||
| `bind_executor(strand, h)` | `strand.wrap(h)` |
|
||||
| `timer.expires_after(d)` | `timer.expires_from_now(d)` |
|
||||
| move-return `async_accept()` | `async_accept(socket_, handler)` |
|
||||
| header-only `error_code` | link **Boost.System** (`find_package(Boost COMPONENTS system)`) |
|
||||
|
||||
Only the `io_service`/`io_context` name and the `expires_after`/`expires_from_now` call actually differ across 1.62…1.90 — isolate both behind `#if BOOST_VERSION >= 106600`:
|
||||
```cpp
|
||||
#include <boost/version.hpp>
|
||||
#include <boost/asio/steady_timer.hpp> // not pulled in by <boost/asio.hpp> on old Boost
|
||||
#if BOOST_VERSION >= 106600
|
||||
using io_service_t = boost::asio::io_context;
|
||||
#else
|
||||
using io_service_t = boost::asio::io_service;
|
||||
#endif
|
||||
template <class T, class Rep, class Period>
|
||||
void timer_expires_in(T& t, std::chrono::duration<Rep,Period> d) {
|
||||
#if BOOST_VERSION >= 106600
|
||||
t.expires_after(d);
|
||||
#else
|
||||
t.expires_from_now(d);
|
||||
#endif
|
||||
}
|
||||
```
|
||||
CMake for this range: `cmake_minimum_required(VERSION 3.5)` (Debian 9 ships cmake 3.7), link `Boost::system` only if the component is found (modern Boost is header-only and has no such component), and use the classic out-of-source build (`mkdir build && cd build && cmake ..`) since `-S`/`-B` need cmake ≥ 3.13.
|
||||
@@ -0,0 +1,412 @@
|
||||
# C++20 Coroutine Style (Boost ≥ 1.77)
|
||||
|
||||
The preferred style when the toolchain allows it. Read `SKILL.md` first — the rules there (write queue, buffer lifetime, version floors) apply here and are not repeated.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
Boost.Asio uses the **Proactor pattern**: async operations run in the background, completion handlers are invoked with results.
|
||||
|
||||
```
|
||||
Program → I/O Object → Execution Context → OS → (completion) → Handler
|
||||
```
|
||||
|
||||
**Execution contexts:** `io_context` (single/multi-thread event loop), `thread_pool`, `system_context`
|
||||
|
||||
**I/O objects:** `tcp::socket`, `tcp::acceptor`, `udp::socket`, `steady_timer`, `ssl::stream<>`
|
||||
|
||||
**Completion tokens:** Control how async results are delivered — `use_awaitable`, `deferred` (default), `detached`, callbacks, futures.
|
||||
|
||||
## C++20 Coroutines (Preferred Style)
|
||||
|
||||
```cpp
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
namespace asio = boost::asio;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
asio::awaitable<void> echo_session(tcp::socket socket) {
|
||||
try {
|
||||
char data[1024];
|
||||
for (;;) {
|
||||
std::size_t n = co_await socket.async_read_some(asio::buffer(data));
|
||||
co_await async_write(socket, asio::buffer(data, n));
|
||||
}
|
||||
} catch (std::exception&) {
|
||||
// Connection closed or error — coroutine ends
|
||||
}
|
||||
}
|
||||
|
||||
asio::awaitable<void> listener(tcp::acceptor acceptor) {
|
||||
for (;;) {
|
||||
auto socket = co_await acceptor.async_accept();
|
||||
co_spawn(acceptor.get_executor(), echo_session(std::move(socket)), asio::detached);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
asio::io_context io(1); // concurrency_hint=1 for single-threaded
|
||||
tcp::acceptor acceptor(io, {tcp::v4(), 8080});
|
||||
co_spawn(io, listener(std::move(acceptor)), asio::detached);
|
||||
io.run();
|
||||
}
|
||||
```
|
||||
|
||||
**Key rules:**
|
||||
- `co_spawn(executor, coroutine, completion_token)` launches a coroutine
|
||||
- Without explicit token, async ops use `deferred` (returns awaitable object for `co_await`)
|
||||
- Errors become `system_error` exceptions by default inside coroutines
|
||||
- Use `asio::detached` when you don't need the coroutine's result
|
||||
|
||||
## Error Handling in Coroutines
|
||||
|
||||
**Default:** Errors throw `boost::system::system_error`.
|
||||
|
||||
**Explicit error handling with `as_tuple`:**
|
||||
```cpp
|
||||
auto [ec, n] = co_await socket.async_read_some(
|
||||
asio::buffer(data), asio::as_tuple(asio::use_awaitable));
|
||||
if (ec) { /* handle error, no exception */ }
|
||||
```
|
||||
**Wrap, don't use bare `as_tuple`.** Always write `as_tuple(use_awaitable)`. Bare `asio::as_tuple` resolves against the operation's *default* completion token (often `deferred`), which compiles in some contexts but fails in others — wrapping an explicit base token is unambiguous everywhere.
|
||||
|
||||
**With `redirect_error`:**
|
||||
```cpp
|
||||
boost::system::error_code ec;
|
||||
std::size_t n = co_await socket.async_read_some(
|
||||
asio::buffer(data), asio::redirect_error(ec));
|
||||
```
|
||||
|
||||
## Strands (Thread Safety)
|
||||
|
||||
**Rule: All async operations on a shared object MUST execute on the same strand.**
|
||||
|
||||
```cpp
|
||||
// Per-connection strand
|
||||
asio::strand<asio::io_context::executor_type> strand(io.get_executor());
|
||||
co_spawn(strand, session(std::move(socket)), asio::detached);
|
||||
|
||||
// Bind handler to strand
|
||||
socket.async_read_some(asio::buffer(data),
|
||||
asio::bind_executor(strand, [](error_code ec, size_t n) { /*...*/ }));
|
||||
```
|
||||
|
||||
**Implicit strands (no explicit strand needed):**
|
||||
- Single-threaded `io_context::run()` — all handlers are sequential
|
||||
- Single chain of async ops on one connection (half-duplex)
|
||||
|
||||
**Explicit strand required when:**
|
||||
- Multiple threads call `io_context::run()`
|
||||
- Full-duplex read+write on same socket
|
||||
- Shared state accessed from multiple async chains
|
||||
|
||||
## Full-Duplex: Strand + Write Queue
|
||||
|
||||
**A strand serializes handler *execution*, NOT whole composed operations.** Two `async_write`s started "concurrently" on the same strand still overlap and **interleave bytes on the wire** — the strand only orders the intermediate handlers, not the byte stream. For full-duplex (a read loop plus pushes/replies writing at the same time on one socket), a strand alone is **not** enough: you must serialize outbound writes yourself with a queue.
|
||||
|
||||
```cpp
|
||||
// Give each accepted socket its OWN strand, then run every chain (read loop,
|
||||
// pushes, replies) on that strand. Passing an executor to async_accept means you
|
||||
// must ALSO pass an explicit completion token — the default-deferred shortcut on
|
||||
// the zero-arg form no longer applies.
|
||||
auto socket = co_await acceptor.async_accept(asio::make_strand(io), asio::use_awaitable);
|
||||
std::make_shared<connection>(std::move(socket))->start();
|
||||
|
||||
class connection : public std::enable_shared_from_this<connection> {
|
||||
tcp::socket socket_; // bound to its own strand
|
||||
std::deque<std::string> outbox_;
|
||||
bool writing_ = false;
|
||||
public:
|
||||
explicit connection(tcp::socket s) : socket_(std::move(s)) {}
|
||||
|
||||
void start() {
|
||||
// Each chain captures `self` so the connection outlives all its coroutines.
|
||||
co_spawn(socket_.get_executor(),
|
||||
[self = shared_from_this()] { return self->read_loop(); }, asio::detached);
|
||||
}
|
||||
|
||||
// Call ONLY from the connection's strand (e.g. from its own coroutines).
|
||||
// From another thread/strand: asio::dispatch(socket_.get_executor(), ...).
|
||||
void send(std::string frame) {
|
||||
outbox_.push_back(std::move(frame));
|
||||
if (!writing_)
|
||||
co_spawn(socket_.get_executor(),
|
||||
[self = shared_from_this()] { return self->write_loop(); }, asio::detached);
|
||||
}
|
||||
private:
|
||||
asio::awaitable<void> write_loop() {
|
||||
writing_ = true;
|
||||
while (!outbox_.empty()) {
|
||||
co_await async_write(socket_, asio::buffer(outbox_.front()));
|
||||
outbox_.pop_front(); // pop only AFTER the write completes
|
||||
}
|
||||
writing_ = false;
|
||||
}
|
||||
asio::awaitable<void> read_loop(); // reads frames, calls send() for replies
|
||||
};
|
||||
```
|
||||
|
||||
**Why each rule matters:**
|
||||
- One strand per connection → read loop and write loop never run their handlers concurrently.
|
||||
- Write queue + `writing_` flag → at most one `async_write` in flight, so frames never interleave.
|
||||
- `enable_shared_from_this` + capturing `self` in every `co_spawn` → the connection survives until all of its read/write/timer chains finish.
|
||||
- The accepted socket from `async_accept(make_strand(...))` is `basic_stream_socket<tcp, strand<...>>`, **not** `tcp::socket`. Take it **by value** (`connection(tcp::socket s)`, store `tcp::socket socket_`) — the strand executor type-erases into `any_io_executor` on the move. Passing that accepted socket to a `tcp::socket&` (by reference) instead will **fail to compile** — use `auto` or accept by value.
|
||||
|
||||
**Strand from inside a coroutine** (when `io` isn't a captured local): get the executor from the coroutine and make a strand off it — no `io_context&` needed:
|
||||
```cpp
|
||||
auto ex = co_await asio::this_coro::executor;
|
||||
auto socket = co_await acceptor.async_accept(asio::make_strand(ex), asio::use_awaitable);
|
||||
```
|
||||
|
||||
**Run the read loop and idle watch together** — two `awaitable<void>` branches; don't inspect the result, the first to finish unwinds the other:
|
||||
```cpp
|
||||
using namespace asio::experimental::awaitable_operators;
|
||||
co_await (read_loop() || idle_watch(socket_, timer_)); // either returning tears down the connection
|
||||
```
|
||||
|
||||
**Stopping a detached side-coroutine** (e.g. a per-symbol ticker that must end on unsubscribe/close): a detached `co_spawn` won't stop itself. Either (a) have its loop re-check a flag each iteration and `co_return` when gone:
|
||||
```cpp
|
||||
while (subscriptions_.contains(symbol) && socket_.is_open()) {
|
||||
timer.expires_after(250ms);
|
||||
co_await timer.async_wait(asio::as_tuple(asio::use_awaitable));
|
||||
if (/* still subscribed */) send(make_tick(symbol));
|
||||
}
|
||||
```
|
||||
or (b) spawn it with a `cancellation_signal` and `emit()` cancellation on unsubscribe. The flag approach is simpler for per-subscription tickers.
|
||||
|
||||
## Timers and Timeouts
|
||||
|
||||
```cpp
|
||||
asio::awaitable<void> with_timeout(tcp::socket& socket) {
|
||||
asio::steady_timer timer(co_await asio::this_coro::executor);
|
||||
timer.expires_after(std::chrono::seconds(30));
|
||||
|
||||
// Race: read vs timeout (requires awaitable_operators)
|
||||
using namespace asio::experimental::awaitable_operators;
|
||||
|
||||
auto result = co_await (
|
||||
socket.async_read_some(asio::buffer(data), asio::use_awaitable)
|
||||
|| timer.async_wait(asio::use_awaitable)
|
||||
);
|
||||
|
||||
if (result.index() == 0) { /* read completed */ }
|
||||
else { /* timeout — cancel the socket */ socket.close(); }
|
||||
}
|
||||
```
|
||||
|
||||
**Re-armable idle timeout** (reset on every received frame — the common server pattern):
|
||||
```cpp
|
||||
// Run as a long-lived parallel branch. Calling expires_after() again cancels the
|
||||
// pending wait, resolving the in-flight async_wait with operation_aborted — that
|
||||
// is the signal to keep waiting, NOT an error. Genuine expiry resolves with no error.
|
||||
asio::awaitable<void> idle_watch(tcp::socket& sock, asio::steady_timer& timer) {
|
||||
for (;;) {
|
||||
auto [ec] = co_await timer.async_wait(asio::as_tuple(asio::use_awaitable));
|
||||
if (ec == asio::error::operation_aborted) continue; // re-armed → keep waiting
|
||||
if (ec) co_return; // timer error
|
||||
sock.close(); // real timeout fired
|
||||
co_return;
|
||||
}
|
||||
}
|
||||
// On every frame received from the peer: timer.expires_after(30s);
|
||||
```
|
||||
|
||||
**Parallel operations (`&&` and `||`):**
|
||||
```cpp
|
||||
#include <boost/asio/experimental/awaitable_operators.hpp>
|
||||
using namespace asio::experimental::awaitable_operators;
|
||||
|
||||
// Wait for both (AND) — cancels other on failure
|
||||
auto [read_n, write_n] = co_await (
|
||||
async_read(sock, in_buf, use_awaitable) &&
|
||||
async_write(sock, out_buf, use_awaitable)
|
||||
);
|
||||
|
||||
// Wait for first (OR) — cancels other on success
|
||||
auto result = co_await (
|
||||
async_read(sock, buf, use_awaitable) ||
|
||||
timer.async_wait(use_awaitable)
|
||||
);
|
||||
```
|
||||
|
||||
**Note:** `||` and `&&` operators require explicit `use_awaitable` token, and the `awaitable_operators.hpp` header (Boost ≥ 1.77 — see the version floors in SKILL.md).
|
||||
|
||||
**Void branches:** when a branch returns `void` (e.g. two `awaitable<void>` chains), that arm contributes `std::monostate` to the result variant. If *both* branches are void the result is `variant<monostate, monostate>` — don't inspect `.index()`; just `co_await` the expression and let whichever finishes first unwind the other.
|
||||
|
||||
## Cancellation
|
||||
|
||||
```cpp
|
||||
asio::awaitable<void> cancellable_work() {
|
||||
// Check cancellation state
|
||||
auto cs = co_await asio::this_coro::cancellation_state;
|
||||
if (cs.cancelled() != asio::cancellation_type::none) {
|
||||
co_return;
|
||||
}
|
||||
|
||||
// Enable cancellation types
|
||||
co_await asio::this_coro::reset_cancellation_state(
|
||||
asio::enable_total_cancellation());
|
||||
}
|
||||
```
|
||||
|
||||
## TCP Server Pattern
|
||||
|
||||
```cpp
|
||||
asio::awaitable<void> server(asio::io_context& io, unsigned short port) {
|
||||
tcp::acceptor acceptor(io, {tcp::v4(), port});
|
||||
acceptor.set_option(tcp::acceptor::reuse_address(true));
|
||||
|
||||
for (;;) {
|
||||
auto socket = co_await acceptor.async_accept();
|
||||
co_spawn(
|
||||
io.get_executor(), // or a strand for multi-threaded
|
||||
handle_client(std::move(socket)),
|
||||
[](std::exception_ptr ep) {
|
||||
if (ep) std::rethrow_exception(ep);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Buffers
|
||||
|
||||
| Type | Use |
|
||||
|------|-----|
|
||||
| `asio::buffer(data, size)` | Wrap existing memory (no ownership) |
|
||||
| `asio::dynamic_buffer(vec)` | Growable buffer over `vector`/`string` |
|
||||
| `asio::streambuf` | Legacy stream buffer |
|
||||
| `asio::const_buffer` | Read-only view |
|
||||
| `asio::mutable_buffer` | Writable view |
|
||||
|
||||
**Critical:** `asio::buffer()` does NOT own memory. The underlying storage must outlive the async operation.
|
||||
|
||||
## Resolver (DNS)
|
||||
|
||||
```cpp
|
||||
asio::awaitable<void> connect_to(asio::io_context& io,
|
||||
std::string host, std::string port) {
|
||||
tcp::resolver resolver(io);
|
||||
auto endpoints = co_await resolver.async_resolve(host, port);
|
||||
|
||||
tcp::socket socket(io);
|
||||
co_await asio::async_connect(socket, endpoints);
|
||||
// socket is now connected
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Threaded io_context
|
||||
|
||||
```cpp
|
||||
asio::io_context io;
|
||||
std::vector<std::thread> threads;
|
||||
|
||||
for (int i = 0; i < std::thread::hardware_concurrency(); ++i) {
|
||||
threads.emplace_back([&io] { io.run(); });
|
||||
}
|
||||
|
||||
// All handlers MUST be strand-protected when sharing state
|
||||
for (auto& t : threads) t.join();
|
||||
```
|
||||
|
||||
## Composed Async Operations (Custom)
|
||||
|
||||
```cpp
|
||||
template <typename CompletionToken>
|
||||
auto async_echo(tcp::socket& socket, CompletionToken&& token) {
|
||||
return asio::async_initiate<CompletionToken, void(boost::system::error_code)>(
|
||||
asio::co_composed<void(boost::system::error_code)>(
|
||||
[](auto state, tcp::socket& socket) -> void {
|
||||
state.throw_if_cancelled(true);
|
||||
state.reset_cancellation_state(asio::enable_terminal_cancellation());
|
||||
try {
|
||||
char data[1024];
|
||||
for (;;) {
|
||||
std::size_t n = co_await socket.async_read_some(asio::buffer(data));
|
||||
co_await async_write(socket, asio::buffer(data, n));
|
||||
}
|
||||
} catch (const boost::system::system_error& e) {
|
||||
co_return {e.code()};
|
||||
}
|
||||
}, socket),
|
||||
token, std::ref(socket));
|
||||
}
|
||||
```
|
||||
|
||||
## Line-Based Protocols
|
||||
|
||||
For newline-delimited protocols, prefer `async_read_until` over manual `async_read_some` + buffer parsing:
|
||||
|
||||
```cpp
|
||||
asio::awaitable<void> line_echo(tcp::socket socket) {
|
||||
asio::streambuf buf;
|
||||
for (;;) {
|
||||
std::size_t n = co_await asio::async_read_until(socket, buf, '\n');
|
||||
std::string line(asio::buffers_begin(buf.data()),
|
||||
asio::buffers_begin(buf.data()) + n);
|
||||
buf.consume(n);
|
||||
co_await async_write(socket, asio::buffer(line));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or with `dynamic_buffer` over a `std::string`:
|
||||
```cpp
|
||||
std::string buf;
|
||||
std::size_t n = co_await asio::async_read_until(socket, asio::dynamic_buffer(buf), '\n');
|
||||
std::string line = buf.substr(0, n);
|
||||
buf.erase(0, n);
|
||||
```
|
||||
|
||||
## Length-Prefixed Binary Framing
|
||||
|
||||
For binary protocols, read the fixed-size header fully, then the body fully — two sequential **composed** reads (`async_read` fills the whole buffer, handling short reads). Do NOT use `async_read_some` for framing.
|
||||
|
||||
```cpp
|
||||
// Frame: [4-byte big-endian length N][N-byte body]
|
||||
asio::awaitable<std::string> read_frame(tcp::socket& sock) {
|
||||
uint32_t len_be = 0;
|
||||
co_await async_read(sock, asio::buffer(&len_be, sizeof len_be)); // exactly 4 bytes
|
||||
uint32_t n = ntohl(len_be); // <arpa/inet.h>; or hand-roll endian swap
|
||||
std::string body(n, '\0');
|
||||
co_await async_read(sock, asio::buffer(body)); // exactly n bytes
|
||||
co_return body;
|
||||
}
|
||||
|
||||
asio::awaitable<void> write_frame(tcp::socket& sock, std::string_view body) {
|
||||
uint32_t len_be = htonl(static_cast<uint32_t>(body.size()));
|
||||
std::array<asio::const_buffer, 2> bufs{
|
||||
asio::buffer(&len_be, sizeof len_be), asio::buffer(body)};
|
||||
co_await async_write(sock, bufs); // gather-write header + body atomically
|
||||
// len_be and body must outlive the write — they do here (co_await suspends in-frame).
|
||||
}
|
||||
```
|
||||
|
||||
## Graceful Shutdown (signal_set)
|
||||
|
||||
```cpp
|
||||
asio::signal_set signals(io, SIGINT, SIGTERM);
|
||||
signals.async_wait([&](const boost::system::error_code&, int /*signo*/) {
|
||||
acceptor.close(); // stop accepting; let in-flight sessions drain, then io.run() returns
|
||||
// or, for an immediate stop: io.stop();
|
||||
});
|
||||
```
|
||||
|
||||
For coroutine-style shutdown, `co_await signals.async_wait()` in a dedicated coroutine instead of a callback.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Operation | Function |
|
||||
|-----------|----------|
|
||||
| Launch coroutine | `co_spawn(executor, coro, token)` |
|
||||
| Accept connection | `co_await acceptor.async_accept()` |
|
||||
| Read some bytes | `co_await socket.async_read_some(buffer)` |
|
||||
| Read exact/until | `co_await async_read(stream, buf)` / `async_read_until(stream, buf, delim)` |
|
||||
| Write all | `co_await async_write(stream, buffer)` |
|
||||
| Connect | `co_await async_connect(socket, endpoints)` |
|
||||
| Resolve DNS | `co_await resolver.async_resolve(host, port)` |
|
||||
| Wait timer | `co_await timer.async_wait()` |
|
||||
| TLS handshake | `co_await stream.async_handshake(type)` |
|
||||
| Get executor | `co_await asio::this_coro::executor` |
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# Pre-C++20 Styles (C++11–17, Boost ≥ 1.74)
|
||||
|
||||
|
||||
If you can't use C++20 `co_await`, the **same Asio library** (modern Boost or standalone) still works — only the *async style* changes. Compile with C++11 or later. Two pre-C++20 styles:
|
||||
|
||||
1. **Completion handlers (callbacks)** — header-only, C++11, no extra dependencies. The recommended baseline.
|
||||
2. **Stackful coroutines** (`asio::spawn` + `yield_context`) — synchronous-looking like `co_await`, but built on Boost.Coroutine/Boost.Context, so it **must be linked** (not header-only) — see build note below.
|
||||
|
||||
**Unchanged from the coroutine style** (these are library, not language, features): `io_context`, `make_strand`, `bind_executor`, `steady_timer`, `ssl::stream`, `signal_set`, `async_read`/`async_write`/`async_read_until`, buffers, `resolver`. Use them exactly as shown in [coroutines.md](coroutines.md).
|
||||
|
||||
**Not available pre-C++20:** `co_await`/`awaitable<T>`, `co_spawn`, `use_awaitable`, the `||`/`&&` `awaitable_operators`, `as_tuple`, and `co_composed`. The table below gives the equivalent.
|
||||
|
||||
### C++20 → pre-C++20 mapping
|
||||
|
||||
| C++20 coroutine | Pre-C++20 equivalent |
|
||||
|-----------------|----------------------|
|
||||
| `co_await op(use_awaitable)` | callback: `op(handler)` · stackful: `op(yield)` |
|
||||
| `awaitable<T>` function | member `do_x()` callback chain · or `spawn(strand, fn)` |
|
||||
| `co_spawn(ex, coro, tok)` | start the callback chain · or `asio::spawn(ex, fn, tok)` |
|
||||
| `as_tuple(use_awaitable)` → `[ec,n]` | callback's `(ec, n)` params · stackful: `op(yield[ec])` |
|
||||
| `a() \|\| b()` (first-wins race) | a **watchdog timer** that closes the socket; the other op fails with `operation_aborted` |
|
||||
| `a() && b()` (wait both) | launch both, count completions in a shared `shared_ptr<int>` |
|
||||
| `co_composed<>` custom op | `asio::async_compose<>` (C++11) |
|
||||
| `co_await this_coro::executor` | `socket_.get_executor()` / a passed-in executor |
|
||||
|
||||
### Callback style: full-duplex + write queue
|
||||
|
||||
The full-duplex write-queue rule is identical — a strand alone doesn't stop interleaved writes — just expressed with chained handlers. Capture `self = shared_from_this()` in **every** handler to keep the connection alive.
|
||||
|
||||
```cpp
|
||||
class connection : public std::enable_shared_from_this<connection> {
|
||||
tcp::socket socket_;
|
||||
asio::strand<asio::any_io_executor> strand_; // tcp::socket's executor is any_io_executor
|
||||
std::deque<std::string> outbox_;
|
||||
bool writing_ = false;
|
||||
char buf_[1024];
|
||||
public:
|
||||
explicit connection(tcp::socket s)
|
||||
: socket_(std::move(s)), strand_(asio::make_strand(socket_.get_executor())) {}
|
||||
void start() { do_read(); }
|
||||
|
||||
void send(std::string frame) { // call on the strand only
|
||||
outbox_.push_back(std::move(frame));
|
||||
if (!writing_) do_write();
|
||||
}
|
||||
private:
|
||||
void do_read() {
|
||||
auto self = shared_from_this();
|
||||
socket_.async_read_some(asio::buffer(buf_),
|
||||
asio::bind_executor(strand_, // serialize handler execution
|
||||
[this, self](boost::system::error_code ec, std::size_t n) {
|
||||
if (ec) return; // self drops here → socket closes
|
||||
/* parse buf_[0..n]; call send() for replies */
|
||||
do_read();
|
||||
}));
|
||||
}
|
||||
void do_write() { // at most one async_write in flight
|
||||
writing_ = true;
|
||||
auto self = shared_from_this();
|
||||
asio::async_write(socket_, asio::buffer(outbox_.front()),
|
||||
asio::bind_executor(strand_,
|
||||
[this, self](boost::system::error_code ec, std::size_t) {
|
||||
if (ec) { writing_ = false; return; }
|
||||
outbox_.pop_front();
|
||||
if (!outbox_.empty()) do_write();
|
||||
else writing_ = false;
|
||||
}));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Stackful style: spawn + yield_context
|
||||
|
||||
`yield` is a completion token: `op(socket, ..., yield)` suspends until done and returns the result; errors **throw** by default, or use `yield[ec]` for an `error_code`. Run each chain on a per-connection strand.
|
||||
|
||||
```cpp
|
||||
asio::spawn(strand, // executor or strand
|
||||
[self](asio::yield_context yield) { // capture self for lifetime
|
||||
try {
|
||||
char data[1024];
|
||||
for (;;) {
|
||||
std::size_t n = self->socket_.async_read_some(asio::buffer(data), yield);
|
||||
asio::async_write(self->socket_, asio::buffer(data, n), yield);
|
||||
}
|
||||
} catch (const std::exception&) { self->socket_.close(); }
|
||||
},
|
||||
asio::detached); // completion token (3rd arg)
|
||||
```
|
||||
|
||||
### Timeout without `||` (watchdog timer)
|
||||
|
||||
Replace the `read || timer` race with a separate watchdog: reset the timer on each read; a second chain waits on it and closes the socket on expiry, which makes the read fail with `operation_aborted`.
|
||||
|
||||
```cpp
|
||||
// callback watchdog
|
||||
void arm_timeout() {
|
||||
timer_.expires_after(std::chrono::seconds(30));
|
||||
auto self = shared_from_this();
|
||||
timer_.async_wait(asio::bind_executor(strand_,
|
||||
[this, self](boost::system::error_code ec) {
|
||||
if (!ec) socket_.close(); // fired → drop; reset cancels with ec
|
||||
}));
|
||||
}
|
||||
// call arm_timeout() again on every frame received to re-arm
|
||||
```
|
||||
|
||||
### Callback multi-step reads + recurring side-tasks
|
||||
|
||||
**Lifetime shift:** coroutine *stack locals* become *member variables* in callback style — a header/body buffer must outlive each async op or it dangles. Chain a composed read of the length, then the body:
|
||||
|
||||
```cpp
|
||||
// members, NOT locals — they must survive until the handler runs
|
||||
uint32_t len_be_;
|
||||
std::string body_;
|
||||
|
||||
void read_frame() {
|
||||
auto self = shared_from_this();
|
||||
asio::async_read(socket_, asio::buffer(&len_be_, sizeof len_be_),
|
||||
asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t) {
|
||||
if (ec) return;
|
||||
body_.assign(ntohl(len_be_), '\0');
|
||||
asio::async_read(socket_, asio::buffer(body_), // read exactly N bytes
|
||||
asio::bind_executor(strand_, [this, self](boost::system::error_code ec2, std::size_t) {
|
||||
if (ec2) return;
|
||||
handle_frame(body_); // dispatch on type byte
|
||||
read_frame(); // next frame
|
||||
}));
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
**Recurring side-task** (e.g. push every 250ms) running concurrently with the read loop — there is no detached coroutine to stop, so use a self-rescheduling timer and `cancel()` it to stop:
|
||||
|
||||
```cpp
|
||||
void schedule_tick(std::string symbol, std::shared_ptr<asio::steady_timer> t) {
|
||||
t->expires_after(std::chrono::milliseconds(250));
|
||||
auto self = shared_from_this();
|
||||
t->async_wait(asio::bind_executor(strand_,
|
||||
[this, self, symbol, t](boost::system::error_code ec) {
|
||||
if (ec) return; // cancelled on unsubscribe/close → stops
|
||||
send(make_tick(symbol)); // enqueue on the write queue
|
||||
schedule_tick(symbol, t); // reschedule itself
|
||||
}));
|
||||
}
|
||||
// start: keep one timer per subscription alive (e.g. in a map); stop: erase + t->cancel()
|
||||
```
|
||||
|
||||
### Build difference (stackful spawn only)
|
||||
|
||||
Callbacks need no change beyond the standard (drop `-fcoroutines`; it's only for C++20 `co_await`):
|
||||
```cmake
|
||||
set(CMAKE_CXX_STANDARD 11) # or 14 / 17
|
||||
set(CMAKE_CXX_EXTENSIONS OFF) # else CMake emits -std=gnu++NN, not literal -std=c++NN
|
||||
target_link_libraries(app PRIVATE Boost::headers Threads::Threads)
|
||||
target_compile_definitions(app PRIVATE BOOST_ERROR_CODE_HEADER_ONLY)
|
||||
```
|
||||
Stackful `spawn` additionally requires Boost.Coroutine (which uses Boost.Context) — **not header-only**:
|
||||
```cmake
|
||||
find_package(Boost REQUIRED COMPONENTS coroutine)
|
||||
target_link_libraries(app PRIVATE Boost::coroutine) # pulls in Boost.Context
|
||||
```
|
||||
> Standalone Asio's `spawn` also depends on Boost.Coroutine/Context — it drags Boost into an otherwise Boost-free build. If you want zero Boost, use the **callback** style.
|
||||
>
|
||||
> The 3-arg `spawn(ex, fn, token)` form needs **Boost ≥ 1.80** (older Boost has only `spawn(ex, fn)`). On old distros like Debian bookworm (Boost 1.74), the **callback** style compiles cleanly while stackful `spawn` does not — verified.
|
||||
@@ -0,0 +1,39 @@
|
||||
# SSL/TLS
|
||||
|
||||
Applies to every style — `ssl::stream<>` is a library feature, not a language one.
|
||||
|
||||
|
||||
```cpp
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace ssl = asio::ssl;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
asio::awaitable<void> tls_client(asio::io_context& io) {
|
||||
ssl::context ctx(ssl::context::tlsv13_client);
|
||||
ctx.set_default_verify_paths();
|
||||
|
||||
ssl::stream<tcp::socket> stream(io, ctx);
|
||||
|
||||
// Connect underlying TCP socket
|
||||
auto& sock = stream.lowest_layer();
|
||||
co_await sock.async_connect(endpoint);
|
||||
|
||||
// Set SNI hostname (required for most servers)
|
||||
SSL_set_tlsext_host_name(stream.native_handle(), "example.com");
|
||||
stream.set_verify_mode(ssl::verify_peer);
|
||||
stream.set_verify_callback(ssl::host_name_verification("example.com"));
|
||||
|
||||
// TLS handshake
|
||||
co_await stream.async_handshake(ssl::stream_base::client);
|
||||
|
||||
// Read/write as normal stream
|
||||
co_await async_write(stream, asio::buffer(request));
|
||||
co_await async_read_until(stream, response_buf, "\r\n");
|
||||
}
|
||||
```
|
||||
|
||||
**Critical:** SSL streams require strand-based synchronization for all async operations — no concurrent reads/writes without a strand.
|
||||
|
||||
Reference in New Issue
Block a user