Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/design/physical-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ private:
| Type | Base(s) | Key Operations |
| ------------ | --------- | ------------------------------------------------------------------ |
| tcp_socket | io_stream | connect(endpoint), shutdown(), TCP options, local/remote_endpoint() |
| tcp_acceptor | io_object | listen(endpoint, backlog), accept(tcp_socket&), local_endpoint() |
| tcp_acceptor | io_object | bind(endpoint), listen(backlog), accept(tcp_socket&), local_endpoint() |
| resolver | io_object | resolve(host, service), resolve(endpoint), cancel() |

**Types table** - Sockets (UDP):
Expand Down
37 changes: 24 additions & 13 deletions doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Code snippets assume:
#include <boost/corosio/tcp_server.hpp>
#include <boost/capy/task.hpp>
#include <boost/capy/buffers.hpp>
#include <boost/capy/write.hpp>

namespace corosio = boost::corosio;
namespace capy = boost::capy;
Expand Down Expand Up @@ -103,7 +104,7 @@ capy::task<> echo_server::worker::do_session()
auto [ec, n] = co_await sock_.read_some(
capy::mutable_buffer(buf_, sizeof buf_));

auto [wec, wn] = co_await corosio::write(
auto [wec, wn] = co_await capy::write(
sock_, capy::const_buffer(buf_, n));

if (wec || ec)
Expand All @@ -119,28 +120,37 @@ Notice:
* We reuse the worker's buffer across reads
* `read_some()` returns when _any_ data arrives — it may deliver bytes alongside an error
* We always write before checking the error (advance-then-check); writing zero bytes is a no-op
* `corosio::write()` writes _all_ data (it's a composed operation)
* `capy::write()` writes _all_ data (it's a composed operation)
* When the coroutine ends, the launcher returns the worker to the pool

== Server Construction

The server constructor populates the worker pool:
A helper builds the worker pool, then the constructor hands it to
`set_workers()`:

[source,cpp]
----
static std::vector<std::unique_ptr<worker_base>>
make_workers(corosio::io_context& ctx, int n)
{
std::vector<std::unique_ptr<worker_base>> v;
v.reserve(n);
for (int i = 0; i < n; ++i)
v.push_back(std::make_unique<worker>(ctx));
return v;
}

public:
echo_server(corosio::io_context& ctx, int max_workers)
: tcp_server(ctx, ctx.get_executor())
{
wv_.reserve(max_workers);
for (int i = 0; i < max_workers; ++i)
wv_.emplace<worker>(ctx);
set_workers(make_workers(ctx, max_workers));
}
};
----

Workers are stored polymorphically via `wv_.emplace<T>()`, allowing different
worker types if needed.
Workers are owned polymorphically through `std::unique_ptr<worker_base>`, so
`set_workers()` accepts any range of worker types.

== Main Function

Expand Down Expand Up @@ -194,15 +204,15 @@ The `tcp_server` framework provides:

=== Why Composed Write?

The `corosio::write()` free function ensures all data is sent:
The `capy::write()` free function ensures all data is sent:

[source,cpp]
----
// write_some: may write partial data
auto [ec, n] = co_await sock.write_some(buf); // n might be < buf.size()

// write: writes all data or fails
auto [ec, n] = co_await corosio::write(sock, buf); // n == buf.size() or error
auto [ec, n] = co_await capy::write(sock, buf); // n == buf.size() or error
----

For echo servers, we want complete message delivery.
Expand All @@ -215,7 +225,7 @@ with advance-then-check, we always act on `n` before inspecting `ec`:
[source,cpp]
----
auto [ec, n] = co_await sock.read_some(buf);
auto [wec, wn] = co_await corosio::write(sock, const_buffer(buf.data(), n));
auto [wec, wn] = co_await capy::write(sock, const_buffer(buf.data(), n));
if (wec || ec)
break; // Normal termination path
----
Expand All @@ -225,7 +235,8 @@ With exceptions, EOF would require a try-catch:
[source,cpp]
----
try {
auto n = (co_await sock.read_some(buf)).value();
auto [ec, n] = co_await sock.read_some(buf);
if (ec) throw std::system_error(ec);
} catch (...) {
// EOF is an exception here
}
Expand Down Expand Up @@ -254,7 +265,7 @@ World

== Next Steps

* xref:3b.http-client.adoc[HTTP Client] — Build an HTTP client
* xref:3.tutorials/3b.http-client.adoc[HTTP Client] — Build an HTTP client
* xref:../4.guide/4k.tcp-server.adoc[TCP Server Guide] — Deep dive into tcp_server
* xref:../4.guide/4d.sockets.adoc[Sockets Guide] — Deep dive into socket operations
* xref:../4.guide/4g.composed-operations.adoc[Composed Operations] — Understanding read/write
115 changes: 84 additions & 31 deletions doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ Code snippets assume:
[source,cpp]
----
#include <boost/corosio.hpp>
#include <boost/corosio/ipv4_address.hpp>
#include <boost/capy/task.hpp>
#include <boost/capy/ex/run_async.hpp>
#include <boost/capy/buffers.hpp>
#include <boost/capy/buffers/string_dynamic_buffer.hpp>
#include <boost/capy/error.hpp>
#include <boost/url/ipv4_address.hpp>
#include <boost/capy/read.hpp>
#include <boost/capy/write.hpp>

namespace corosio = boost::corosio;
namespace capy = boost::capy;
Expand All @@ -40,7 +43,8 @@ Making an HTTP request involves:
4. Reading the response
5. Handling connection close (EOF)

We'll use the exception-based pattern with `.value()` for concise code.
We'll use the exception-based pattern, throwing `std::system_error` on
failure, for concise code.

== Building the Request

Expand Down Expand Up @@ -71,48 +75,56 @@ capy::task<void> do_request(
{
// Build and send the request
std::string request = build_request(host);
(co_await corosio::write(
stream, capy::const_buffer(request.data(), request.size()))).value();
if (auto [ec, n] = co_await capy::write(
stream, capy::const_buffer(request.data(), request.size())); ec)
throw std::system_error(ec);

// Read the entire response
std::string response;
auto [ec, n] = co_await corosio::read(stream, response);
auto [ec, n] = co_await capy::read(
stream, capy::string_dynamic_buffer(&response));

// EOF is expected when server closes connection
if (ec && ec != capy::error::eof)
throw boost::system::system_error(ec);
// Reading into a dynamic buffer completes with success at EOF
if (ec && ec != capy::cond::eof)
throw std::system_error(ec);

std::cout << response << std::endl;
}
----

Key points:

* `.value()` on the write throws if writing fails
* `corosio::read(stream, string)` reads until EOF
* We check for EOF explicitly because it's expected here
* The write throws if writing fails
* `capy::read(stream, capy::string_dynamic_buffer(&response))` reads until EOF
* Reading into a dynamic buffer completes with *success* at end-of-stream — it
returns no error and reports the total bytes read. The `!= capy::cond::eof`
check is a harmless defensive guard, not the thing that signals
end-of-response

== The Connection Coroutine

[source,cpp]
----
capy::task<void> run_client(
corosio::io_context& ioc,
boost::urls::ipv4_address addr,
corosio::ipv4_address addr,
std::uint16_t port)
{
corosio::tcp_socket s(ioc);
s.open();

// Connect (throws on error)
(co_await s.connect(corosio::endpoint(addr, port))).value();
if (auto [ec] = co_await s.connect(corosio::endpoint(addr, port)); ec)
throw std::system_error(ec);

co_await do_request(s, addr.to_string());
}
----

The socket must be opened before connecting. We pass the socket as an
`io_stream&` to `do_request`, enabling code reuse with TLS streams later.
`io_stream&` to `do_request`, so the same function works with any plain
socket. TLS streams have a different type and need their own overload, as
shown below.

== Main Function

Expand All @@ -128,8 +140,8 @@ int main(int argc, char* argv[])
}

// Parse IP address
auto addr_result = boost::urls::parse_ipv4_address(argv[1]);
if (!addr_result)
corosio::ipv4_address addr;
if (auto ec = corosio::parse_ipv4_address(argv[1], addr); ec)
{
std::cerr << "Invalid IP address: " << argv[1] << "\n";
return 1;
Expand All @@ -139,25 +151,28 @@ int main(int argc, char* argv[])

corosio::io_context ioc;
capy::run_async(ioc.get_executor())(
run_client(ioc, *addr_result, port));
run_client(ioc, addr, port));
ioc.run();
}
----

== Reading Until EOF

The `corosio::read(io_stream&, std::string&)` overload reads until EOF:
Wrapping a `std::string` in `capy::string_dynamic_buffer` lets
`capy::read` grow it as data arrives, reading until EOF:

[source,cpp]
----
std::string response;
auto [ec, n] = co_await corosio::read(stream, response);
auto [ec, n] = co_await capy::read(
stream, capy::string_dynamic_buffer(&response));
----

This function:
This:

* Automatically grows the string as needed
* Returns `capy::error::eof` when the connection closes
* Completes with *success* (no error) when the connection closes, since the
dynamic-buffer read treats EOF as the natural end of the stream
* Returns the total bytes read in `n`

== Error vs. Exception Patterns
Expand All @@ -183,7 +198,8 @@ With exceptions:

[source,cpp]
----
(co_await s.connect(ep)).value(); // Throws on error
if (auto [ec] = co_await s.connect(ep); ec) // Throw on error
throw std::system_error(ec);
----

Both are valid. Use exceptions when errors are exceptional; use structured
Expand Down Expand Up @@ -216,36 +232,73 @@ Content-Type: text/html; charset=UTF-8

== Adding TLS Support

To make HTTPS requests, wrap the socket in a `wolfssl_stream`:
To make HTTPS requests, wrap the connected socket in a `wolfssl_stream`.
A `wolfssl_stream` is not an `io_stream`, so it needs its own
`do_request` overload taking `corosio::tls_stream&`:

[source,cpp]
----
#include <boost/corosio/wolfssl_stream.hpp>

capy::task<void> do_request(
corosio::tls_stream& stream,
std::string_view host)
{
std::string request = build_request(host);
if (auto [ec, n] = co_await capy::write(
stream, capy::const_buffer(request.data(), request.size())); ec)
throw std::system_error(ec);

std::string response;
auto [ec, n] = co_await capy::read(
stream, capy::string_dynamic_buffer(&response));
// As with the plain stream, the dynamic-buffer read completes with
// success at EOF; this check is a defensive guard.
if (ec && ec != capy::cond::eof)
throw std::system_error(ec);

std::cout << response << std::endl;
}

capy::task<void> run_https_client(
corosio::io_context& ioc,
boost::urls::ipv4_address addr,
corosio::ipv4_address addr,
std::uint16_t port,
std::string_view hostname)
{
corosio::tcp_socket s(ioc);
s.open();

(co_await s.connect(corosio::endpoint(addr, port))).value();
if (auto [ec] = co_await s.connect(corosio::endpoint(addr, port)); ec)
throw std::system_error(ec);

// Configure the TLS context
corosio::tls_context ctx;
ctx.set_hostname(hostname);
if (auto ec = ctx.set_default_verify_paths(); ec)
throw std::system_error(ec);
if (auto ec = ctx.set_verify_mode(corosio::tls_verify_mode::peer); ec)
throw std::system_error(ec);

// Wrap in TLS
corosio::wolfssl_stream secure(s);
(co_await secure.handshake(corosio::wolfssl_stream::client)).value();
// Wrap the connected socket without taking ownership (pointer form)
corosio::wolfssl_stream secure(&s, ctx);
if (auto [ec] = co_await secure.handshake(
corosio::wolfssl_stream::client); ec)
throw std::system_error(ec);

co_await do_request(secure, hostname);

if (auto [ec] = co_await secure.shutdown(); ec)
throw std::system_error(ec);
}
----

The `do_request` function works unchanged because both `socket` and
`wolfssl_stream` inherit from `io_stream`.
The TLS overload mirrors the plain one: `capy::read` and `capy::write`
work with `tls_stream` exactly as they do with `io_stream`. Only the
parameter type and the surrounding handshake/shutdown differ.

== Next Steps

* xref:3c.dns-lookup.adoc[DNS Lookup] — Resolve hostnames to addresses
* xref:3.tutorials/3c.dns-lookup.adoc[DNS Lookup] — Resolve hostnames to addresses
* xref:../4.guide/4l.tls.adoc[TLS Guide] — WolfSSL integration details
* xref:../4.guide/4g.composed-operations.adoc[Composed Operations] — How read/write work
Loading
Loading