From af9e9226c762c5dac82e04fa8115a7bd644adc4d Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sat, 17 Jan 2026 00:57:30 -0800 Subject: [PATCH 1/2] Antora documentation revision --- doc/antora.yml | 17 + doc/modules/ROOT/nav.adoc | 28 ++ .../ROOT/pages/advanced/composed-ops.adoc | 321 ++++++++++++++++++ .../ROOT/pages/advanced/executors.adoc | 276 +++++++++++++++ doc/modules/ROOT/pages/advanced/signals.adoc | 285 ++++++++++++++++ doc/modules/ROOT/pages/advanced/ssl.adoc | 292 ++++++++++++++++ .../async-model/completion-handlers.adoc | 220 ++++++++++++ .../async-model/initiating-functions.adoc | 185 ++++++++++ .../ROOT/pages/async-model/memory-model.adoc | 241 +++++++++++++ doc/modules/ROOT/pages/concepts/index.adoc | 282 +++++++++++++++ doc/modules/ROOT/pages/core/buffers.adoc | 270 +++++++++++++++ .../ROOT/pages/core/error-handling.adoc | 287 ++++++++++++++++ doc/modules/ROOT/pages/core/io-context.adoc | 233 +++++++++++++ doc/modules/ROOT/pages/core/streams.adoc | 314 +++++++++++++++++ doc/modules/ROOT/pages/index.adoc | 101 ++++++ .../ROOT/pages/networking/resolving.adoc | 248 ++++++++++++++ .../ROOT/pages/networking/tcp-client.adoc | 252 ++++++++++++++ .../ROOT/pages/networking/tcp-server.adoc | 257 ++++++++++++++ doc/modules/ROOT/pages/networking/udp.adoc | 256 ++++++++++++++ doc/modules/ROOT/pages/quick-start.adoc | 154 +++++++++ doc/modules/ROOT/pages/threading/strands.adoc | 275 +++++++++++++++ doc/modules/ROOT/pages/threading/threads.adoc | 264 ++++++++++++++ .../ROOT/pages/timers/async-timer.adoc | 169 +++++++++ .../ROOT/pages/timers/recurring-timer.adoc | 219 ++++++++++++ 24 files changed, 5446 insertions(+) create mode 100644 doc/antora.yml create mode 100644 doc/modules/ROOT/nav.adoc create mode 100644 doc/modules/ROOT/pages/advanced/composed-ops.adoc create mode 100644 doc/modules/ROOT/pages/advanced/executors.adoc create mode 100644 doc/modules/ROOT/pages/advanced/signals.adoc create mode 100644 doc/modules/ROOT/pages/advanced/ssl.adoc create mode 100644 doc/modules/ROOT/pages/async-model/completion-handlers.adoc create mode 100644 doc/modules/ROOT/pages/async-model/initiating-functions.adoc create mode 100644 doc/modules/ROOT/pages/async-model/memory-model.adoc create mode 100644 doc/modules/ROOT/pages/concepts/index.adoc create mode 100644 doc/modules/ROOT/pages/core/buffers.adoc create mode 100644 doc/modules/ROOT/pages/core/error-handling.adoc create mode 100644 doc/modules/ROOT/pages/core/io-context.adoc create mode 100644 doc/modules/ROOT/pages/core/streams.adoc create mode 100644 doc/modules/ROOT/pages/index.adoc create mode 100644 doc/modules/ROOT/pages/networking/resolving.adoc create mode 100644 doc/modules/ROOT/pages/networking/tcp-client.adoc create mode 100644 doc/modules/ROOT/pages/networking/tcp-server.adoc create mode 100644 doc/modules/ROOT/pages/networking/udp.adoc create mode 100644 doc/modules/ROOT/pages/quick-start.adoc create mode 100644 doc/modules/ROOT/pages/threading/strands.adoc create mode 100644 doc/modules/ROOT/pages/threading/threads.adoc create mode 100644 doc/modules/ROOT/pages/timers/async-timer.adoc create mode 100644 doc/modules/ROOT/pages/timers/recurring-timer.adoc diff --git a/doc/antora.yml b/doc/antora.yml new file mode 100644 index 000000000..901c15b4c --- /dev/null +++ b/doc/antora.yml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +name: asio +version: ~ +title: Boost.Asio +start_page: index.adoc +asciidoc: + attributes: + source-language: asciidoc@ + table-caption: false +nav: + - modules/ROOT/nav.adoc diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc new file mode 100644 index 000000000..0ad3df466 --- /dev/null +++ b/doc/modules/ROOT/nav.adoc @@ -0,0 +1,28 @@ +* xref:index.adoc[Introduction] +* xref:quick-start.adoc[Quick Start] +* Timers +** xref:timers/async-timer.adoc[Async Timer] +** xref:timers/recurring-timer.adoc[Recurring Timer] +* Networking +** xref:networking/tcp-client.adoc[TCP Client] +** xref:networking/tcp-server.adoc[TCP Server] +** xref:networking/udp.adoc[UDP] +** xref:networking/resolving.adoc[DNS Resolution] +* Core Concepts +** xref:core/io-context.adoc[The I/O Context] +** xref:core/buffers.adoc[Buffers] +** xref:core/error-handling.adoc[Error Handling] +** xref:core/streams.adoc[Reading and Writing] +* Threading +** xref:threading/threads.adoc[Multi-Threading] +** xref:threading/strands.adoc[Strands] +* Advanced +** xref:advanced/ssl.adoc[SSL/TLS] +** xref:advanced/signals.adoc[Signal Handling] +** xref:advanced/composed-ops.adoc[Composed Operations] +** xref:advanced/executors.adoc[Executors] +* The Async Model +** xref:async-model/initiating-functions.adoc[Initiating Functions] +** xref:async-model/completion-handlers.adoc[Completion Handlers] +** xref:async-model/memory-model.adoc[Memory and Lifetimes] +* xref:concepts/index.adoc[Concepts Reference] diff --git a/doc/modules/ROOT/pages/advanced/composed-ops.adoc b/doc/modules/ROOT/pages/advanced/composed-ops.adoc new file mode 100644 index 000000000..937c11612 --- /dev/null +++ b/doc/modules/ROOT/pages/advanced/composed-ops.adoc @@ -0,0 +1,321 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Composed Operations + +Learn how to write reusable asynchronous operations using coroutines. + +== What is a Composed Operation? + +A composed operation is an async operation built from other async operations. +Examples in Asio include: + +* `async_read` — Built from `async_read_some` +* `async_write` — Built from `async_write_some` +* `async_connect` (with range) — Built from `async_connect` calls + +You can write your own composed operations for reusable async logic. + +== Coroutine-Based Composition + +The simplest approach: return `awaitable`: + +[source,cpp] +---- +#include + +namespace asio = boost::asio; +using asio::awaitable; +using asio::use_awaitable; +using asio::ip::tcp; + +// A composed operation that reads a length-prefixed message +awaitable async_read_message(tcp::socket& socket) +{ + // Read 4-byte length header + uint32_t length; + co_await asio::async_read( + socket, + asio::buffer(&length, sizeof(length)), + use_awaitable); + + length = ntohl(length); // Network to host byte order + + // Read the message body + std::string message(length, '\0'); + co_await asio::async_read( + socket, + asio::buffer(message), + use_awaitable); + + co_return message; +} + +// Usage +awaitable client(tcp::socket& socket) +{ + std::string msg = co_await async_read_message(socket); + std::cout << "Received: " << msg << "\n"; +} +---- + +== Writing a Message + +[source,cpp] +---- +awaitable async_write_message(tcp::socket& socket, std::string_view message) +{ + // Write length header + uint32_t length = htonl(static_cast(message.size())); + co_await asio::async_write( + socket, + asio::buffer(&length, sizeof(length)), + use_awaitable); + + // Write message body + co_await asio::async_write( + socket, + asio::buffer(message), + use_awaitable); +} +---- + +== Operation with Timeout + +A composed operation that races against a timer: + +[source,cpp] +---- +#include + +using namespace asio::experimental::awaitable_operators; + +template +awaitable with_timeout( + awaitable operation, + std::chrono::steady_clock::duration timeout) +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor, timeout); + + auto result = co_await ( + std::move(operation) || timer.async_wait(use_awaitable) + ); + + if (result.index() == 1) + throw std::runtime_error("Operation timed out"); + + co_return std::get<0>(std::move(result)); +} + +// Usage +awaitable example(tcp::socket& socket) +{ + using namespace std::chrono_literals; + + auto msg = co_await with_timeout( + async_read_message(socket), + 5s); +} +---- + +== Retry Pattern + +A composed operation that retries on failure: + +[source,cpp] +---- +template +awaitable> with_retry( + Func&& func, + int max_attempts, + std::chrono::milliseconds delay) +{ + auto executor = co_await asio::this_coro::executor; + + for (int attempt = 1; attempt <= max_attempts; ++attempt) + { + try + { + co_return co_await func(); + } + catch (const boost::system::system_error& e) + { + if (attempt == max_attempts) + throw; + + std::cout << "Attempt " << attempt << " failed: " + << e.what() << ", retrying...\n"; + + asio::steady_timer timer(executor, delay); + co_await timer.async_wait(use_awaitable); + } + } + + throw std::logic_error("Unreachable"); +} + +// Usage +awaitable connect_with_retry(tcp::socket& socket, tcp::endpoint ep) +{ + co_await with_retry( + [&]() -> awaitable { + co_await socket.async_connect(ep, use_awaitable); + }, + 3, // max attempts + std::chrono::milliseconds(1000)); // delay between attempts +} +---- + +== Parallel Operations + +Run multiple operations concurrently: + +[source,cpp] +---- +#include + +using namespace asio::experimental::awaitable_operators; + +// Wait for both to complete +awaitable parallel_both() +{ + auto executor = co_await asio::this_coro::executor; + + tcp::socket socket1(executor); + tcp::socket socket2(executor); + + // Connect to both servers in parallel + co_await ( + socket1.async_connect(endpoint1, use_awaitable) + && socket2.async_connect(endpoint2, use_awaitable) + ); + + // Both are now connected +} + +// Wait for first to complete +awaitable parallel_race() +{ + auto executor = co_await asio::this_coro::executor; + + tcp::socket socket1(executor); + tcp::socket socket2(executor); + + // Connect to whichever responds first + auto result = co_await ( + socket1.async_connect(endpoint1, use_awaitable) + || socket2.async_connect(endpoint2, use_awaitable) + ); + + if (result.index() == 0) + std::cout << "socket1 connected first\n"; + else + std::cout << "socket2 connected first\n"; +} +---- + +== Generic Stream Operations + +Make operations work with any async stream: + +[source,cpp] +---- +template +awaitable async_read_line(AsyncStream& stream) +{ + std::string data; + + std::size_t n = co_await asio::async_read_until( + stream, + asio::dynamic_buffer(data), + '\n', + use_awaitable); + + std::string line = data.substr(0, n - 1); // Remove \n + co_return line; +} + +// Works with tcp::socket +awaitable use_with_socket(tcp::socket& socket) +{ + auto line = co_await async_read_line(socket); +} + +// Works with ssl::stream +awaitable use_with_ssl(asio::ssl::stream& stream) +{ + auto line = co_await async_read_line(stream); +} +---- + +== Error Handling in Composed Operations + +Propagate errors naturally with exceptions: + +[source,cpp] +---- +awaitable safe_read_message(tcp::socket& socket) +{ + try + { + co_return co_await async_read_message(socket); + } + catch (const boost::system::system_error& e) + { + if (e.code() == asio::error::eof) + co_return ""; // Return empty on EOF + throw; // Re-throw other errors + } +} +---- + +Or use error codes: + +[source,cpp] +---- +awaitable> +async_read_message_ec(tcp::socket& socket) +{ + uint32_t length; + + auto [ec1, n1] = co_await asio::async_read( + socket, + asio::buffer(&length, sizeof(length)), + asio::experimental::as_tuple(use_awaitable)); + + if (ec1) + co_return {ec1, ""}; + + length = ntohl(length); + std::string message(length, '\0'); + + auto [ec2, n2] = co_await asio::async_read( + socket, + asio::buffer(message), + asio::experimental::as_tuple(use_awaitable)); + + co_return {ec2, std::move(message)}; +} +---- + +== Best Practices + +1. **Return `awaitable`** — Makes the operation composable with other coroutines + +2. **Accept streams by reference** — Don't take ownership unless necessary + +3. **Use `use_awaitable`** — Consistent with the rest of the coroutine + +4. **Handle cancellation** — Operations should handle `operation_aborted` + +5. **Document lifetimes** — Be clear about what must remain valid + +== Next Steps + +* xref:async-model/initiating-functions.adoc[Initiating Functions] — Understand the async model +* xref:advanced/executors.adoc[Executors] — Control where operations run diff --git a/doc/modules/ROOT/pages/advanced/executors.adoc b/doc/modules/ROOT/pages/advanced/executors.adoc new file mode 100644 index 000000000..45c839a10 --- /dev/null +++ b/doc/modules/ROOT/pages/advanced/executors.adoc @@ -0,0 +1,276 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Executors + +Understand how executors control where and how operations run. + +== What is an Executor? + +An executor represents a context where work can be executed. It answers: + +* **Where** does work run? (Which thread pool? Which io_context?) +* **How** is work submitted? (Queued? Immediate? Deferred?) + +Every I/O object in Asio is associated with an executor, and completions are +delivered through that executor. + +== Getting Executors + +[source,cpp] +---- +#include + +namespace asio = boost::asio; + +// From io_context +asio::io_context ctx; +auto exec = ctx.get_executor(); + +// From an I/O object +asio::ip::tcp::socket socket(ctx); +auto exec2 = socket.get_executor(); + +// From a coroutine +asio::awaitable example() +{ + auto exec = co_await asio::this_coro::executor; +} +---- + +== Executor Types + +=== `io_context::executor_type` + +The executor for `io_context`. Work submitted to this executor runs when +`io_context::run()` is called. + +[source,cpp] +---- +asio::io_context ctx; +asio::io_context::executor_type exec = ctx.get_executor(); +---- + +=== `any_io_executor` + +A type-erased executor that can hold any I/O executor: + +[source,cpp] +---- +asio::any_io_executor exec = ctx.get_executor(); +---- + +Useful for: + +* Function parameters that accept any executor +* Runtime polymorphism + +=== `strand` + +An executor adapter that serializes execution: + +[source,cpp] +---- +auto strand = asio::make_strand(ctx); +---- + +See xref:threading/strands.adoc[Strands] for details. + +== Posting Work + +Submit work to an executor: + +[source,cpp] +---- +auto exec = ctx.get_executor(); + +// Post: queue for later execution +asio::post(exec, []() { + std::cout << "Posted work\n"; +}); + +// Dispatch: run immediately if possible, otherwise post +asio::dispatch(exec, []() { + std::cout << "Dispatched work\n"; +}); + +// Defer: like post, but may batch with other work +asio::defer(exec, []() { + std::cout << "Deferred work\n"; +}); +---- + +With coroutines: + +[source,cpp] +---- +asio::awaitable example() +{ + auto exec = co_await asio::this_coro::executor; + + // Switch to this executor (no-op if already there) + co_await asio::dispatch(exec, asio::use_awaitable); + + // Post and wait + co_await asio::post(exec, asio::use_awaitable); +} +---- + +== Associated Executor + +I/O objects and handlers have an associated executor: + +[source,cpp] +---- +// Socket's executor (from construction) +asio::ip::tcp::socket socket(ctx); +auto sock_exec = socket.get_executor(); + +// Timer with explicit executor +auto strand = asio::make_strand(ctx); +asio::steady_timer timer(strand); // Timer uses the strand +---- + +When an async operation completes, the completion handler runs on the +associated executor. + +== Binding Executors + +Force a handler to run on a specific executor: + +[source,cpp] +---- +auto strand = asio::make_strand(ctx); + +timer.async_wait( + asio::bind_executor(strand, [](auto ec) { + // This runs on the strand, regardless of timer's executor + })); +---- + +== Executor Properties + +Executors have queryable properties: + +[source,cpp] +---- +// Check if the executor requires blocking (it doesn't for io_context) +auto blocking = asio::query(exec, asio::execution::blocking); + +// Get the execution context +auto& ctx = asio::query(exec, asio::execution::context); +---- + +== System Executor + +A lightweight executor for simple cases: + +[source,cpp] +---- +asio::system_executor exec; + +asio::post(exec, []() { + // Runs on a system thread pool +}); +---- + +The system executor uses a global thread pool. It's useful when you don't +need a dedicated `io_context`. + +== Custom Executors + +You can create custom executors by implementing the executor concept. +A minimal executor needs: + +* `execute(f)` — Run a function +* Equality comparison +* Copy/move constructors + +[source,cpp] +---- +class my_executor +{ +public: + // Execute work + template + void execute(F&& f) const + { + // Run f somehow + f(); + } + + // Comparison + bool operator==(const my_executor&) const noexcept { return true; } + bool operator!=(const my_executor&) const noexcept { return false; } +}; +---- + +== Executor in Coroutines + +Inside a coroutine, get the executor with `this_coro::executor`: + +[source,cpp] +---- +asio::awaitable example() +{ + // Get current executor + auto exec = co_await asio::this_coro::executor; + + // Create objects using this executor + asio::steady_timer timer(exec, std::chrono::seconds(1)); + asio::ip::tcp::socket socket(exec); + + // Spawn child coroutines on the same executor + asio::co_spawn(exec, child_coroutine(), asio::detached); +} +---- + +== Thread Pool + +Asio provides a thread pool with its own executor: + +[source,cpp] +---- +asio::thread_pool pool(4); // 4 threads + +// Get executor +auto exec = pool.get_executor(); + +// Post work +asio::post(exec, []() { + // Runs on one of the pool's threads +}); + +// Wait for all work to complete +pool.join(); +---- + +Or use it with coroutines: + +[source,cpp] +---- +asio::thread_pool pool(4); + +asio::co_spawn(pool, my_coroutine(), asio::detached); + +pool.join(); +---- + +== Best Practices + +1. **Prefer `any_io_executor` in interfaces** — Allows flexibility + +2. **Get executor from `this_coro`** — Propagates the executor through the call chain + +3. **Use strands for synchronization** — Cleaner than mutexes for async code + +4. **Don't assume thread identity** — Unless using a strand, handlers may run on any thread + +== Next Steps + +* xref:threading/strands.adoc[Strands] — Serialize handler execution +* xref:threading/threads.adoc[Multi-Threading] — Run executors from multiple threads diff --git a/doc/modules/ROOT/pages/advanced/signals.adoc b/doc/modules/ROOT/pages/advanced/signals.adoc new file mode 100644 index 000000000..7efda3116 --- /dev/null +++ b/doc/modules/ROOT/pages/advanced/signals.adoc @@ -0,0 +1,285 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Signal Handling + +Learn how to handle operating system signals with Asio coroutines. + +== Basic Signal Handling + +Use `signal_set` to wait for signals asynchronously: + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::awaitable; +using asio::use_awaitable; + +awaitable signal_handler() +{ + auto executor = co_await asio::this_coro::executor; + + asio::signal_set signals(executor, SIGINT, SIGTERM); + + auto [ec, signum] = co_await signals.async_wait( + asio::experimental::as_tuple(use_awaitable)); + + if (!ec) + { + std::cout << "Received signal " << signum << "\n"; + } +} +---- + +== Graceful Shutdown Pattern + +The most common use: shut down cleanly on Ctrl+C: + +[source,cpp] +---- +int main() +{ + asio::io_context ctx; + + // Set up signal handler + asio::signal_set signals(ctx, SIGINT, SIGTERM); + signals.async_wait([&](auto ec, auto signum) { + if (!ec) + { + std::cout << "Shutting down...\n"; + ctx.stop(); + } + }); + + // Run your server + asio::co_spawn(ctx, server(), asio::detached); + + ctx.run(); + std::cout << "Shutdown complete\n"; +} +---- + +Or with coroutines: + +[source,cpp] +---- +awaitable run_with_shutdown() +{ + auto executor = co_await asio::this_coro::executor; + + asio::signal_set signals(executor, SIGINT, SIGTERM); + + // Start server in parallel with signal wait + // When signal arrives, stop accepting + + asio::ip::tcp::acceptor acceptor(executor, {asio::ip::tcp::v4(), 8080}); + + try + { + for (;;) + { + auto socket = co_await acceptor.async_accept(use_awaitable); + asio::co_spawn(executor, handle_session(std::move(socket)), + asio::detached); + } + } + catch (const boost::system::system_error& e) + { + if (e.code() == asio::error::operation_aborted) + std::cout << "Server stopped\n"; + else + throw; + } +} +---- + +== Multiple Signals + +Wait for any of several signals: + +[source,cpp] +---- +asio::signal_set signals(executor, SIGINT, SIGTERM, SIGHUP); + +auto [ec, signum] = co_await signals.async_wait( + asio::experimental::as_tuple(use_awaitable)); + +switch (signum) +{ + case SIGINT: + case SIGTERM: + std::cout << "Shutdown requested\n"; + break; + case SIGHUP: + std::cout << "Reload configuration\n"; + break; +} +---- + +== Adding and Removing Signals + +[source,cpp] +---- +asio::signal_set signals(executor); + +// Add signals +signals.add(SIGINT); +signals.add(SIGTERM); + +// Remove a signal +signals.remove(SIGTERM); + +// Clear all signals +signals.clear(); + +// Cancel pending wait +signals.cancel(); +---- + +== Repeated Signal Handling + +To handle signals repeatedly (not just once): + +[source,cpp] +---- +awaitable signal_loop() +{ + auto executor = co_await asio::this_coro::executor; + asio::signal_set signals(executor, SIGHUP); + + for (;;) + { + auto [ec, signum] = co_await signals.async_wait( + asio::experimental::as_tuple(use_awaitable)); + + if (ec == asio::error::operation_aborted) + break; + + std::cout << "Reloading configuration...\n"; + // Reload config here + } +} +---- + +== Common Signals + +[cols="1,2"] +|=== +| Signal | Typical Use + +| `SIGINT` | Interrupt (Ctrl+C) +| `SIGTERM` | Termination request +| `SIGHUP` | Hangup / reload config +| `SIGPIPE` | Broken pipe (usually ignored) +| `SIGUSR1` | User-defined +| `SIGUSR2` | User-defined +|=== + +== Ignoring SIGPIPE + +Writing to a closed socket generates `SIGPIPE`, which terminates the process +by default. Asio handles this on most platforms, but you may want to be explicit: + +[source,cpp] +---- +#include + +int main() +{ + // Ignore SIGPIPE (Asio returns an error instead) + std::signal(SIGPIPE, SIG_IGN); + + asio::io_context ctx; + // ... +} +---- + +== Windows Considerations + +On Windows, only these signals are supported: + +* `SIGINT` — Ctrl+C +* `SIGTERM` — Not typically used +* `SIGBREAK` — Ctrl+Break + +For console close events, use Windows-specific APIs. + +== Example: Graceful Server with Cleanup + +[source,cpp] +---- +class server +{ + asio::io_context& ctx_; + asio::ip::tcp::acceptor acceptor_; + asio::signal_set signals_; + std::vector> sessions_; + +public: + server(asio::io_context& ctx, unsigned short port) + : ctx_(ctx) + , acceptor_(ctx, {asio::ip::tcp::v4(), port}) + , signals_(ctx, SIGINT, SIGTERM) + { + // Start signal handler + signals_.async_wait([this](auto ec, auto) { + if (!ec) shutdown(); + }); + } + + awaitable run() + { + try + { + for (;;) + { + auto socket = co_await acceptor_.async_accept(use_awaitable); + auto sess = std::make_shared(std::move(socket)); + sessions_.push_back(sess); + asio::co_spawn(ctx_, sess->run(), asio::detached); + } + } + catch (const boost::system::system_error& e) + { + if (e.code() != asio::error::operation_aborted) + throw; + } + } + +private: + void shutdown() + { + // Stop accepting + acceptor_.close(); + + // Close all sessions + for (auto& sess : sessions_) + sess->close(); + + sessions_.clear(); + } +}; +---- + +== Common Mistakes + +**Not handling signals** — Without a handler, Ctrl+C terminates abruptly. +Add graceful shutdown. + +**Multiple signal_sets for same signal** — Only one can receive the signal. +Use a single `signal_set` and dispatch as needed. + +**Forgetting Windows differences** — SIGHUP doesn't exist on Windows. +Use conditional compilation. + +== Next Steps + +* xref:core/io-context.adoc[The I/O Context] — Stopping the event loop +* xref:networking/tcp-server.adoc[TCP Server] — Server shutdown patterns diff --git a/doc/modules/ROOT/pages/advanced/ssl.adoc b/doc/modules/ROOT/pages/advanced/ssl.adoc new file mode 100644 index 000000000..2c8900c07 --- /dev/null +++ b/doc/modules/ROOT/pages/advanced/ssl.adoc @@ -0,0 +1,292 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += SSL/TLS + +Learn how to secure connections with SSL/TLS using coroutines. + +== Prerequisites + +Asio's SSL support requires OpenSSL. Link with `-lssl -lcrypto` on Linux/macOS +or the equivalent on Windows. + +== SSL Context + +The `ssl::context` holds SSL configuration (certificates, protocols, etc.): + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +namespace ssl = asio::ssl; + +// Client context +ssl::context make_client_context() +{ + ssl::context ctx(ssl::context::tlsv13_client); + + // Use system's trusted CA certificates + ctx.set_default_verify_paths(); + + // Verify the server's certificate + ctx.set_verify_mode(ssl::verify_peer); + + return ctx; +} + +// Server context +ssl::context make_server_context() +{ + ssl::context ctx(ssl::context::tlsv13_server); + + // Load certificate and private key + ctx.use_certificate_chain_file("server.crt"); + ctx.use_private_key_file("server.key", ssl::context::pem); + + return ctx; +} +---- + +== SSL Client + +[source,cpp] +---- +using asio::awaitable; +using asio::use_awaitable; +using asio::ip::tcp; + +awaitable ssl_client(ssl::context& ssl_ctx) +{ + auto executor = co_await asio::this_coro::executor; + + // Create an SSL stream wrapping a TCP socket + ssl::stream stream(executor, ssl_ctx); + + // Resolve and connect + tcp::resolver resolver(executor); + auto endpoints = co_await resolver.async_resolve( + "www.example.com", "443", use_awaitable); + + co_await asio::async_connect( + stream.lowest_layer(), endpoints, use_awaitable); + + // Set SNI hostname (required by many servers) + SSL_set_tlsext_host_name(stream.native_handle(), "www.example.com"); + + // Perform TLS handshake + co_await stream.async_handshake(ssl::stream_base::client, use_awaitable); + + std::cout << "TLS handshake complete\n"; + + // Send a request + std::string request = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"; + co_await asio::async_write(stream, asio::buffer(request), use_awaitable); + + // Read the response + std::string response; + char buf[1024]; + + for (;;) + { + auto [ec, n] = co_await stream.async_read_some( + asio::buffer(buf), + asio::experimental::as_tuple(use_awaitable)); + + if (ec == asio::error::eof || + ec == asio::ssl::error::stream_truncated) + break; + if (ec) + throw boost::system::system_error(ec); + + response.append(buf, n); + } + + std::cout << "Received " << response.size() << " bytes\n"; + + // Graceful shutdown + boost::system::error_code ec; + co_await stream.async_shutdown(asio::redirect_error(use_awaitable, ec)); + // Shutdown errors are often expected (peer may close without shutdown) +} +---- + +== SSL Server + +[source,cpp] +---- +awaitable ssl_session(ssl::stream stream) +{ + try + { + // Perform TLS handshake + co_await stream.async_handshake( + ssl::stream_base::server, use_awaitable); + + // Handle the connection + char buf[1024]; + for (;;) + { + std::size_t n = co_await stream.async_read_some( + asio::buffer(buf), use_awaitable); + + co_await asio::async_write( + stream, asio::buffer(buf, n), use_awaitable); + } + } + catch (const std::exception& e) + { + std::cout << "Session error: " << e.what() << "\n"; + } +} + +awaitable ssl_server(ssl::context& ssl_ctx, unsigned short port) +{ + auto executor = co_await asio::this_coro::executor; + tcp::acceptor acceptor(executor, {tcp::v4(), port}); + + for (;;) + { + tcp::socket socket = co_await acceptor.async_accept(use_awaitable); + + // Wrap in SSL stream + ssl::stream stream(std::move(socket), ssl_ctx); + + asio::co_spawn(executor, ssl_session(std::move(stream)), asio::detached); + } +} +---- + +== Certificate Verification + +=== Verify Against System CA + +[source,cpp] +---- +ssl::context ctx(ssl::context::tlsv13_client); +ctx.set_default_verify_paths(); +ctx.set_verify_mode(ssl::verify_peer); +---- + +=== Custom CA Certificate + +[source,cpp] +---- +ctx.load_verify_file("ca.crt"); +---- + +=== Hostname Verification + +[source,cpp] +---- +// Before handshake +stream.set_verify_callback(ssl::host_name_verification("www.example.com")); +---- + +=== Skip Verification (Development Only!) + +[source,cpp] +---- +ctx.set_verify_mode(ssl::verify_none); // DON'T DO THIS IN PRODUCTION +---- + +== SSL Protocol Versions + +[source,cpp] +---- +// Specific version +ssl::context ctx(ssl::context::tlsv13); + +// Or use method flags +ssl::context ctx(ssl::context::sslv23); +ctx.set_options( + ssl::context::default_workarounds | + ssl::context::no_sslv2 | + ssl::context::no_sslv3 | + ssl::context::no_tlsv1 | + ssl::context::no_tlsv1_1 +); +// Now only TLS 1.2 and 1.3 are allowed +---- + +== Common SSL Errors + +[cols="1,2"] +|=== +| Error | Meaning + +| `ssl::error::stream_truncated` | Peer closed without shutdown (common) +| `certificate verify failed` | Certificate validation failed +| `unknown ca` | CA not in trust store +| `certificate has expired` | Certificate past its validity period +| `handshake failure` | Protocol negotiation failed +|=== + +== Error Handling + +[source,cpp] +---- +awaitable robust_ssl_client(ssl::stream& stream) +{ + try + { + co_await stream.async_handshake(ssl::stream_base::client, use_awaitable); + } + catch (const boost::system::system_error& e) + { + if (e.code().category() == asio::error::get_ssl_category()) + { + // SSL-specific error + std::cerr << "SSL error: " << e.what() << "\n"; + + // Get detailed OpenSSL error + auto ssl_err = ERR_get_error(); + std::cerr << "OpenSSL: " << ERR_error_string(ssl_err, nullptr) << "\n"; + } + throw; + } +} +---- + +== Layered Streams + +`ssl::stream` wraps any stream. The underlying stream is accessible via: + +* `lowest_layer()` — The bottom-most layer (usually TCP socket) +* `next_layer()` — The next layer down (same as lowest_layer for two layers) + +[source,cpp] +---- +ssl::stream stream(executor, ctx); + +// Access the TCP socket for connect, close, etc. +stream.lowest_layer().connect(endpoint); +stream.lowest_layer().close(); + +// Read/write go through SSL +co_await asio::async_write(stream, asio::buffer(data), use_awaitable); +---- + +== Common Mistakes + +**Forgetting SNI** — Many servers require SNI to select the right certificate. +Always set it with `SSL_set_tlsext_host_name`. + +**Ignoring handshake errors** — A failed handshake means no encryption. +Don't continue on handshake failure. + +**Not verifying certificates** — Without verification, you're vulnerable to +man-in-the-middle attacks. + +**Treating `stream_truncated` as fatal** — Many servers close without a proper +SSL shutdown. It's often safe to ignore. + +== Next Steps + +* xref:networking/tcp-client.adoc[TCP Client] — Non-SSL networking +* xref:core/error-handling.adoc[Error Handling] — Handle SSL errors diff --git a/doc/modules/ROOT/pages/async-model/completion-handlers.adoc b/doc/modules/ROOT/pages/async-model/completion-handlers.adoc new file mode 100644 index 000000000..4195e6dfc --- /dev/null +++ b/doc/modules/ROOT/pages/async-model/completion-handlers.adoc @@ -0,0 +1,220 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Completion Handlers + +Understand how async operation results are delivered. + +NOTE: This section explains the handler model for background. With coroutines +and `use_awaitable`, you rarely interact with handlers directly—`co_await` +handles it for you. + +== What is a Completion Handler? + +A *completion handler* is a function object that receives the result of an +async operation. When the operation completes, Asio invokes the handler with +the result. + +[source,cpp] +---- +// With callbacks (for comparison) +socket.async_read_some(buffer, + [](boost::system::error_code ec, std::size_t n) { + // This is the completion handler + if (!ec) + std::cout << "Read " << n << " bytes\n"; + }); +---- + +With coroutines, the completion handler is hidden inside `use_awaitable`: + +[source,cpp] +---- +// The awaitable machinery handles the completion internally +std::size_t n = co_await socket.async_read_some(buffer, use_awaitable); +---- + +== Handler Guarantees + +Asio provides these guarantees for completion handlers: + +=== 1. Invoked At Most Once + +A completion handler is invoked *exactly once* if the operation completes, +and *zero times* if the operation is destroyed before completion (rare). + +=== 2. Invoked Asynchronously + +The handler is never invoked directly from the initiating function. +It's always invoked later, through the executor: + +[source,cpp] +---- +socket.async_read_some(buffer, handler); +// handler is NOT called yet +// ...code here runs before handler... + +ctx.run(); // handler is called from here +---- + +This means you can safely set up state after calling the initiating function. + +=== 3. Invoked by the Associated Executor + +The handler runs on its associated executor. For coroutines with `use_awaitable`, +this is the executor from `this_coro::executor`. + +== Handler Signatures + +Each async operation defines the handler signature: + +[source,cpp] +---- +// Timer wait: void(error_code) +timer.async_wait([](boost::system::error_code ec) { + // ... +}); + +// Socket read: void(error_code, size_t) +socket.async_read_some(buffer, + [](boost::system::error_code ec, std::size_t n) { + // ... + }); + +// Accept: void(error_code, socket) +acceptor.async_accept( + [](boost::system::error_code ec, tcp::socket socket) { + // ... + }); +---- + +== Move-Only Handlers + +Handlers can be move-only (no copy required): + +[source,cpp] +---- +auto ptr = std::make_unique(); + +socket.async_read_some(buffer, + [p = std::move(ptr)](auto ec, auto n) { + // p is valid here + }); +---- + +== Associated Characteristics + +Handlers can have associated characteristics: + +=== Associated Executor + +Where the handler runs: + +[source,cpp] +---- +auto strand = asio::make_strand(ctx); + +// Handler runs on the strand +socket.async_read_some(buffer, + asio::bind_executor(strand, [](auto ec, auto n) { + // Runs on strand + })); +---- + +With coroutines, the executor comes from `this_coro::executor`. + +=== Associated Allocator + +Memory allocation for async operations: + +[source,cpp] +---- +// Provide a custom allocator for handler memory +socket.async_read_some(buffer, + asio::bind_allocator(my_allocator, [](auto ec, auto n) { + // ... + })); +---- + +This is an advanced optimization for reducing allocations. + +== Handler Invocation Order + +When multiple operations complete, handlers are invoked in an unspecified order +unless you use a strand to serialize them. + +[source,cpp] +---- +// These may complete in any order +socket.async_read_some(buffer1, handler1); +socket.async_write_some(buffer2, handler2); + +// With strand, they won't run concurrently +auto strand = asio::make_strand(ctx); +socket.async_read_some(buffer1, asio::bind_executor(strand, handler1)); +socket.async_write_some(buffer2, asio::bind_executor(strand, handler2)); +---- + +== The Coroutine Advantage + +With `use_awaitable`, you don't manage handlers directly: + +[source,cpp] +---- +// No explicit handlers needed +awaitable example(tcp::socket& socket) +{ + char buf[1024]; + + // Internally, use_awaitable creates a handler that resumes the coroutine + std::size_t n = co_await socket.async_read_some( + asio::buffer(buf), use_awaitable); + + // When the read completes, the coroutine resumes here + std::cout << "Read " << n << " bytes\n"; +} +---- + +The machinery: + +1. `use_awaitable` creates a special handler +2. When awaited, the coroutine suspends +3. When the operation completes, the handler resumes the coroutine +4. Execution continues after the `co_await` + +== Exception Propagation + +With coroutines, errors throw exceptions by default: + +[source,cpp] +---- +try +{ + co_await socket.async_connect(endpoint, use_awaitable); +} +catch (const boost::system::system_error& e) +{ + // e.code() contains the error +} +---- + +Use `as_tuple` to get error codes instead: + +[source,cpp] +---- +auto [ec] = co_await socket.async_connect( + endpoint, + asio::experimental::as_tuple(use_awaitable)); + +if (ec) + std::cout << "Error: " << ec.message() << "\n"; +---- + +== Next Steps + +* xref:async-model/memory-model.adoc[Memory and Lifetimes] — Resource guarantees +* xref:async-model/initiating-functions.adoc[Initiating Functions] — Starting operations diff --git a/doc/modules/ROOT/pages/async-model/initiating-functions.adoc b/doc/modules/ROOT/pages/async-model/initiating-functions.adoc new file mode 100644 index 000000000..3fe8b969e --- /dev/null +++ b/doc/modules/ROOT/pages/async-model/initiating-functions.adoc @@ -0,0 +1,185 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Initiating Functions + +Understand the functions that start asynchronous operations. + +== What is an Initiating Function? + +An *initiating function* is a function that starts an asynchronous operation. +When you call it, the operation begins—but it doesn't necessarily complete +before the function returns. + +Examples: + +* `socket.async_connect(endpoint, token)` +* `socket.async_read_some(buffer, token)` +* `timer.async_wait(token)` +* `async_read(stream, buffer, token)` + +== Anatomy of an Initiating Function + +[source,cpp] +---- +socket.async_read_some(buffer, completion_token); +// ~~~~~~~~~~~~~~~ ~~~~~~ ~~~~~~~~~~~~~~~~ +// Initiating func Args Completion token +---- + +**Initiating function**: The function that starts the operation. + +**Arguments**: Operation-specific parameters (endpoint, buffer, etc.). + +**Completion token**: Determines how the result is delivered. + +With `use_awaitable`, the token makes the function return an `awaitable`: + +[source,cpp] +---- +awaitable a = socket.async_read_some(buffer, use_awaitable); +std::size_t n = co_await a; +---- + +== What Happens When You Call an Initiating Function? + +1. **The operation is initiated** — The OS is asked to perform the work +2. **The function returns** — Control returns to the caller immediately +3. **Time passes** — The OS does the work in the background +4. **The operation completes** — The OS signals completion +5. **The completion handler is invoked** — Your code receives the result + +With coroutines, steps 3-5 happen behind the `co_await`. + +== Immediate vs Deferred Completion + +Some operations may complete immediately. For example, reading from a socket +that already has data buffered. Asio handles this correctly—the operation +still goes through the event loop to invoke the completion handler. + +[source,cpp] +---- +// Even if data is ready, the coroutine still suspends +// and resumes via the event loop +std::size_t n = co_await socket.async_read_some(buffer, use_awaitable); +---- + +This consistency is important: you can rely on the completion handler always +being invoked asynchronously (not inline from the initiating function). + +== Return Values + +Initiating functions with `use_awaitable` return `awaitable` where `T` is +the result type: + +[cols="1,2"] +|=== +| Operation | Return Type + +| `async_wait` | `awaitable` +| `async_read_some` | `awaitable` +| `async_accept` | `awaitable` +| `async_resolve` | `awaitable` +|=== + +With `as_tuple`, error codes are included: + +[source,cpp] +---- +auto [ec, n] = co_await socket.async_read_some( + buffer, + asio::experimental::as_tuple(use_awaitable)); +// Returns awaitable> +---- + +== Completion Signatures + +Each initiating function documents its *completion signature*—the parameters +passed to the completion handler. For example: + +[source,cpp] +---- +// async_read_some completion signature: +// void(error_code ec, std::size_t bytes_transferred) + +// async_connect completion signature: +// void(error_code ec) + +// async_accept completion signature: +// void(error_code ec, tcp::socket socket) +---- + +With `use_awaitable`: + +* `void(error_code)` → `awaitable` (throws on error) +* `void(error_code, T)` → `awaitable` (throws on error) + +== Cancellation + +Initiating functions may be cancelled: + +* Close the I/O object (e.g., `socket.close()`) +* Call `cancel()` (e.g., `timer.cancel()`) +* Reset a timer's expiry time + +Cancelled operations complete with `asio::error::operation_aborted`: + +[source,cpp] +---- +try +{ + co_await timer.async_wait(use_awaitable); +} +catch (const boost::system::system_error& e) +{ + if (e.code() == asio::error::operation_aborted) + std::cout << "Timer was cancelled\n"; +} +---- + +== Overlapping Operations + +On the same object, you can typically have: + +* Multiple reads outstanding? Usually **no** +* Multiple writes outstanding? Usually **no** +* A read and a write? **Yes**, these can overlap + +[source,cpp] +---- +// WRONG: Two reads on same socket +co_await socket.async_read_some(buf1, use_awaitable); // Started +co_await socket.async_read_some(buf2, use_awaitable); // Don't do this! + +// OK: Read and write can overlap +auto read_task = socket.async_read_some(buf1, use_awaitable); +auto write_task = async_write(socket, buf2, use_awaitable); +co_await (read_task && write_task); // Both in parallel +---- + +== The Completion Token Mechanism + +The final parameter to an initiating function is the *completion token*. +It determines how results are delivered: + +[cols="1,2"] +|=== +| Token | Behavior + +| `use_awaitable` | Return `awaitable` for `co_await` +| `detached` | Discard the result, don't wait +| `as_tuple(token)` | Include error code in result tuple +| `redirect_error(token, ec)` | Store error in `ec` instead of throwing +|=== + +This is one of Asio's most powerful features: the same operation works with +different async styles without changing the operation itself. + +== Next Steps + +* xref:async-model/completion-handlers.adoc[Completion Handlers] — How completions are delivered +* xref:async-model/memory-model.adoc[Memory and Lifetimes] — Resource management guarantees diff --git a/doc/modules/ROOT/pages/async-model/memory-model.adoc b/doc/modules/ROOT/pages/async-model/memory-model.adoc new file mode 100644 index 000000000..ce3ec697b --- /dev/null +++ b/doc/modules/ROOT/pages/async-model/memory-model.adoc @@ -0,0 +1,241 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Memory and Lifetimes + +Understand Asio's resource management guarantees. + +== The Deallocate-Before-Invoke Guarantee + +This is one of Asio's most important guarantees: + +[quote] +If an asynchronous operation requires a temporary resource (such as memory, +a file descriptor, or a thread), this resource is released before calling +the completion handler. + +Why does this matter? Consider a chain of operations: + +[source,cpp] +---- +awaitable chain() +{ + co_await async_op1(); // Uses internal buffer + co_await async_op2(); // Uses internal buffer + co_await async_op3(); // Uses internal buffer +} +---- + +Without this guarantee, all three internal buffers might exist simultaneously, +tripling memory usage. With the guarantee, each operation releases its +resources before the next operation starts. + +== Practical Impact + +=== Memory Efficiency + +[source,cpp] +---- +awaitable echo_loop(tcp::socket& socket) +{ + char buf[1024]; + for (;;) + { + // Op 1: internal resources allocated + std::size_t n = co_await socket.async_read_some( + asio::buffer(buf), use_awaitable); + // Op 1: internal resources released before we get here + + // Op 2: can reuse the same memory + co_await asio::async_write( + socket, asio::buffer(buf, n), use_awaitable); + // Op 2: internal resources released + } +} +---- + +Each iteration uses the same peak memory, not accumulating. + +=== Safe Handler Chaining + +The guarantee enables patterns like: + +[source,cpp] +---- +awaitable infinite_accept(tcp::acceptor& acceptor) +{ + for (;;) + { + // Accept completes, releases internal state + auto socket = co_await acceptor.async_accept(use_awaitable); + + // Now safe to do the next accept without resource buildup + co_spawn(socket.get_executor(), + handle_session(std::move(socket)), + detached); + } +} +---- + +== Buffer Lifetimes + +**Critical**: You must keep buffers alive until the operation completes. + +[source,cpp] +---- +// WRONG: buffer destroyed before operation completes +awaitable bad() +{ + auto socket = /* ... */; + { + std::string data = "Hello"; + socket.async_write_some(asio::buffer(data), use_awaitable); + } // data destroyed here! + + // ... operation still in progress, buffer is gone +} + +// CORRECT: buffer outlives operation +awaitable good() +{ + auto socket = /* ... */; + std::string data = "Hello"; + co_await asio::async_write(socket, asio::buffer(data), use_awaitable); + // data still valid, operation is complete +} +---- + +With `co_await`, this is natural: the local variable stays alive until the +operation completes. + +== I/O Object Lifetimes + +Keep I/O objects alive while operations are pending: + +[source,cpp] +---- +// WRONG: socket destroyed while operation pending +void bad() +{ + auto socket = std::make_unique(ctx); + socket->async_read_some(buffer, handler); + socket.reset(); // Destroys socket while read is pending! +} + +// CORRECT: socket outlives operation +awaitable good() +{ + tcp::socket socket(co_await asio::this_coro::executor); + co_await socket.async_connect(endpoint, use_awaitable); + co_await socket.async_read_some(buffer, use_awaitable); + // socket destroyed here, after operations complete +} +---- + +For servers with multiple sessions, use `shared_ptr`: + +[source,cpp] +---- +class session : public std::enable_shared_from_this +{ + tcp::socket socket_; + +public: + awaitable run() + { + // Keep self alive while running + auto self = shared_from_this(); + + co_await do_read(); + co_await do_write(); + } +}; +---- + +== Executor Lifetime + +The executor (and its underlying `io_context`) must outlive all operations: + +[source,cpp] +---- +// WRONG: context destroyed before operation completes +void bad() +{ + asio::io_context ctx; + tcp::socket socket(ctx); + socket.async_connect(endpoint, handler); +} // ctx destroyed before run() called! + +// CORRECT: context outlives operations +void good() +{ + asio::io_context ctx; + tcp::socket socket(ctx); + socket.async_connect(endpoint, handler); + ctx.run(); // Keeps ctx alive until complete +} +---- + +== Cancellation and Resources + +When an operation is cancelled, resources are still released before the +handler is invoked: + +[source,cpp] +---- +awaitable cancellable() +{ + tcp::socket socket(co_await asio::this_coro::executor); + + try + { + co_await socket.async_connect(endpoint, use_awaitable); + } + catch (const boost::system::system_error& e) + { + if (e.code() == asio::error::operation_aborted) + { + // Resources are already released + // Safe to start new operations + } + } +} +---- + +== The Cost of Safety + +The deallocate-before-invoke guarantee has minimal cost: + +* Operations already need to complete before invoking the handler +* The deallocation happens at a natural point in the flow +* Modern allocators make this efficient + +== Debugging Lifetime Issues + +Enable buffer debugging: + +[source,cpp] +---- +#define BOOST_ASIO_ENABLE_BUFFER_DEBUGGING +#include +---- + +This adds runtime checks that detect use of invalidated buffers. + +== Summary + +1. **Internal resources released before handler** — Enables efficient chaining +2. **Your buffers must outlive operations** — With `co_await`, this is natural +3. **I/O objects must outlive pending operations** — Use `shared_ptr` when needed +4. **Executors must outlive operations** — Don't destroy `io_context` early + +These guarantees make async code composable and memory-efficient. + +== Next Steps + +* xref:core/buffers.adoc[Buffers] — Buffer types and usage +* xref:async-model/initiating-functions.adoc[Initiating Functions] — Starting operations diff --git a/doc/modules/ROOT/pages/concepts/index.adoc b/doc/modules/ROOT/pages/concepts/index.adoc new file mode 100644 index 000000000..bf857167e --- /dev/null +++ b/doc/modules/ROOT/pages/concepts/index.adoc @@ -0,0 +1,282 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Concepts Reference + +Named requirements for Asio types and functions. + +== Stream Concepts + +=== AsyncReadStream + +A type that supports async read operations. + +**Requirements:** + +* `async_read_some(buffers, token)` — Initiates a read operation + +**Models:** + +* `tcp::socket` +* `ssl::stream` +* `buffered_read_stream<>` + +**Usage with coroutines:** + +[source,cpp] +---- +template +awaitable read_some(AsyncReadStream& stream) +{ + char buf[1024]; + co_return co_await stream.async_read_some( + asio::buffer(buf), use_awaitable); +} +---- + +=== AsyncWriteStream + +A type that supports async write operations. + +**Requirements:** + +* `async_write_some(buffers, token)` — Initiates a write operation + +**Models:** + +* `tcp::socket` +* `ssl::stream` +* `buffered_write_stream<>` + +=== AsyncStream + +A type that is both AsyncReadStream and AsyncWriteStream. + +== Buffer Concepts + +=== ConstBufferSequence + +A sequence of read-only buffers. + +**Requirements:** + +* Iterable with `begin()` / `end()` +* Iterator dereferences to something convertible to `const_buffer` + +**Models:** + +* `const_buffer` +* `std::array` +* `std::vector` +* Result of `buffer("string")` + +=== MutableBufferSequence + +A sequence of writable buffers. + +**Requirements:** + +* Iterable with `begin()` / `end()` +* Iterator dereferences to something convertible to `mutable_buffer` + +**Models:** + +* `mutable_buffer` +* `std::array` +* `std::vector` +* Result of `buffer(array)` + +=== DynamicBuffer + +A buffer that can grow to accommodate data. + +**Requirements:** + +* `size()` — Current size +* `max_size()` — Maximum allowed size +* `capacity()` — Current capacity +* `data()` — Get const buffer sequence +* `prepare(n)` — Get mutable buffer for writing n bytes +* `commit(n)` — Mark n bytes as written +* `consume(n)` — Remove n bytes from front + +**Models:** + +* `dynamic_buffer(std::string&)` +* `dynamic_buffer(std::vector&)` +* `streambuf` + +== Protocol Concepts + +=== Protocol + +Defines a communication protocol (TCP, UDP, etc.). + +**Requirements:** + +* `endpoint` — Nested endpoint type +* `socket` — Nested socket type (or via `basic_socket`) + +**Models:** + +* `tcp` +* `udp` +* `icmp` +* `local::stream_protocol` +* `local::datagram_protocol` + +=== Endpoint + +An endpoint in a protocol (address + port). + +**Requirements:** + +* `protocol()` — Return the protocol +* Default constructible +* Copyable + +**Models:** + +* `tcp::endpoint` +* `udp::endpoint` +* `local::stream_protocol::endpoint` + +=== InternetProtocol + +A Protocol for IP-based communication. + +**Requirements:** + +* All Protocol requirements +* `v4()` — Return IPv4 variant +* `v6()` — Return IPv6 variant +* `resolver` — Nested resolver type + +**Models:** + +* `tcp` +* `udp` +* `icmp` + +== Executor Concepts + +=== Executor + +A handle to an execution context. + +**Requirements:** + +* `execute(f)` — Execute a function +* Equality comparable +* Copyable + +**Models:** + +* `io_context::executor_type` +* `thread_pool::executor_type` +* `strand` +* `any_io_executor` + +=== ExecutionContext + +A context that can create executors. + +**Requirements:** + +* `get_executor()` — Return an executor + +**Models:** + +* `io_context` +* `thread_pool` +* `system_context` + +== Completion Token Concepts + +=== CompletionToken + +Determines how async operation results are delivered. + +**Models:** + +* `use_awaitable_t` — Return `awaitable` for coroutines +* `detached_t` — Discard result +* Function objects — Called with result + +=== CompletionCondition + +Controls when composed read/write operations complete. + +**Requirements:** + +* `operator()(error_code, bytes_transferred)` — Return bytes remaining + +**Models:** + +* `transfer_all()` — Until buffer full/empty +* `transfer_at_least(n)` — Until n bytes transferred +* `transfer_exactly(n)` — Exactly n bytes + +== Service Concepts + +=== Service + +A singleton attached to an execution context. + +**Requirements:** + +* `key_type` — Nested type identifying the service +* Constructor taking `execution_context&` + +Services allow extending `io_context` with custom functionality. + +== Socket Option Concepts + +=== GettableSocketOption + +An option that can be read from a socket. + +**Models:** + +* `socket_base::reuse_address` +* `tcp::no_delay` +* `socket_base::receive_buffer_size` + +=== SettableSocketOption + +An option that can be set on a socket. + +**Models:** + +Same as GettableSocketOption—most options are both. + +== Usage Notes + +These concepts are informal—they're documented requirements, not C++20 +`concept` declarations. Asio uses SFINAE and `enable_if` for constraint checks. + +When writing generic code: + +[source,cpp] +---- +// Works with any AsyncReadStream +template +awaitable read_and_process(Stream& stream) +{ + char buf[1024]; + auto n = co_await stream.async_read_some( + asio::buffer(buf), use_awaitable); + // process buf[0..n) +} +---- + +For full details on each concept, see the Boost.Asio reference documentation. + +== Next Steps + +* xref:index.adoc[Introduction] — Library overview +* xref:async-model/initiating-functions.adoc[Initiating Functions] — How async operations start diff --git a/doc/modules/ROOT/pages/core/buffers.adoc b/doc/modules/ROOT/pages/core/buffers.adoc new file mode 100644 index 000000000..395b31242 --- /dev/null +++ b/doc/modules/ROOT/pages/core/buffers.adoc @@ -0,0 +1,270 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Buffers + +Learn how Asio represents memory for I/O operations. + +== The `buffer()` Function + +The `buffer()` function creates a buffer view from various types: + +[source,cpp] +---- +#include + +namespace asio = boost::asio; + +void buffer_examples() +{ + // From a char array + char arr[1024]; + auto b1 = asio::buffer(arr); // Entire array + auto b2 = asio::buffer(arr, 512); // First 512 bytes + + // From std::array + std::array std_arr; + auto b3 = asio::buffer(std_arr); + + // From std::vector + std::vector vec(1024); + auto b4 = asio::buffer(vec); + + // From std::string (read-only) + std::string str = "Hello"; + auto b5 = asio::buffer(str); // const_buffer + + // From std::string_view (read-only) + auto b6 = asio::buffer(std::string_view("Hello")); +} +---- + +== Mutable vs Const Buffers + +Asio distinguishes between buffers you can write to and buffers you can only read: + +[cols="1,2"] +|=== +| Type | Use + +| `mutable_buffer` | For receiving data (`async_read`, `async_receive`) +| `const_buffer` | For sending data (`async_write`, `async_send`) +|=== + +`buffer()` returns the appropriate type based on the source: + +[source,cpp] +---- +char arr[1024]; +auto mb = asio::buffer(arr); // mutable_buffer + +const char* data = "Hello"; +auto cb = asio::buffer(data, 5); // const_buffer + +std::string str = "Hello"; +auto cb2 = asio::buffer(str); // const_buffer (string is treated as const) +---- + +== Buffer Sequences + +Many operations accept a sequence of buffers (scatter/gather I/O): + +[source,cpp] +---- +asio::awaitable scatter_gather_example(asio::ip::tcp::socket& socket) +{ + // Gather write: send multiple buffers in one operation + std::string header = "HTTP/1.1 200 OK\r\n\r\n"; + std::string body = "Hello, World!"; + + std::array buffers = { + asio::buffer(header), + asio::buffer(body) + }; + + co_await asio::async_write(socket, buffers, asio::use_awaitable); + + // Scatter read: receive into multiple buffers + char header_buf[100]; + char body_buf[1000]; + + std::array recv_buffers = { + asio::buffer(header_buf), + asio::buffer(body_buf) + }; + + co_await socket.async_read_some(recv_buffers, asio::use_awaitable); +} +---- + +== Dynamic Buffers + +For growing buffers (useful with `async_read_until`): + +[source,cpp] +---- +asio::awaitable dynamic_buffer_example(asio::ip::tcp::socket& socket) +{ + // Using std::string as a dynamic buffer + std::string data; + std::size_t n = co_await asio::async_read_until( + socket, + asio::dynamic_buffer(data), + '\n', + asio::use_awaitable); + + std::cout << "Received line: " << data.substr(0, n); + + // Or with std::vector + std::vector vec_data; + co_await asio::async_read_until( + socket, + asio::dynamic_buffer(vec_data), + "\r\n\r\n", + asio::use_awaitable); +} +---- + +== `streambuf` + +`asio::streambuf` provides iostream-compatible buffering: + +[source,cpp] +---- +asio::awaitable streambuf_example(asio::ip::tcp::socket& socket) +{ + asio::streambuf buf; + + // Read until newline + co_await asio::async_read_until(socket, buf, '\n', asio::use_awaitable); + + // Extract the data + std::istream is(&buf); + std::string line; + std::getline(is, line); + + std::cout << "Line: " << line << "\n"; +} +---- + +== Buffer Size and Consumption + +Get buffer size: + +[source,cpp] +---- +auto buf = asio::buffer(data, 100); +std::size_t size = buf.size(); // 100 +---- + +After a partial read, you often need to work with a subset: + +[source,cpp] +---- +char data[1024]; +auto buf = asio::buffer(data); + +// Read some data +std::size_t n = co_await socket.async_read_some(buf, asio::use_awaitable); + +// Create a buffer for just the received data +auto received = asio::buffer(data, n); + +// Or advance past consumed data +buf += n; // Now points to data[n] with size 1024-n +---- + +== Buffer Lifetime + +**Critical:** The buffer must remain valid until the async operation completes. + +[source,cpp] +---- +// WRONG: string destroyed before operation completes +asio::awaitable bad_example(asio::ip::tcp::socket& socket) +{ + { + std::string msg = "Hello"; + socket.async_write_some(asio::buffer(msg), ...); // BAD! + } // msg destroyed here, but write may still be in progress +} + +// CORRECT: keep the buffer alive +asio::awaitable good_example(asio::ip::tcp::socket& socket) +{ + std::string msg = "Hello"; + co_await asio::async_write(socket, asio::buffer(msg), asio::use_awaitable); + // msg still valid here, operation is complete +} +---- + +For operations that outlive the current scope, store the buffer in a member +variable or use `std::shared_ptr`. + +== Buffer Debugging + +Enable buffer debugging to catch lifetime issues: + +[source,cpp] +---- +// Define before including Asio headers +#define BOOST_ASIO_ENABLE_BUFFER_DEBUGGING +#include +---- + +This adds runtime checks that detect use of invalidated buffers. + +== Common Patterns + +=== Fixed-size read buffer + +[source,cpp] +---- +char buf[8192]; +for (;;) +{ + std::size_t n = co_await socket.async_read_some( + asio::buffer(buf), asio::use_awaitable); + // Process buf[0..n) +} +---- + +=== Exact-size read + +[source,cpp] +---- +char buf[100]; +co_await asio::async_read( + socket, asio::buffer(buf), asio::use_awaitable); +// All 100 bytes are now filled +---- + +=== Read until delimiter + +[source,cpp] +---- +std::string data; +std::size_t n = co_await asio::async_read_until( + socket, asio::dynamic_buffer(data), '\n', asio::use_awaitable); +// data[0..n) includes the delimiter +---- + +== Common Mistakes + +**Buffer too small** — Reading into a small buffer silently truncates data. +Size your buffers appropriately. + +**Buffer goes out of scope** — The most common bug. Always ensure buffers +outlive the async operation. + +**Modifying buffer during operation** — Don't modify the buffer while an +async operation is using it. + +== Next Steps + +* xref:core/streams.adoc[Reading and Writing] — Use buffers with I/O +* xref:core/error-handling.adoc[Error Handling] — Handle partial reads/writes diff --git a/doc/modules/ROOT/pages/core/error-handling.adoc b/doc/modules/ROOT/pages/core/error-handling.adoc new file mode 100644 index 000000000..3b8cd287a --- /dev/null +++ b/doc/modules/ROOT/pages/core/error-handling.adoc @@ -0,0 +1,287 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Error Handling + +Learn how errors are reported in Asio coroutines. + +== Default: Exceptions + +By default, errors in `co_await` expressions throw `boost::system::system_error`: + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::awaitable; +using asio::use_awaitable; + +awaitable connect_with_exceptions() +{ + auto executor = co_await asio::this_coro::executor; + asio::ip::tcp::socket socket(executor); + + try + { + asio::ip::tcp::endpoint ep(asio::ip::make_address("127.0.0.1"), 12345); + co_await socket.async_connect(ep, use_awaitable); + std::cout << "Connected!\n"; + } + catch (const boost::system::system_error& e) + { + std::cout << "Error: " << e.what() << "\n"; + std::cout << "Code: " << e.code() << "\n"; + // e.code() == asio::error::connection_refused, etc. + } +} +---- + +== Using Error Codes + +If you prefer error codes over exceptions, use `redirect_error`: + +[source,cpp] +---- +awaitable connect_with_error_codes() +{ + auto executor = co_await asio::this_coro::executor; + asio::ip::tcp::socket socket(executor); + + asio::ip::tcp::endpoint ep(asio::ip::make_address("127.0.0.1"), 12345); + + boost::system::error_code ec; + co_await socket.async_connect(ep, asio::redirect_error(use_awaitable, ec)); + + if (ec) + { + std::cout << "Error: " << ec.message() << "\n"; + co_return; + } + + std::cout << "Connected!\n"; +} +---- + +== Using `as_tuple` + +`as_tuple` returns all results including the error code as a tuple: + +[source,cpp] +---- +#include + +awaitable read_with_as_tuple() +{ + auto executor = co_await asio::this_coro::executor; + asio::ip::tcp::socket socket(executor); + // ... connect ... + + char buf[1024]; + auto [ec, n] = co_await socket.async_read_some( + asio::buffer(buf), + asio::experimental::as_tuple(use_awaitable)); + + if (ec) + { + if (ec == asio::error::eof) + std::cout << "Connection closed\n"; + else + std::cout << "Error: " << ec.message() << "\n"; + co_return; + } + + std::cout << "Read " << n << " bytes\n"; +} +---- + +This is particularly useful when the error isn't exceptional (like EOF). + +== Common Error Codes + +=== Connection Errors + +[cols="1,2"] +|=== +| Error | Meaning + +| `asio::error::connection_refused` | No server listening on that port +| `asio::error::connection_reset` | Peer forcibly closed the connection +| `asio::error::host_unreachable` | No route to host +| `asio::error::network_unreachable` | No route to network +| `asio::error::timed_out` | Connection attempt timed out +|=== + +=== Read/Write Errors + +[cols="1,2"] +|=== +| Error | Meaning + +| `asio::error::eof` | Connection closed cleanly by peer +| `asio::error::connection_reset` | Connection forcibly closed +| `asio::error::broken_pipe` | Write to a closed connection +| `asio::error::operation_aborted` | Operation was cancelled +|=== + +=== Resolution Errors + +[cols="1,2"] +|=== +| Error | Meaning + +| `asio::error::host_not_found` | DNS lookup failed +| `asio::error::host_not_found_try_again` | Temporary DNS failure +| `asio::error::no_data` | Host exists but has no address +|=== + +== Checking Error Codes + +[source,cpp] +---- +if (ec == asio::error::eof) +{ + // Handle clean close +} +else if (ec == asio::error::connection_refused) +{ + // Handle refused connection +} +else if (ec) +{ + // Handle other errors +} +---- + +You can also check error categories: + +[source,cpp] +---- +if (ec.category() == asio::error::get_system_category()) +{ + // System-level error (OS error code) +} +else if (ec.category() == asio::error::get_misc_category()) +{ + // Asio-specific error (eof, etc.) +} +---- + +== Cancellation + +When an operation is cancelled (e.g., timer cancelled, socket closed), it +completes with `asio::error::operation_aborted`: + +[source,cpp] +---- +awaitable handle_cancellation() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor, std::chrono::seconds(10)); + + try + { + co_await timer.async_wait(use_awaitable); + std::cout << "Timer expired\n"; + } + catch (const boost::system::system_error& e) + { + if (e.code() == asio::error::operation_aborted) + std::cout << "Timer was cancelled\n"; + else + throw; + } +} +---- + +== Partial Operations + +Some operations may succeed partially. For example, `async_read` might read +fewer bytes than requested if the connection closes: + +[source,cpp] +---- +awaitable handle_partial_read() +{ + // ... socket setup ... + + char buf[1000]; + boost::system::error_code ec; + + std::size_t n = co_await asio::async_read( + socket, + asio::buffer(buf), + asio::redirect_error(use_awaitable, ec)); + + // n bytes were read, even if ec indicates an error + std::cout << "Read " << n << " bytes\n"; + + if (ec == asio::error::eof) + std::cout << "Connection closed after partial read\n"; +} +---- + +== Exception Safety in Coroutines + +Exceptions propagate through coroutine chains: + +[source,cpp] +---- +awaitable inner() +{ + throw std::runtime_error("Something went wrong"); +} + +awaitable outer() +{ + try + { + co_await inner(); + } + catch (const std::exception& e) + { + std::cout << "Caught: " << e.what() << "\n"; + } +} +---- + +If an exception escapes a coroutine spawned with `detached`, it terminates +the program. Use a completion handler to catch exceptions: + +[source,cpp] +---- +asio::co_spawn(ctx, my_coroutine(), + [](std::exception_ptr ep) { + if (ep) + { + try { std::rethrow_exception(ep); } + catch (const std::exception& e) { + std::cerr << "Unhandled: " << e.what() << "\n"; + } + } + }); +---- + +== Best Practices + +1. **Use exceptions for unexpected errors** — Connection failures, permission + denied, etc. + +2. **Use `as_tuple` for expected conditions** — EOF is normal when reading to + end of stream; don't force a try/catch. + +3. **Always handle `operation_aborted`** — It's not really an error, just + indicates the operation was cancelled. + +4. **Log error details** — Include `ec.message()` and `ec.value()` in logs + for debugging. + +== Next Steps + +* xref:core/streams.adoc[Reading and Writing] — Handle read/write errors +* xref:advanced/ssl.adoc[SSL/TLS] — SSL-specific errors diff --git a/doc/modules/ROOT/pages/core/io-context.adoc b/doc/modules/ROOT/pages/core/io-context.adoc new file mode 100644 index 000000000..e869ae326 --- /dev/null +++ b/doc/modules/ROOT/pages/core/io-context.adoc @@ -0,0 +1,233 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += The I/O Context + +Understand the `io_context`, the engine that drives all asynchronous operations. + +== What is `io_context`? + +The `io_context` is the event loop at the heart of Asio. It: + +* Receives completion notifications from the operating system +* Runs completion handlers (including coroutine resumptions) +* Tracks outstanding work to know when to stop + +Every I/O object (socket, timer, etc.) is associated with an `io_context` +(via an executor), and all async operations are processed by calling `run()`. + +== Basic Usage + +[source,cpp] +---- +#include + +namespace asio = boost::asio; + +int main() +{ + // Create the event loop + asio::io_context ctx; + + // Add work (spawn a coroutine, create timers, etc.) + asio::co_spawn(ctx, some_coroutine(), asio::detached); + + // Run the event loop + ctx.run(); + + // run() returns when there's no more work +} +---- + +== The `run()` Function + +`run()` blocks and processes events until: + +1. All work is complete (no pending async operations) +2. `stop()` is called +3. An exception escapes a handler + +[source,cpp] +---- +ctx.run(); // Blocks until done + +// These variants are also available: +ctx.run_one(); // Process at most one handler, then return +ctx.run_for(1s); // Run for at most 1 second +ctx.run_until(tp); // Run until a time point +ctx.poll(); // Process ready handlers without blocking +ctx.poll_one(); // Process at most one ready handler +---- + +== Spawning Coroutines + +Use `co_spawn` to launch a coroutine onto an `io_context`: + +[source,cpp] +---- +asio::awaitable my_coroutine() +{ + // ... + co_return; +} + +int main() +{ + asio::io_context ctx; + + // Launch with detached (fire and forget) + asio::co_spawn(ctx, my_coroutine(), asio::detached); + + // Or capture the result + asio::co_spawn(ctx, my_coroutine(), + [](std::exception_ptr ep) { + if (ep) std::rethrow_exception(ep); + }); + + ctx.run(); +} +---- + +From within a coroutine, you can spawn child coroutines: + +[source,cpp] +---- +asio::awaitable parent() +{ + auto executor = co_await asio::this_coro::executor; + + // Spawn a child coroutine + asio::co_spawn(executor, child(), asio::detached); +} +---- + +== Getting the Executor + +Every `io_context` has an associated executor. I/O objects need this executor: + +[source,cpp] +---- +// From main, get the executor from the context +auto executor = ctx.get_executor(); +asio::steady_timer timer(executor, 1s); + +// From a coroutine, get it from this_coro +asio::awaitable example() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor, 1s); + // ... +} +---- + +== Stopping the Event Loop + +To stop `run()` from outside: + +[source,cpp] +---- +ctx.stop(); // Causes run() to return soon + +// Check if stopped +if (ctx.stopped()) + ctx.restart(); // Must restart before calling run() again +---- + +Common pattern with signal handling: + +[source,cpp] +---- +int main() +{ + asio::io_context ctx; + + // Stop on Ctrl+C + asio::signal_set signals(ctx, SIGINT, SIGTERM); + signals.async_wait([&](auto, auto) { + ctx.stop(); + }); + + asio::co_spawn(ctx, server(), asio::detached); + ctx.run(); +} +---- + +== Work Tracking + +`run()` returns when there's no more work. But sometimes you want to keep it +running even when idle (e.g., waiting for new connections). + +Pending async operations count as work. So does an outstanding `executor_work_guard`: + +[source,cpp] +---- +int main() +{ + asio::io_context ctx; + + // Keep ctx.run() from returning even when idle + auto work = asio::make_work_guard(ctx); + + // ... spawn things ... + + // When ready to stop: + work.reset(); // Allow run() to return when idle + // or + ctx.stop(); // Stop immediately + + ctx.run(); +} +---- + +== Concurrency Hint + +You can hint to `io_context` that only one thread will call `run()`: + +[source,cpp] +---- +// Single-threaded (may enable optimizations) +asio::io_context ctx(1); + +// Or explicitly +asio::io_context ctx(BOOST_ASIO_CONCURRENCY_HINT_1); +---- + +This can improve performance by avoiding unnecessary synchronization. + +== Multiple `io_context` Objects + +You can have multiple `io_context` instances: + +[source,cpp] +---- +asio::io_context ctx1; +asio::io_context ctx2; + +// Each has its own event loop +std::thread t1([&]{ ctx1.run(); }); +std::thread t2([&]{ ctx2.run(); }); +---- + +I/O objects are tied to their context. A socket created with `ctx1`'s executor +cannot be moved to `ctx2`. + +== Common Mistakes + +**Forgetting to call `run()`** — Nothing happens until `run()` is called. +Spawning coroutines just queues them; they don't execute. + +**`run()` returns immediately** — This happens when there's no work. Make sure +you've started async operations before calling `run()`. + +**Calling `run()` from a handler** — Don't nest `run()` calls. If you need to +run more work from a handler, use `post()` instead. + +== Next Steps + +* xref:threading/threads.adoc[Multi-Threading] — Run `io_context` from multiple threads +* xref:threading/strands.adoc[Strands] — Synchronize handlers +* xref:core/buffers.adoc[Buffers] — Understand buffer management diff --git a/doc/modules/ROOT/pages/core/streams.adoc b/doc/modules/ROOT/pages/core/streams.adoc new file mode 100644 index 000000000..ec5754bb1 --- /dev/null +++ b/doc/modules/ROOT/pages/core/streams.adoc @@ -0,0 +1,314 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Reading and Writing + +Learn the different ways to read and write data with Asio coroutines. + +== Overview + +Asio provides several read/write functions with different behaviors: + +[cols="1,2"] +|=== +| Function | Behavior + +| `async_read_some` | Read *some* data (may be less than buffer size) +| `async_read` | Read *exact* amount (loops internally) +| `async_read_until` | Read until delimiter found +| `async_write_some` | Write *some* data (may be less than buffer size) +| `async_write` | Write *all* data (loops internally) +|=== + +== Reading Some Data + +`async_read_some` is the lowest-level read. It returns whatever data is +available, which may be less than your buffer size: + +[source,cpp] +---- +awaitable read_some_example(asio::ip::tcp::socket& socket) +{ + char buf[1024]; + + // May return 1 byte or 1024 bytes or anywhere in between + std::size_t n = co_await socket.async_read_some( + asio::buffer(buf), asio::use_awaitable); + + std::cout << "Received " << n << " bytes\n"; + // Process buf[0..n) +} +---- + +Use `async_read_some` when: + +* You want to process data as it arrives +* You don't know how much data to expect +* You're implementing a streaming protocol + +== Reading Exact Amount + +`async_read` keeps reading until the buffer is full: + +[source,cpp] +---- +awaitable read_exact_example(asio::ip::tcp::socket& socket) +{ + char header[4]; // Read exactly 4 bytes + + co_await asio::async_read( + socket, asio::buffer(header), asio::use_awaitable); + + // header is now completely filled + uint32_t length = /* parse header */; + + std::vector body(length); + co_await asio::async_read( + socket, asio::buffer(body), asio::use_awaitable); +} +---- + +Use `async_read` when: + +* You need exactly N bytes (e.g., fixed-size headers) +* You're reading length-prefixed messages + +== Reading Until Delimiter + +`async_read_until` reads until a delimiter is found: + +[source,cpp] +---- +awaitable read_line(asio::ip::tcp::socket& socket) +{ + std::string data; + + std::size_t n = co_await asio::async_read_until( + socket, + asio::dynamic_buffer(data), + '\n', + asio::use_awaitable); + + // data[0..n) includes the delimiter + std::string line = data.substr(0, n - 1); // Remove \n + data.erase(0, n); // Remove consumed data + + co_return line; +} +---- + +You can also use string delimiters: + +[source,cpp] +---- +// Read until double newline (HTTP header end) +std::size_t n = co_await asio::async_read_until( + socket, + asio::dynamic_buffer(data), + "\r\n\r\n", + asio::use_awaitable); +---- + +Or regex: + +[source,cpp] +---- +#include +#include + +std::size_t n = co_await asio::async_read_until( + socket, + asio::dynamic_buffer(data), + boost::regex("\\r?\\n"), // \n or \r\n + asio::use_awaitable); +---- + +== Writing Some Data + +`async_write_some` writes some data, possibly less than provided: + +[source,cpp] +---- +awaitable write_some_example(asio::ip::tcp::socket& socket) +{ + std::string data = "Hello, World!"; + + // May write less than data.size() + std::size_t n = co_await socket.async_write_some( + asio::buffer(data), asio::use_awaitable); + + std::cout << "Wrote " << n << " of " << data.size() << " bytes\n"; +} +---- + +You rarely need `async_write_some` directly. + +== Writing All Data + +`async_write` ensures all data is written: + +[source,cpp] +---- +awaitable write_all_example(asio::ip::tcp::socket& socket) +{ + std::string data = "Hello, World!"; + + // Writes all data, retrying as needed + co_await asio::async_write( + socket, asio::buffer(data), asio::use_awaitable); + + // All of data has been sent +} +---- + +Use `async_write` for almost all writes. It handles partial writes internally. + +== Scatter/Gather I/O + +Write multiple buffers in one operation (gather): + +[source,cpp] +---- +awaitable gather_write(asio::ip::tcp::socket& socket) +{ + std::string header = "HTTP/1.1 200 OK\r\n\r\n"; + std::string body = "Hello!"; + + std::array buffers = { + asio::buffer(header), + asio::buffer(body) + }; + + co_await asio::async_write(socket, buffers, asio::use_awaitable); +} +---- + +Read into multiple buffers (scatter): + +[source,cpp] +---- +awaitable scatter_read(asio::ip::tcp::socket& socket) +{ + char header[4]; + char body[100]; + + std::array buffers = { + asio::buffer(header), + asio::buffer(body) + }; + + // Fills header first, then body + co_await asio::async_read(socket, buffers, asio::use_awaitable); +} +---- + +== Completion Conditions + +`async_read` accepts a completion condition to customize when it stops: + +[source,cpp] +---- +// Read at least 100 bytes, but accept more +std::size_t n = co_await asio::async_read( + socket, + asio::buffer(buf), + asio::transfer_at_least(100), + asio::use_awaitable); + +// Read exactly 100 bytes (default behavior) +co_await asio::async_read( + socket, + asio::buffer(buf, 100), + asio::transfer_exactly(100), + asio::use_awaitable); + +// Read until buffer is full +co_await asio::async_read( + socket, + asio::buffer(buf), + asio::transfer_all(), + asio::use_awaitable); +---- + +== Handling Short Reads and Writes + +With `async_read_some`, you typically loop until you have what you need: + +[source,cpp] +---- +awaitable read_message(asio::ip::tcp::socket& socket) +{ + std::vector message; + char buf[1024]; + + while (message.size() < expected_size) + { + auto [ec, n] = co_await socket.async_read_some( + asio::buffer(buf), + asio::experimental::as_tuple(asio::use_awaitable)); + + if (ec == asio::error::eof) + break; + if (ec) + throw boost::system::system_error(ec); + + message.insert(message.end(), buf, buf + n); + } +} +---- + +Or just use `async_read` which does this for you. + +== EOF Handling + +When reading, `asio::error::eof` means the peer closed the connection: + +[source,cpp] +---- +awaitable read_until_close(asio::ip::tcp::socket& socket) +{ + std::string data; + char buf[1024]; + + for (;;) + { + auto [ec, n] = co_await socket.async_read_some( + asio::buffer(buf), + asio::experimental::as_tuple(asio::use_awaitable)); + + if (ec == asio::error::eof) + { + std::cout << "Connection closed, received total: " + << data.size() << " bytes\n"; + break; + } + if (ec) + throw boost::system::system_error(ec); + + data.append(buf, n); + } +} +---- + +== Common Mistakes + +**Using `async_read_some` expecting full reads** — It may return less data +than requested. Use `async_read` for exact amounts. + +**Forgetting buffer lifetime** — The buffer must remain valid until the +operation completes. + +**Not handling EOF** — EOF is normal, not an error. Handle it appropriately. + +**Mixing reads and writes on same socket without care** — Operations can +overlap, but make sure you understand the protocol. + +== Next Steps + +* xref:core/buffers.adoc[Buffers] — Buffer types in detail +* xref:core/error-handling.adoc[Error Handling] — Handle read/write errors +* xref:networking/tcp-server.adoc[TCP Server] — Full server example diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc new file mode 100644 index 000000000..f47a4f0f2 --- /dev/null +++ b/doc/modules/ROOT/pages/index.adoc @@ -0,0 +1,101 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Boost.Asio + +Boost.Asio is a cross-platform C++ library for network and low-level I/O +programming that provides a consistent asynchronous model using modern C++. + +NOTE: This documentation covers **C++20 coroutines only**. All examples use +`co_await` with `awaitable<>`. For callback-based or other completion token +styles, see the reference documentation. + +== What This Library Does + +Asio provides portable, efficient asynchronous I/O: + +* **Networking** — TCP and UDP sockets, name resolution +* **Timers** — High-resolution deadline and steady timers +* **SSL/TLS** — Secure sockets via OpenSSL +* **Signal handling** — Portable signal interception +* **Extensible** — Write your own async operations + +The library uses the operating system's most efficient I/O mechanisms: +`epoll` on Linux, `kqueue` on BSD/macOS, and I/O completion ports on Windows. + +== What This Library Does Not Do + +Asio is a low-level toolkit, not a framework: + +* No HTTP, WebSocket, or other protocol implementations (see Boost.Beast) +* No serialization or message framing +* No connection pooling or load balancing +* No built-in thread pool (though you can run `io_context` from multiple threads) + +== Design Philosophy + +**Proactor pattern.** Asio uses the proactor model: you initiate an operation, +then the system notifies you when it completes. This differs from the reactor +pattern (select/poll) where you wait for readiness then perform the operation. + +**Completion tokens.** Every async operation accepts a "completion token" that +determines how results are delivered. With `use_awaitable`, results arrive via +`co_await`. The same operation can work with callbacks, futures, or other +mechanisms—the initiating function is decoupled from result delivery. + +**Zero-overhead abstractions.** The library is designed so that you pay only +for what you use. Header-only by default, with optional separate compilation. + +== Requirements + +* **C++20** with coroutine support (`-std=c++20` or `/std:c++20`) +* **Boost** — System, and optionally Coroutine for stackful coroutines + +=== Tested Compilers + +* GCC 10+ +* Clang 12+ +* MSVC 19.28+ (Visual Studio 2019 16.8+) + +=== Platform Support + +* Linux (epoll) +* macOS / BSD (kqueue) +* Windows (IOCP) + +== Quick Example + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::awaitable; +using asio::use_awaitable; + +awaitable wait_and_print() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor, std::chrono::seconds(1)); + co_await timer.async_wait(use_awaitable); + std::cout << "Timer expired!\n"; +} + +int main() +{ + asio::io_context ctx; + asio::co_spawn(ctx, wait_and_print(), asio::detached); + ctx.run(); +} +---- + +== Next Steps + +* xref:quick-start.adoc[Quick Start] — Build and run your first program +* xref:timers/async-timer.adoc[Async Timer] — Understand the async pattern +* xref:networking/tcp-client.adoc[TCP Client] — Connect to a server diff --git a/doc/modules/ROOT/pages/networking/resolving.adoc b/doc/modules/ROOT/pages/networking/resolving.adoc new file mode 100644 index 000000000..c69b832b4 --- /dev/null +++ b/doc/modules/ROOT/pages/networking/resolving.adoc @@ -0,0 +1,248 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += DNS Resolution + +Learn how to resolve hostnames to IP addresses using coroutines. + +== Basic Resolution + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::ip::tcp; +using asio::awaitable; +using asio::use_awaitable; + +awaitable resolve_hostname() +{ + auto executor = co_await asio::this_coro::executor; + + tcp::resolver resolver(executor); + + // Resolve hostname and service name + auto results = co_await resolver.async_resolve( + "www.example.com", "http", use_awaitable); + + for (const auto& entry : results) + { + std::cout << entry.endpoint() << "\n"; + } +} + +int main() +{ + asio::io_context ctx; + asio::co_spawn(ctx, resolve_hostname(), asio::detached); + ctx.run(); +} +---- + +Output might be: +---- +93.184.216.34:80 +2606:2800:220:1:248:1893:25c8:1946:80 +---- + +== Understanding Results + +`async_resolve` returns a range of `resolver::results_type`, containing +`resolver::endpoint_type` entries. Each entry includes: + +* **Endpoint** — IP address and port +* **Host name** — The canonical name (if available) +* **Service name** — The service (e.g., "http") + +A single hostname may resolve to multiple addresses (IPv4 and IPv6, or +multiple servers for load balancing). + +== Connecting with Resolution + +The typical pattern is to resolve, then try each address until one works: + +[source,cpp] +---- +awaitable connect_to_host(std::string host, std::string service) +{ + auto executor = co_await asio::this_coro::executor; + + tcp::resolver resolver(executor); + auto endpoints = co_await resolver.async_resolve(host, service, use_awaitable); + + tcp::socket socket(executor); + co_await asio::async_connect(socket, endpoints, use_awaitable); + + co_return socket; +} +---- + +`async_connect` with an endpoint range automatically tries each address in +sequence until one succeeds. + +== Resolver Flags + +Control resolution behavior with flags: + +[source,cpp] +---- +awaitable resolve_with_flags() +{ + auto executor = co_await asio::this_coro::executor; + tcp::resolver resolver(executor); + + // Only return IPv4 addresses + auto results = co_await resolver.async_resolve( + "www.example.com", "443", + tcp::resolver::numeric_service, // "443" is a port number, not a service name + use_awaitable); + + // Or combine flags + auto results2 = co_await resolver.async_resolve( + "www.example.com", "https", + tcp::resolver::passive | // For binding (servers) + tcp::resolver::address_configured, // Only configured addresses + use_awaitable); +} +---- + +Common flags: + +[cols="1,2"] +|=== +| Flag | Meaning + +| `numeric_host` | The host string is an IP address, don't resolve +| `numeric_service` | The service string is a port number +| `passive` | For server sockets (bind, not connect) +| `canonical_name` | Request the canonical name +| `v4_mapped` | Return IPv4-mapped IPv6 addresses if no IPv6 +| `all_matching` | Return all matching addresses +| `address_configured` | Only return addresses the system can use +|=== + +== IPv4 Only or IPv6 Only + +To restrict to a specific IP version: + +[source,cpp] +---- +awaitable resolve_ipv4_only() +{ + auto executor = co_await asio::this_coro::executor; + + // TCP IPv4 resolver + asio::ip::tcp::resolver resolver(executor); + + // Resolve with IPv4 protocol hint + auto results = co_await resolver.async_resolve( + asio::ip::tcp::v4(), // Protocol hint + "www.example.com", + "http", + use_awaitable); + + // All results will be IPv4 +} +---- + +== Reverse DNS Lookup + +Look up the hostname for an IP address: + +[source,cpp] +---- +awaitable reverse_lookup() +{ + auto executor = co_await asio::this_coro::executor; + tcp::resolver resolver(executor); + + tcp::endpoint endpoint(asio::ip::make_address("93.184.216.34"), 80); + + auto results = co_await resolver.async_resolve(endpoint, use_awaitable); + + for (const auto& entry : results) + { + std::cout << entry.host_name() << "\n"; + } +} +---- + +== Error Handling + +Resolution can fail for various reasons: + +[source,cpp] +---- +awaitable resolve_with_error_handling() +{ + auto executor = co_await asio::this_coro::executor; + tcp::resolver resolver(executor); + + try + { + auto results = co_await resolver.async_resolve( + "nonexistent.invalid", "http", use_awaitable); + } + catch (const boost::system::system_error& e) + { + // e.code() might be: + // - asio::error::host_not_found + // - asio::error::host_not_found_try_again + // - asio::error::no_data + // - asio::error::no_recovery + std::cerr << "Resolution failed: " << e.what() << "\n"; + } +} +---- + +== Caching + +Asio doesn't cache DNS results. Each `async_resolve` call queries the system +resolver. For high-volume applications, consider: + +* Caching results yourself (respecting TTL) +* Using connection pools that reuse existing connections +* Resolving once at startup for known hosts + +== UDP Resolution + +Works the same way, just use `udp::resolver`: + +[source,cpp] +---- +awaitable resolve_udp() +{ + auto executor = co_await asio::this_coro::executor; + + asio::ip::udp::resolver resolver(executor); + auto results = co_await resolver.async_resolve( + "dns.google", "53", use_awaitable); + + for (const auto& entry : results) + { + std::cout << entry.endpoint() << "\n"; + } +} +---- + +== Common Mistakes + +**Resolving every time** — Resolution has overhead. Resolve once and reuse the +endpoint, or use connection pooling. + +**Ignoring multiple results** — A host may have several addresses. Using +`async_connect` with the results range handles this correctly. + +**Not handling errors** — Network issues, DNS server problems, and typos in +hostnames all cause resolution to fail. + +== Next Steps + +* xref:networking/tcp-client.adoc[TCP Client] — Use resolved addresses +* xref:core/error-handling.adoc[Error Handling] — Handle resolution failures diff --git a/doc/modules/ROOT/pages/networking/tcp-client.adoc b/doc/modules/ROOT/pages/networking/tcp-client.adoc new file mode 100644 index 000000000..dc95a4052 --- /dev/null +++ b/doc/modules/ROOT/pages/networking/tcp-client.adoc @@ -0,0 +1,252 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += TCP Client + +Learn how to connect to a server and exchange data over TCP using coroutines. + +== Basic TCP Client + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::ip::tcp; +using asio::awaitable; +using asio::use_awaitable; + +awaitable tcp_client() +{ + auto executor = co_await asio::this_coro::executor; + + // Create a socket + tcp::socket socket(executor); + + // Connect to the server + tcp::endpoint endpoint(asio::ip::make_address("93.184.216.34"), 80); + co_await socket.async_connect(endpoint, use_awaitable); + + std::cout << "Connected!\n"; + + // Send a request + std::string request = "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n"; + co_await asio::async_write(socket, asio::buffer(request), use_awaitable); + + // Read the response + std::string response; + char buf[1024]; + + for (;;) + { + boost::system::error_code ec; + std::size_t n = co_await socket.async_read_some( + asio::buffer(buf), + asio::redirect_error(use_awaitable, ec)); + + if (ec == asio::error::eof) + break; // Connection closed cleanly + if (ec) + throw boost::system::system_error(ec); + + response.append(buf, n); + } + + std::cout << "Received " << response.size() << " bytes\n"; +} + +int main() +{ + asio::io_context ctx; + asio::co_spawn(ctx, tcp_client(), asio::detached); + ctx.run(); +} +---- + +== Step by Step + +=== 1. Create the Socket + +[source,cpp] +---- +tcp::socket socket(executor); +---- + +The socket is created in a closed state. It needs an executor to associate +async operations with the event loop. + +=== 2. Connect + +[source,cpp] +---- +tcp::endpoint endpoint(asio::ip::make_address("93.184.216.34"), 80); +co_await socket.async_connect(endpoint, use_awaitable); +---- + +`async_connect` initiates a TCP connection. The coroutine suspends until the +connection succeeds or fails. + +=== 3. Send Data + +[source,cpp] +---- +co_await asio::async_write(socket, asio::buffer(request), use_awaitable); +---- + +`async_write` sends all the data in the buffer. It handles partial writes +internally—you don't need a loop. + +=== 4. Receive Data + +[source,cpp] +---- +std::size_t n = co_await socket.async_read_some(asio::buffer(buf), use_awaitable); +---- + +`async_read_some` reads *some* data—possibly less than the buffer size. +You typically loop until you have all the data you need or the connection closes. + +== Using DNS Resolution + +Usually you have a hostname, not an IP address. Use the resolver: + +[source,cpp] +---- +awaitable tcp_client_with_dns(std::string host, std::string port) +{ + auto executor = co_await asio::this_coro::executor; + + // Resolve the hostname + tcp::resolver resolver(executor); + auto endpoints = co_await resolver.async_resolve(host, port, use_awaitable); + + // Connect to the first endpoint that works + tcp::socket socket(executor); + co_await asio::async_connect(socket, endpoints, use_awaitable); + + std::cout << "Connected to " << socket.remote_endpoint() << "\n"; +} +---- + +`async_resolve` returns a range of endpoints (a host may have multiple IPs). +`async_connect` with an endpoint range tries each one until one succeeds. + +== Error Handling + +By default, errors throw exceptions: + +[source,cpp] +---- +awaitable tcp_client_with_exceptions() +{ + try + { + auto executor = co_await asio::this_coro::executor; + tcp::socket socket(executor); + + tcp::endpoint endpoint(asio::ip::make_address("127.0.0.1"), 12345); + co_await socket.async_connect(endpoint, use_awaitable); + } + catch (const boost::system::system_error& e) + { + std::cerr << "Connection failed: " << e.what() << "\n"; + // e.code() == asio::error::connection_refused, etc. + } +} +---- + +For error codes without exceptions, use `redirect_error` or `as_tuple`: + +[source,cpp] +---- +awaitable tcp_client_with_error_codes() +{ + auto executor = co_await asio::this_coro::executor; + tcp::socket socket(executor); + + tcp::endpoint endpoint(asio::ip::make_address("127.0.0.1"), 12345); + + boost::system::error_code ec; + co_await socket.async_connect( + endpoint, + asio::redirect_error(use_awaitable, ec)); + + if (ec) + { + std::cerr << "Connection failed: " << ec.message() << "\n"; + co_return; + } + + std::cout << "Connected!\n"; +} +---- + +== Adding a Timeout + +[source,cpp] +---- +#include + +using namespace asio::experimental::awaitable_operators; + +awaitable tcp_client_with_timeout() +{ + auto executor = co_await asio::this_coro::executor; + + tcp::socket socket(executor); + tcp::endpoint endpoint(asio::ip::make_address("10.0.0.1"), 80); + + asio::steady_timer timer(executor, std::chrono::seconds(5)); + + // Race connect against timeout + auto result = co_await ( + socket.async_connect(endpoint, use_awaitable) + || timer.async_wait(use_awaitable) + ); + + if (result.index() == 0) + std::cout << "Connected\n"; + else + std::cout << "Connection timed out\n"; +} +---- + +== Reading Until a Delimiter + +For line-based protocols: + +[source,cpp] +---- +awaitable read_line(tcp::socket& socket) +{ + asio::streambuf buf; + co_await asio::async_read_until(socket, buf, '\n', use_awaitable); + + std::istream is(&buf); + std::string line; + std::getline(is, line); + co_return line; +} +---- + +== Common Mistakes + +**Not checking for `eof`** — When the peer closes the connection, `async_read_some` +returns `asio::error::eof`. This is normal, not an error. + +**Using `async_read_some` when you need exact bytes** — Use `async_read` with a +buffer size if you need exactly N bytes. `async_read_some` may return fewer. + +**Forgetting buffer lifetime** — The buffer must remain valid until the operation +completes. Don't use a local that goes out of scope. + +== Next Steps + +* xref:networking/tcp-server.adoc[TCP Server] — Accept incoming connections +* xref:networking/resolving.adoc[DNS Resolution] — Hostname lookup in detail +* xref:core/buffers.adoc[Buffers] — Understanding buffer types diff --git a/doc/modules/ROOT/pages/networking/tcp-server.adoc b/doc/modules/ROOT/pages/networking/tcp-server.adoc new file mode 100644 index 000000000..4a6ebfebb --- /dev/null +++ b/doc/modules/ROOT/pages/networking/tcp-server.adoc @@ -0,0 +1,257 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += TCP Server + +Learn how to accept connections and handle multiple clients using coroutines. + +== Echo Server + +A complete TCP server that echoes back whatever clients send: + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::ip::tcp; +using asio::awaitable; +using asio::co_spawn; +using asio::detached; +using asio::use_awaitable; + +awaitable echo_session(tcp::socket socket) +{ + try + { + char data[1024]; + for (;;) + { + std::size_t n = co_await socket.async_read_some( + asio::buffer(data), use_awaitable); + + co_await asio::async_write( + socket, asio::buffer(data, n), use_awaitable); + } + } + catch (const std::exception& e) + { + std::cout << "Session ended: " << e.what() << "\n"; + } +} + +awaitable listener(unsigned short port) +{ + auto executor = co_await asio::this_coro::executor; + + tcp::acceptor acceptor(executor, {tcp::v4(), port}); + std::cout << "Listening on port " << port << "\n"; + + for (;;) + { + tcp::socket socket = co_await acceptor.async_accept(use_awaitable); + std::cout << "New connection from " << socket.remote_endpoint() << "\n"; + + // Spawn a new coroutine for this client + co_spawn(executor, echo_session(std::move(socket)), detached); + } +} + +int main() +{ + asio::io_context ctx; + co_spawn(ctx, listener(8080), detached); + ctx.run(); +} +---- + +== How It Works + +=== 1. Create the Acceptor + +[source,cpp] +---- +tcp::acceptor acceptor(executor, {tcp::v4(), port}); +---- + +The acceptor listens for incoming connections. The endpoint `{tcp::v4(), port}` +means "listen on all IPv4 interfaces on this port." + +=== 2. Accept Connections + +[source,cpp] +---- +tcp::socket socket = co_await acceptor.async_accept(use_awaitable); +---- + +`async_accept` waits for a client to connect and returns a new socket for +that connection. The acceptor continues to listen for more connections. + +=== 3. Spawn a Session + +[source,cpp] +---- +co_spawn(executor, echo_session(std::move(socket)), detached); +---- + +Each client gets its own coroutine. The socket is moved into the session +coroutine. `detached` means we don't wait for the session to complete. + +=== 4. Handle the Session + +The `echo_session` coroutine runs independently, reading and writing until +the client disconnects (which throws an exception caught by the try/catch). + +== Dual-Stack Server (IPv4 and IPv6) + +To accept both IPv4 and IPv6 connections: + +[source,cpp] +---- +awaitable listener_v6(unsigned short port) +{ + auto executor = co_await asio::this_coro::executor; + + tcp::acceptor acceptor(executor, {tcp::v6(), port}); + + // Allow IPv4 connections on this IPv6 socket + acceptor.set_option(asio::ip::v6_only(false)); + + for (;;) + { + tcp::socket socket = co_await acceptor.async_accept(use_awaitable); + co_spawn(executor, echo_session(std::move(socket)), detached); + } +} +---- + +NOTE: On some platforms, you may need separate acceptors for v4 and v6. + +== Graceful Shutdown + +To stop accepting new connections while letting existing sessions finish: + +[source,cpp] +---- +awaitable listener_with_shutdown(unsigned short port) +{ + auto executor = co_await asio::this_coro::executor; + + tcp::acceptor acceptor(executor, {tcp::v4(), port}); + + try + { + for (;;) + { + tcp::socket socket = co_await acceptor.async_accept(use_awaitable); + co_spawn(executor, echo_session(std::move(socket)), detached); + } + } + catch (const boost::system::system_error& e) + { + if (e.code() != asio::error::operation_aborted) + throw; + // Normal shutdown + } +} + +int main() +{ + asio::io_context ctx; + + // Handle Ctrl+C + asio::signal_set signals(ctx, SIGINT, SIGTERM); + signals.async_wait([&](auto, auto) { + ctx.stop(); // Cancels pending accepts + }); + + co_spawn(ctx, listener_with_shutdown(8080), detached); + ctx.run(); +} +---- + +== Limiting Concurrent Connections + +Use a semaphore pattern to limit simultaneous sessions: + +[source,cpp] +---- +class server +{ + tcp::acceptor acceptor_; + std::atomic connection_count_{0}; + static constexpr int max_connections = 100; + +public: + server(asio::any_io_executor executor, unsigned short port) + : acceptor_(executor, {tcp::v4(), port}) + {} + + awaitable run() + { + for (;;) + { + tcp::socket socket = co_await acceptor_.async_accept(use_awaitable); + + if (connection_count_ >= max_connections) + { + // Reject the connection + socket.close(); + continue; + } + + ++connection_count_; + co_spawn( + acceptor_.get_executor(), + handle_session(std::move(socket)), + detached); + } + } + +private: + awaitable handle_session(tcp::socket socket) + { + // ... handle the session ... + --connection_count_; + co_return; + } +}; +---- + +== Socket Options + +Common options to set on the acceptor or sockets: + +[source,cpp] +---- +// Allow address reuse (avoids "address already in use" on restart) +acceptor.set_option(asio::socket_base::reuse_address(true)); + +// Disable Nagle's algorithm for lower latency +socket.set_option(tcp::no_delay(true)); + +// Set receive buffer size +socket.set_option(asio::socket_base::receive_buffer_size(65536)); +---- + +== Common Mistakes + +**Forgetting to move the socket** — If you pass the socket by reference to +`co_spawn`, it may be destroyed before the session uses it. + +**Not handling session errors** — Always wrap session code in try/catch. +A crashing session shouldn't bring down the server. + +**Blocking in a session** — Don't call blocking operations in a coroutine. +Everything should be `co_await` async operations. + +== Next Steps + +* xref:networking/udp.adoc[UDP] — Connectionless datagram sockets +* xref:threading/threads.adoc[Multi-Threading] — Scale with multiple threads +* xref:threading/strands.adoc[Strands] — Thread-safe shared state diff --git a/doc/modules/ROOT/pages/networking/udp.adoc b/doc/modules/ROOT/pages/networking/udp.adoc new file mode 100644 index 000000000..61aa49c39 --- /dev/null +++ b/doc/modules/ROOT/pages/networking/udp.adoc @@ -0,0 +1,256 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += UDP + +Learn how to send and receive datagrams using UDP. + +== UDP vs TCP + +[cols="1,1"] +|=== +| TCP | UDP + +| Connection-oriented | Connectionless +| Reliable, ordered delivery | Best-effort, may lose or reorder +| Stream of bytes | Discrete datagrams +| `async_read` / `async_write` | `async_send_to` / `async_receive_from` +|=== + +Use UDP when: + +* You need low latency (no connection setup) +* Occasional packet loss is acceptable +* You're building on a protocol that handles reliability itself + +== UDP Client + +Send a datagram to a server and receive a response: + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::ip::udp; +using asio::awaitable; +using asio::use_awaitable; + +awaitable udp_client() +{ + auto executor = co_await asio::this_coro::executor; + + // Create a socket + udp::socket socket(executor, udp::v4()); + + // Server address + udp::endpoint server(asio::ip::make_address("127.0.0.1"), 9000); + + // Send a datagram + std::string message = "Hello, UDP!"; + co_await socket.async_send_to( + asio::buffer(message), server, use_awaitable); + + // Receive a response + char reply[1024]; + udp::endpoint sender; + std::size_t n = co_await socket.async_receive_from( + asio::buffer(reply), sender, use_awaitable); + + std::cout << "Received " << n << " bytes from " << sender << "\n"; + std::cout << std::string_view(reply, n) << "\n"; +} + +int main() +{ + asio::io_context ctx; + asio::co_spawn(ctx, udp_client(), asio::detached); + ctx.run(); +} +---- + +== UDP Server + +Receive datagrams and echo them back: + +[source,cpp] +---- +awaitable udp_server(unsigned short port) +{ + auto executor = co_await asio::this_coro::executor; + + // Create a socket bound to a port + udp::socket socket(executor, udp::endpoint(udp::v4(), port)); + + std::cout << "UDP server listening on port " << port << "\n"; + + char data[1024]; + for (;;) + { + udp::endpoint sender; + std::size_t n = co_await socket.async_receive_from( + asio::buffer(data), sender, use_awaitable); + + std::cout << "Received " << n << " bytes from " << sender << "\n"; + + // Echo it back + co_await socket.async_send_to( + asio::buffer(data, n), sender, use_awaitable); + } +} + +int main() +{ + asio::io_context ctx; + asio::co_spawn(ctx, udp_server(9000), asio::detached); + ctx.run(); +} +---- + +== Key Differences from TCP + +=== No Connection + +UDP has no `async_connect` or `async_accept`. Each datagram is independent. +You can send to different addresses from the same socket. + +=== Message Boundaries + +Each `async_send_to` sends one datagram. Each `async_receive_from` receives +one datagram. Messages are not split or merged like TCP streams. + +=== Sender Address + +`async_receive_from` fills in the sender's address. This is how the server +knows where to send replies. + +== Connected UDP + +You can "connect" a UDP socket to a specific endpoint. This doesn't establish +a connection—it just sets a default destination: + +[source,cpp] +---- +awaitable connected_udp() +{ + auto executor = co_await asio::this_coro::executor; + + udp::socket socket(executor, udp::v4()); + udp::endpoint server(asio::ip::make_address("127.0.0.1"), 9000); + + // Set default destination + co_await socket.async_connect(server, use_awaitable); + + // Now you can use async_send instead of async_send_to + std::string message = "Hello!"; + co_await socket.async_send(asio::buffer(message), use_awaitable); + + // And async_receive instead of async_receive_from + char reply[1024]; + std::size_t n = co_await socket.async_receive( + asio::buffer(reply), use_awaitable); +} +---- + +Benefits of connected UDP: + +* Slightly more efficient (no per-send address lookup) +* Receives only packets from the connected address +* Can detect ICMP errors (connection refused, etc.) + +== Multicast + +To receive multicast traffic, join a multicast group: + +[source,cpp] +---- +awaitable multicast_receiver() +{ + auto executor = co_await asio::this_coro::executor; + + // Listen on the multicast port + udp::socket socket(executor, udp::endpoint(udp::v4(), 30001)); + + // Join the multicast group + socket.set_option(asio::ip::multicast::join_group( + asio::ip::make_address("239.255.0.1"))); + + char data[1024]; + for (;;) + { + udp::endpoint sender; + std::size_t n = co_await socket.async_receive_from( + asio::buffer(data), sender, use_awaitable); + + std::cout << std::string_view(data, n) << "\n"; + } +} + +awaitable multicast_sender() +{ + auto executor = co_await asio::this_coro::executor; + + udp::socket socket(executor, udp::v4()); + udp::endpoint multicast_endpoint( + asio::ip::make_address("239.255.0.1"), 30001); + + std::string message = "Hello, multicast!"; + co_await socket.async_send_to( + asio::buffer(message), multicast_endpoint, use_awaitable); +} +---- + +== Broadcast + +To send to all hosts on the local network: + +[source,cpp] +---- +awaitable broadcast_sender() +{ + auto executor = co_await asio::this_coro::executor; + + udp::socket socket(executor, udp::v4()); + + // Enable broadcast + socket.set_option(asio::socket_base::broadcast(true)); + + udp::endpoint broadcast_endpoint( + asio::ip::address_v4::broadcast(), 30001); + + std::string message = "Hello, everyone!"; + co_await socket.async_send_to( + asio::buffer(message), broadcast_endpoint, use_awaitable); +} +---- + +== Error Handling + +UDP operations can fail, but differently from TCP: + +* **Send errors** are rare (usually only if the network interface is down) +* **Receive errors** can indicate the socket was closed or an ICMP error arrived +* **Lost packets** are silent—there's no error, the data just doesn't arrive + +For reliability, implement your own acknowledgment and retry logic, or use TCP. + +== Common Mistakes + +**Buffer too small** — If the buffer is smaller than the incoming datagram, +the excess is discarded. Make your buffer large enough. + +**Expecting reliability** — UDP doesn't guarantee delivery. Don't assume +every send results in a receive. + +**Blocking on receive** — If no datagram arrives, `async_receive_from` waits +forever. Use a timeout if needed. + +== Next Steps + +* xref:networking/resolving.adoc[DNS Resolution] — Look up hostnames +* xref:timers/recurring-timer.adoc[Recurring Timer] — Add timeouts to UDP diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc new file mode 100644 index 000000000..b018cf232 --- /dev/null +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -0,0 +1,154 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Quick Start + +Get a working Asio program running in five minutes. + +NOTE: This guide requires C++20 with coroutine support. + +== Minimal Example + +Create a file `timer.cpp`: + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::awaitable; +using asio::use_awaitable; + +awaitable wait_one_second() +{ + // Get the executor from the current coroutine + auto executor = co_await asio::this_coro::executor; + + // Create a timer that expires in 1 second + asio::steady_timer timer(executor, std::chrono::seconds(1)); + + // Wait for the timer to expire + co_await timer.async_wait(use_awaitable); + + std::cout << "Hello from Asio!\n"; +} + +int main() +{ + // Create the I/O context — this runs the event loop + asio::io_context ctx; + + // Spawn the coroutine onto the context + asio::co_spawn(ctx, wait_one_second(), asio::detached); + + // Run the event loop until all work completes + ctx.run(); +} +---- + +== Build and Run + +[source,bash] +---- +# Linux / macOS with GCC or Clang +g++ -std=c++20 -o timer timer.cpp -pthread + +# Windows with MSVC +cl /std:c++20 /EHsc timer.cpp + +# Run +./timer +---- + +Expected output after 1 second: + +---- +Hello from Asio! +---- + +== What Just Happened? + +1. **`io_context ctx`** — Created the event loop that drives all async operations +2. **`co_spawn(ctx, wait_one_second(), detached)`** — Launched the coroutine on the context +3. **`ctx.run()`** — Ran the event loop; blocks until all work completes +4. Inside the coroutine: + - **`co_await this_coro::executor`** — Got the executor from the coroutine's context + - **`steady_timer timer(...)`** — Created a timer bound to that executor + - **`co_await timer.async_wait(use_awaitable)`** — Suspended until the timer expires +5. When the timer expired, the coroutine resumed and printed the message +6. The coroutine completed, `run()` saw no more work, and returned + +== Key Concepts + +[cols="1,3"] +|=== +| `io_context` | The event loop. Manages I/O and runs completion handlers. +| `co_spawn` | Launches a coroutine onto an executor or context. +| `awaitable` | A coroutine that returns `T` and can be `co_await`-ed. +| `use_awaitable` | Completion token that makes async operations return `awaitable`. +| `detached` | Completion token for `co_spawn` that discards the result. +|=== + +== Adding Error Handling + +Asio coroutines report errors by throwing exceptions: + +[source,cpp] +---- +awaitable connect_to_server() +{ + try + { + auto executor = co_await asio::this_coro::executor; + asio::ip::tcp::socket socket(executor); + + asio::ip::tcp::endpoint endpoint( + asio::ip::make_address("127.0.0.1"), 8080); + + co_await socket.async_connect(endpoint, use_awaitable); + std::cout << "Connected!\n"; + } + catch (const std::exception& e) + { + std::cerr << "Error: " << e.what() << "\n"; + } +} +---- + +If you prefer error codes over exceptions, use `as_tuple`: + +[source,cpp] +---- +#include + +awaitable connect_to_server() +{ + auto executor = co_await asio::this_coro::executor; + asio::ip::tcp::socket socket(executor); + + asio::ip::tcp::endpoint endpoint( + asio::ip::make_address("127.0.0.1"), 8080); + + auto [ec] = co_await socket.async_connect( + endpoint, + asio::experimental::as_tuple(use_awaitable)); + + if (ec) + std::cerr << "Error: " << ec.message() << "\n"; + else + std::cout << "Connected!\n"; +} +---- + +== Next Steps + +Now that you have a working program: + +* xref:timers/async-timer.adoc[Async Timer] — More timer patterns +* xref:networking/tcp-client.adoc[TCP Client] — Network I/O with coroutines +* xref:core/io-context.adoc[The I/O Context] — Understand the event loop diff --git a/doc/modules/ROOT/pages/threading/strands.adoc b/doc/modules/ROOT/pages/threading/strands.adoc new file mode 100644 index 000000000..3f4c0f13d --- /dev/null +++ b/doc/modules/ROOT/pages/threading/strands.adoc @@ -0,0 +1,275 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Strands + +Learn how to synchronize handlers without explicit locks. + +== What is a Strand? + +A strand is a serialization mechanism. Handlers dispatched through a strand +are guaranteed to: + +* Not run concurrently with each other +* Run in the order they were posted + +Think of it as a queue that processes one handler at a time. + +== Why Use Strands? + +When multiple threads call `io_context::run()`, handlers can run concurrently. +If two handlers access shared state, you need synchronization. + +Options: + +1. **Mutex** — Traditional locking +2. **Strand** — Serialize handlers so they don't run concurrently + +Strands are often cleaner than mutexes for Asio code because they work with +the async model naturally. + +== Creating a Strand + +[source,cpp] +---- +#include + +namespace asio = boost::asio; + +asio::io_context ctx; + +// Create a strand associated with the context +auto strand = asio::make_strand(ctx); + +// Or from an existing executor +auto strand2 = asio::make_strand(ctx.get_executor()); +---- + +== Using Strands with Coroutines + +Bind a coroutine's operations to a strand: + +[source,cpp] +---- +class connection : public std::enable_shared_from_this +{ + asio::strand strand_; + asio::ip::tcp::socket socket_; + std::string shared_data_; + +public: + connection(asio::io_context& ctx) + : strand_(asio::make_strand(ctx)) + , socket_(strand_) // Socket bound to strand + {} + + awaitable run() + { + // All operations on this socket are serialized + // because the socket's executor is the strand + char buf[1024]; + for (;;) + { + std::size_t n = co_await socket_.async_read_some( + asio::buffer(buf), asio::use_awaitable); + + // Safe to modify shared_data_ here + shared_data_.append(buf, n); + + co_await asio::async_write( + socket_, asio::buffer(shared_data_), asio::use_awaitable); + } + } +}; +---- + +== Dispatching to a Strand + +Use `dispatch` or `post` to run work on a strand: + +[source,cpp] +---- +awaitable work_on_strand( + asio::strand& strand) +{ + // Switch to the strand + co_await asio::dispatch(strand, asio::use_awaitable); + + // Now running on the strand + // Safe to access state protected by this strand +} +---- + +Difference between `dispatch` and `post`: + +* `dispatch` — Run immediately if already on the strand, otherwise queue +* `post` — Always queue (never runs immediately) + +== Binding Handlers to Strands + +Use `bind_executor` to ensure a handler runs on a strand: + +[source,cpp] +---- +auto strand = asio::make_strand(ctx); + +timer.async_wait( + asio::bind_executor(strand, + [](boost::system::error_code ec) { + // This handler runs on the strand + })); +---- + +== Example: Shared Counter + +Two coroutines increment a shared counter safely: + +[source,cpp] +---- +class counter +{ + asio::strand strand_; + int value_ = 0; + +public: + counter(asio::io_context& ctx) + : strand_(asio::make_strand(ctx)) + {} + + awaitable increment() + { + co_await asio::dispatch(strand_, asio::use_awaitable); + ++value_; + std::cout << "Value: " << value_ << "\n"; + } + + awaitable run_incrementer() + { + for (int i = 0; i < 100; ++i) + co_await increment(); + } +}; + +int main() +{ + asio::io_context ctx; + counter c(ctx); + + // Two concurrent incrementers + asio::co_spawn(ctx, c.run_incrementer(), asio::detached); + asio::co_spawn(ctx, c.run_incrementer(), asio::detached); + + // Run from multiple threads + std::thread t1([&]{ ctx.run(); }); + std::thread t2([&]{ ctx.run(); }); + + t1.join(); + t2.join(); + // Final value is 200, guaranteed +} +---- + +== Strands vs Mutexes + +[cols="1,1"] +|=== +| Strand | Mutex + +| Non-blocking (handlers are queued) | Can block waiting for lock +| Natural fit for async code | Requires careful lock scoping +| One strand per resource | One mutex per resource +| Cannot deadlock with other strands | Can deadlock with other mutexes +| Slight overhead even if single-threaded | Zero overhead if single-threaded +|=== + +== Implicit Strands + +Some operations create implicit serialization: + +[source,cpp] +---- +awaitable session(tcp::socket socket) +{ + // This coroutine runs to completion before handling + // another operation on this socket. No explicit strand needed + // if there's only one outstanding operation at a time. + + char buf[1024]; + for (;;) + { + auto n = co_await socket.async_read_some( + asio::buffer(buf), asio::use_awaitable); + co_await asio::async_write( + socket, asio::buffer(buf, n), asio::use_awaitable); + } +} +---- + +The `co_await` ensures sequential execution within the coroutine. You only +need an explicit strand if: + +* Multiple coroutines access the same resource +* You have concurrent operations on the same object + +== Common Patterns + +=== Per-Connection Strand + +[source,cpp] +---- +class session +{ + asio::strand strand_; + tcp::socket socket_; + +public: + session(asio::io_context& ctx) + : strand_(asio::make_strand(ctx)) + , socket_(strand_) + {} +}; +---- + +=== Shared Resource Strand + +[source,cpp] +---- +class shared_cache +{ + asio::strand strand_; + std::unordered_map data_; + +public: + awaitable get(std::string key) + { + co_await asio::dispatch(strand_, asio::use_awaitable); + co_return data_[key]; + } + + awaitable set(std::string key, std::string value) + { + co_await asio::dispatch(strand_, asio::use_awaitable); + data_[key] = std::move(value); + } +}; +---- + +== Common Mistakes + +**Using strand when single-threaded** — Unnecessary overhead if only one +thread calls `run()`. + +**Holding work across `co_await`** — The strand only protects while the handler +is running. Between `co_await`s, another handler could run. + +**Creating too many strands** — One strand per logical resource is typical. +Don't create a strand per operation. + +== Next Steps + +* xref:threading/threads.adoc[Multi-Threading] — Thread pool patterns +* xref:advanced/executors.adoc[Executors] — Understanding executor types diff --git a/doc/modules/ROOT/pages/threading/threads.adoc b/doc/modules/ROOT/pages/threading/threads.adoc new file mode 100644 index 000000000..e0007650e --- /dev/null +++ b/doc/modules/ROOT/pages/threading/threads.adoc @@ -0,0 +1,264 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Multi-Threading + +Learn how to scale Asio applications with multiple threads. + +== Single-Threaded by Default + +The simplest Asio usage is single-threaded: + +[source,cpp] +---- +int main() +{ + asio::io_context ctx; + asio::co_spawn(ctx, server(), asio::detached); + ctx.run(); // Single thread runs the event loop +} +---- + +This is often sufficient. All coroutines run on one thread, so there's no +need for synchronization. A single thread can handle thousands of concurrent +connections if the work is I/O-bound. + +== When to Use Multiple Threads + +Add threads when: + +* CPU-bound work blocks the event loop +* You need to scale beyond what one core can handle +* Latency-sensitive operations compete with slow handlers + +== Running `io_context` from Multiple Threads + +The simplest multi-threaded pattern: call `run()` from multiple threads: + +[source,cpp] +---- +int main() +{ + asio::io_context ctx; + + // Spawn your coroutines + asio::co_spawn(ctx, server(), asio::detached); + + // Create a pool of threads to run the context + std::vector threads; + unsigned int thread_count = std::thread::hardware_concurrency(); + + for (unsigned int i = 0; i < thread_count; ++i) + { + threads.emplace_back([&ctx]() { + ctx.run(); + }); + } + + // Wait for all threads to complete + for (auto& t : threads) + t.join(); +} +---- + +With this setup: + +* Any thread can run any handler +* Handlers for the same socket may run on different threads +* You need synchronization if handlers share mutable state + +== Thread Safety Guarantees + +Asio provides these guarantees: + +1. **Distinct objects:** Operations on distinct objects are thread-safe +2. **Same object:** Concurrent operations on the *same* object require synchronization +3. **Shared state:** You must protect any shared mutable state + +[source,cpp] +---- +// SAFE: different sockets, no synchronization needed +void thread1() { socket1.async_read_some(...); } +void thread2() { socket2.async_read_some(...); } + +// UNSAFE: same socket, needs synchronization +void thread1() { socket.async_read_some(...); } +void thread2() { socket.async_write(...); } // BAD! +---- + +== io_context-per-Thread + +An alternative pattern: one `io_context` per thread: + +[source,cpp] +---- +class thread_pool +{ + std::vector contexts_; + std::vector threads_; + std::atomic next_{0}; + +public: + thread_pool(std::size_t size) + : contexts_(size) + { + for (auto& ctx : contexts_) + { + threads_.emplace_back([&ctx]() { + auto work = asio::make_work_guard(ctx); + ctx.run(); + }); + } + } + + // Round-robin assignment + asio::io_context& get_context() + { + return contexts_[next_++ % contexts_.size()]; + } + + void stop() + { + for (auto& ctx : contexts_) + ctx.stop(); + } + + ~thread_pool() + { + stop(); + for (auto& t : threads_) + t.join(); + } +}; +---- + +Benefits: + +* Handlers for one connection always run on the same thread +* No synchronization needed within a connection +* Better cache locality + +Drawbacks: + +* More complex to set up +* Load balancing across contexts is manual + +== Using Strands for Synchronization + +For shared state with a single `io_context`, use strands (see next page): + +[source,cpp] +---- +class shared_resource +{ + asio::strand strand_; + std::string data_; + +public: + shared_resource(asio::io_context& ctx) + : strand_(asio::make_strand(ctx)) + {} + + awaitable update(std::string new_data) + { + // Ensure we're on the strand + co_await asio::dispatch(strand_, asio::use_awaitable); + data_ = std::move(new_data); + } +}; +---- + +== Concurrency Hints + +When creating `io_context`, you can provide hints: + +[source,cpp] +---- +// Tell Asio only one thread will call run() +asio::io_context ctx(1); // Single-threaded hint + +// Or be explicit +asio::io_context ctx(BOOST_ASIO_CONCURRENCY_HINT_UNSAFE); // No internal locking +asio::io_context ctx(BOOST_ASIO_CONCURRENCY_HINT_SAFE); // Thread-safe (default) +---- + +`BOOST_ASIO_CONCURRENCY_HINT_1` enables optimizations for single-threaded use. + +== Example: Multi-Threaded Echo Server + +[source,cpp] +---- +#include +#include +#include +#include + +namespace asio = boost::asio; +using asio::ip::tcp; +using asio::awaitable; +using asio::use_awaitable; + +awaitable echo_session(tcp::socket socket) +{ + char data[1024]; + try + { + for (;;) + { + std::size_t n = co_await socket.async_read_some( + asio::buffer(data), use_awaitable); + co_await asio::async_write( + socket, asio::buffer(data, n), use_awaitable); + } + } + catch (const std::exception&) {} +} + +awaitable listener(tcp::acceptor& acceptor) +{ + for (;;) + { + tcp::socket socket = co_await acceptor.async_accept(use_awaitable); + asio::co_spawn( + acceptor.get_executor(), + echo_session(std::move(socket)), + asio::detached); + } +} + +int main() +{ + asio::io_context ctx; + tcp::acceptor acceptor(ctx, {tcp::v4(), 8080}); + + asio::co_spawn(ctx, listener(acceptor), asio::detached); + + // Run from multiple threads + std::vector threads; + for (int i = 0; i < 4; ++i) + threads.emplace_back([&]{ ctx.run(); }); + + for (auto& t : threads) + t.join(); +} +---- + +== Common Mistakes + +**Assuming single-threaded behavior** — With multiple threads calling `run()`, +handlers can run concurrently. Add synchronization for shared state. + +**Over-using threads** — More threads don't always mean more performance. +For I/O-bound work, fewer threads often perform better. + +**Forgetting to join threads** — Always join your threads before destroying +the `io_context`. + +== Next Steps + +* xref:threading/strands.adoc[Strands] — Synchronize without locks +* xref:core/io-context.adoc[The I/O Context] — Event loop details diff --git a/doc/modules/ROOT/pages/timers/async-timer.adoc b/doc/modules/ROOT/pages/timers/async-timer.adoc new file mode 100644 index 000000000..485fcd968 --- /dev/null +++ b/doc/modules/ROOT/pages/timers/async-timer.adoc @@ -0,0 +1,169 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Async Timer + +Learn how to wait for a timer to expire using `co_await`. + +== Basic Timer Wait + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::awaitable; +using asio::use_awaitable; + +awaitable wait_for_timer() +{ + auto executor = co_await asio::this_coro::executor; + + // Create a timer that expires 2 seconds from now + asio::steady_timer timer(executor, std::chrono::seconds(2)); + + std::cout << "Waiting...\n"; + + // Suspend until the timer expires + co_await timer.async_wait(use_awaitable); + + std::cout << "Done!\n"; +} + +int main() +{ + asio::io_context ctx; + asio::co_spawn(ctx, wait_for_timer(), asio::detached); + ctx.run(); +} +---- + +== How It Works + +1. **Create the timer** — `steady_timer` takes an executor and a duration or time point +2. **Start the async wait** — `async_wait(use_awaitable)` initiates the operation +3. **Coroutine suspends** — Execution returns to the event loop +4. **Timer expires** — The operating system signals the event loop +5. **Coroutine resumes** — Execution continues after the `co_await` + +The coroutine does not block a thread while waiting. The `io_context` is free +to run other work during this time. + +== Timer Types + +Asio provides several timer types: + +[cols="1,2"] +|=== +| `steady_timer` | Uses `std::chrono::steady_clock`. Monotonic, not affected by system clock changes. **Use this for timeouts and intervals.** +| `system_timer` | Uses `std::chrono::system_clock`. Can be affected by system clock adjustments. +| `high_resolution_timer` | Uses `std::chrono::high_resolution_clock`. May be an alias for one of the above. +|=== + +== Setting Expiry Time + +You can set the expiry time in several ways: + +[source,cpp] +---- +// Relative: expires N seconds from now +timer.expires_after(std::chrono::seconds(5)); + +// Absolute: expires at a specific time point +timer.expires_at(std::chrono::steady_clock::now() + std::chrono::seconds(5)); + +// At construction +asio::steady_timer timer(executor, std::chrono::seconds(5)); +---- + +== Cancelling a Timer + +Timers can be cancelled, which causes the pending `co_await` to throw +`boost::system::system_error` with `asio::error::operation_aborted`: + +[source,cpp] +---- +awaitable cancellable_wait(asio::steady_timer& timer) +{ + try + { + co_await timer.async_wait(use_awaitable); + std::cout << "Timer expired normally\n"; + } + catch (const boost::system::system_error& e) + { + if (e.code() == asio::error::operation_aborted) + std::cout << "Timer was cancelled\n"; + else + throw; + } +} + +awaitable example() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor, std::chrono::seconds(10)); + + // Start the wait + auto wait = cancellable_wait(timer); + + // Cancel after 1 second + asio::steady_timer cancel_timer(executor, std::chrono::seconds(1)); + co_await cancel_timer.async_wait(use_awaitable); + + timer.cancel(); // This causes the wait to complete with operation_aborted +} +---- + +Resetting the expiry time also cancels any pending wait: + +[source,cpp] +---- +timer.expires_after(std::chrono::seconds(5)); // Cancels pending waits +---- + +== Using Error Codes Instead of Exceptions + +If you prefer handling errors without exceptions: + +[source,cpp] +---- +#include + +awaitable wait_with_error_code() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor, std::chrono::seconds(2)); + + auto [ec] = co_await timer.async_wait( + asio::experimental::as_tuple(use_awaitable)); + + if (ec == asio::error::operation_aborted) + std::cout << "Cancelled\n"; + else if (ec) + std::cout << "Error: " << ec.message() << "\n"; + else + std::cout << "Expired\n"; +} +---- + +== Common Mistakes + +**Forgetting to call `run()`** — The timer won't expire unless `io_context::run()` +is called. The event loop must be running. + +**Timer goes out of scope** — If the timer object is destroyed while a wait is +pending, the operation is cancelled. Keep the timer alive. + +**Using the wrong clock** — `steady_timer` is almost always what you want. +`system_timer` can jump forward or backward if the system clock is adjusted. + +== Next Steps + +* xref:timers/recurring-timer.adoc[Recurring Timer] — Wait repeatedly in a loop +* xref:networking/tcp-client.adoc[TCP Client] — Apply async patterns to networking diff --git a/doc/modules/ROOT/pages/timers/recurring-timer.adoc b/doc/modules/ROOT/pages/timers/recurring-timer.adoc new file mode 100644 index 000000000..11b36b747 --- /dev/null +++ b/doc/modules/ROOT/pages/timers/recurring-timer.adoc @@ -0,0 +1,219 @@ +// +// Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + += Recurring Timer + +Learn how to implement repeating timers with coroutines. + +== Simple Loop + +The most straightforward approach is a loop that resets the timer after each expiry: + +[source,cpp] +---- +#include +#include + +namespace asio = boost::asio; +using asio::awaitable; +using asio::use_awaitable; + +awaitable heartbeat() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor); + + for (int i = 0; i < 5; ++i) + { + timer.expires_after(std::chrono::seconds(1)); + co_await timer.async_wait(use_awaitable); + std::cout << "Tick " << i << "\n"; + } +} + +int main() +{ + asio::io_context ctx; + asio::co_spawn(ctx, heartbeat(), asio::detached); + ctx.run(); +} +---- + +Output: +---- +Tick 0 +Tick 1 +Tick 2 +Tick 3 +Tick 4 +---- + +== Avoiding Timer Drift + +The simple loop can drift over time because it waits *after* processing. +If processing takes 100ms, a 1-second interval becomes 1.1 seconds. + +To maintain precise intervals, calculate expiry from the previous expiry time: + +[source,cpp] +---- +awaitable precise_heartbeat() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor); + + auto next_tick = std::chrono::steady_clock::now(); + + for (int i = 0; i < 5; ++i) + { + next_tick += std::chrono::seconds(1); + timer.expires_at(next_tick); + co_await timer.async_wait(use_awaitable); + + std::cout << "Tick " << i << "\n"; + + // Simulate some processing time + // The next tick is still calculated from the scheduled time, + // not from when processing finishes + } +} +---- + +== Infinite Loop with Graceful Shutdown + +For a timer that runs indefinitely until cancelled: + +[source,cpp] +---- +awaitable infinite_heartbeat() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor); + + try + { + for (;;) + { + timer.expires_after(std::chrono::seconds(1)); + co_await timer.async_wait(use_awaitable); + std::cout << "Tick\n"; + } + } + catch (const boost::system::system_error& e) + { + if (e.code() != asio::error::operation_aborted) + throw; + // Normal shutdown via cancellation + } +} + +int main() +{ + asio::io_context ctx; + + // Set up signal handling for graceful shutdown + asio::signal_set signals(ctx, SIGINT, SIGTERM); + signals.async_wait([&](auto, auto) { + ctx.stop(); + }); + + asio::co_spawn(ctx, infinite_heartbeat(), asio::detached); + ctx.run(); + + std::cout << "Shutdown complete\n"; +} +---- + +== Multiple Concurrent Timers + +You can run multiple timers in parallel using `co_spawn`: + +[source,cpp] +---- +awaitable timer_a() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor); + + for (int i = 0; i < 3; ++i) + { + timer.expires_after(std::chrono::milliseconds(500)); + co_await timer.async_wait(use_awaitable); + std::cout << "A\n"; + } +} + +awaitable timer_b() +{ + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor); + + for (int i = 0; i < 3; ++i) + { + timer.expires_after(std::chrono::milliseconds(700)); + co_await timer.async_wait(use_awaitable); + std::cout << "B\n"; + } +} + +int main() +{ + asio::io_context ctx; + asio::co_spawn(ctx, timer_a(), asio::detached); + asio::co_spawn(ctx, timer_b(), asio::detached); + ctx.run(); +} +---- + +Both timers run concurrently on the same thread. The output will interleave: +---- +A +B +A +A +B +B +---- + +== Timeout Pattern + +A common pattern is to race a timer against another operation: + +[source,cpp] +---- +#include + +using namespace asio::experimental::awaitable_operators; + +awaitable with_timeout() +{ + auto executor = co_await asio::this_coro::executor; + + asio::steady_timer timer(executor, std::chrono::seconds(5)); + asio::ip::tcp::socket socket(executor); + + // Race: connect vs timeout + // First one to complete wins, the other is cancelled + auto result = co_await ( + socket.async_connect( + asio::ip::tcp::endpoint(asio::ip::make_address("93.184.216.34"), 80), + use_awaitable + ) + || timer.async_wait(use_awaitable) + ); + + if (result.index() == 0) + std::cout << "Connected\n"; + else + std::cout << "Timeout\n"; +} +---- + +== Next Steps + +* xref:networking/tcp-client.adoc[TCP Client] — Network I/O with timeouts +* xref:threading/strands.adoc[Strands] — Thread-safe timer access From f2b38f10ab93857466127744af08aa54655cbad1 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sat, 17 Jan 2026 01:05:58 -0800 Subject: [PATCH 2/2] Build Antora docs in GHA --- doc/Jamfile.v2 | 7 +++++++ doc/build_antora.bat | 25 +++++++++++++++++++++++++ doc/build_antora.sh | 26 ++++++++++++++++++++++++++ doc/local-playbook.yml | 36 ++++++++++++++++++++++++++++++++++++ doc/package.json | 15 +++++++++++++++ 5 files changed, 109 insertions(+) create mode 100644 doc/build_antora.bat create mode 100644 doc/build_antora.sh create mode 100644 doc/local-playbook.yml create mode 100644 doc/package.json diff --git a/doc/Jamfile.v2 b/doc/Jamfile.v2 index add938154..a3025d70d 100644 --- a/doc/Jamfile.v2 +++ b/doc/Jamfile.v2 @@ -15,6 +15,8 @@ xml standalone_doc asio.qbk ; +explicit standalone_doc ; + install images : overview/proactor.png @@ -35,6 +37,8 @@ install images html/boost_asio ; +explicit images ; + local example-names = cpp11/allocation cpp11/buffers cpp11/chat cpp11/deferred cpp11/echo cpp11/executors cpp11/fork cpp11/futures cpp11/handler_tracking cpp11/http/client cpp11/http/server cpp11/http/server2 cpp11/http/server3 @@ -49,6 +53,7 @@ local example-names = cpp11/allocation cpp11/buffers cpp11/chat cpp11/deferred for local l in $(example-names) { install ex_$(l) : [ glob ../example/$(l)/*.*pp ] : html/boost_asio/example/$(l) ; + explicit ex_$(l) ; } boostbook standalone @@ -66,6 +71,8 @@ boostbook standalone pdf:boost.url.prefix=http://www.boost.org/doc/libs/release/libs/asio/doc/html ; +explicit standalone ; + ######################################################################## # HTML documentation for $(BOOST_ROOT)/doc/html diff --git a/doc/build_antora.bat b/doc/build_antora.bat new file mode 100644 index 000000000..229370d21 --- /dev/null +++ b/doc/build_antora.bat @@ -0,0 +1,25 @@ +@echo off +setlocal + +REM +REM Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +REM +REM Distributed under the Boost Software License, Version 1.0. (See accompanying +REM file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +REM + +if "%~1"=="" ( + echo No playbook supplied, using default playbook + set "PLAYBOOK=local-playbook.yml" +) else ( + set "PLAYBOOK=%~1" +) + +echo Building documentation with Antora... +echo Installing npm dependencies... +call npm ci + +echo Building docs in custom dir... +set "PATH=%CD%\node_modules\.bin;%PATH%" +call npx antora --clean --fetch "%PLAYBOOK%" +echo Done diff --git a/doc/build_antora.sh b/doc/build_antora.sh new file mode 100644 index 000000000..a1f81cda2 --- /dev/null +++ b/doc/build_antora.sh @@ -0,0 +1,26 @@ +# +# Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +set -xe + +if [ $# -eq 0 ] + then + echo "No playbook supplied, using default playbook" + PLAYBOOK="local-playbook.yml" + else + PLAYBOOK=$1 +fi + +echo "Building documentation with Antora..." +echo "Installing npm dependencies..." +npm ci + +echo "Building docs in custom dir..." +PATH="$(pwd)/node_modules/.bin:${PATH}" +export PATH +npx antora --clean --fetch "$PLAYBOOK" +echo "Done" diff --git a/doc/local-playbook.yml b/doc/local-playbook.yml new file mode 100644 index 000000000..0ff877ff1 --- /dev/null +++ b/doc/local-playbook.yml @@ -0,0 +1,36 @@ +# +# Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# + +site: + title: Boost.Asio + url: https://antora.cppalliance.org/develop/lib/doc + start_page: asio::index.adoc + robots: allow + keys: + repo_url: 'https://github.com/boostorg/asio' + +content: + sources: + - url: .. + start_path: doc + edit_url: 'https://github.com/boostorg/asio/edit/{refname}/{path}' + +ui: + bundle: + url: https://github.com/boostorg/website-v2-docs/releases/download/ui-develop/ui-bundle.zip + snapshot: true + +antora: + extensions: + - require: '@asciidoctor/tabs' + +asciidoc: + attributes: + # Enable pagination + page-pagination: '' + extensions: + - '@asciidoctor/tabs' diff --git a/doc/package.json b/doc/package.json new file mode 100644 index 000000000..ae45edf32 --- /dev/null +++ b/doc/package.json @@ -0,0 +1,15 @@ +{ + "name": "boost-asio-docs", + "version": "1.0.0", + "description": "Boost.Asio Antora documentation", + "private": true, + "devDependencies": { + "@antora/cli": "3.1.14", + "@antora/site-generator": "3.1.14", + "antora": "3.1.14" + }, + "dependencies": { + "@antora/expand-path-helper": "^3.0.0", + "@asciidoctor/tabs": "^1.0.0-beta.6" + } +}