diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 21436f70..926fe722 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -1,30 +1,28 @@ * xref:index.adoc[Introduction] * xref:quick-start.adoc[Quick Start] -* Tutorials -** xref:tutorials/echo-server.adoc[Echo Server] -** xref:tutorials/http-client.adoc[HTTP Client] -** xref:tutorials/dns-lookup.adoc[DNS Lookup] -** xref:tutorials/tls-context.adoc[TLS Context Configuration] -* Guide -** xref:guide/tcp-networking.adoc[TCP/IP Networking] -** xref:guide/concurrent-programming.adoc[Concurrent Programming] -** xref:guide/io-context.adoc[I/O Context] -** xref:guide/sockets.adoc[Sockets] -** xref:guide/tcp_acceptor.adoc[Acceptors] -** xref:guide/endpoints.adoc[Endpoints] -** xref:guide/composed-operations.adoc[Composed Operations] -** xref:guide/timers.adoc[Timers] -** xref:guide/signals.adoc[Signal Handling] -** xref:guide/resolver.adoc[Name Resolution] -** xref:guide/tcp-server.adoc[TCP Server] -** xref:guide/tls.adoc[TLS Encryption] -** xref:guide/error-handling.adoc[Error Handling] -** xref:guide/buffers.adoc[Buffer Sequences] -* Concepts -** xref:reference/design-rationale.adoc[Design Rationale] -** xref:concepts/affine-awaitables.adoc[Affine Awaitables] -* Testing -** xref:testing/mocket.adoc[Mock Sockets] +* xref:2.networking-tutorial/2.intro.adoc[Networking Tutorial] +* xref:3.tutorials/3.intro.adoc[Tutorials] +** xref:3.tutorials/3a.echo-server.adoc[Echo Server] +** xref:3.tutorials/3b.http-client.adoc[HTTP Client] +** xref:3.tutorials/3c.dns-lookup.adoc[DNS Lookup] +** xref:3.tutorials/3d.tls-context.adoc[TLS Context Configuration] +* xref:4.guide/4.intro.adoc[Guide] +** xref:4.guide/4a.tcp-networking.adoc[TCP/IP Networking] +** xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] +** xref:4.guide/4c.io-context.adoc[I/O Context] +** xref:4.guide/4d.sockets.adoc[Sockets] +** xref:4.guide/4e.tcp-acceptor.adoc[Acceptors] +** xref:4.guide/4f.endpoints.adoc[Endpoints] +** xref:4.guide/4g.composed-operations.adoc[Composed Operations] +** xref:4.guide/4h.timers.adoc[Timers] +** xref:4.guide/4i.signals.adoc[Signal Handling] +** xref:4.guide/4j.resolver.adoc[Name Resolution] +** xref:4.guide/4k.tcp-server.adoc[TCP Server] +** xref:4.guide/4l.tls.adoc[TLS Encryption] +** xref:4.guide/4m.error-handling.adoc[Error Handling] +** xref:4.guide/4n.buffers.adoc[Buffer Sequences] +* xref:5.testing/5.intro.adoc[Testing] +** xref:5.testing/5a.mocket.adoc[Mock Sockets] * xref:reference:boost/corosio.adoc[Reference] -* xref:reference/glossary.adoc[Glossary] -* xref:reference/benchmark-report.adoc[Benchmarks] +* xref:glossary.adoc[Glossary] +* xref:benchmark-report.adoc[Benchmarks] diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2.intro.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2.intro.adoc new file mode 100644 index 00000000..9dd132b7 --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2.intro.adoc @@ -0,0 +1,18 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Networking Tutorial + +Every network application starts with a conversation between two programs. One program asks a question, the other answers, and useful work happens across the wire. This section walks you through that conversation from the ground up, building your understanding of networked programming with Corosio one concept at a time. + +You will begin with the fundamentals: what happens when your program opens a connection, how bytes travel between machines, and how the operating system manages all of it on your behalf. From there you will progress to writing real I/O code -- sending data, receiving responses, and handling the inevitable errors that arise when communicating over an unreliable medium. + +As your confidence grows, the material advances into the patterns that distinguish production-quality network code from toy examples. You will see how asynchronous I/O lets a single thread juggle thousands of connections without blocking, how coroutines make that concurrency feel sequential and natural, and how the event loop ties it all together. + +By the end, you will have a practical understanding of TCP networking sufficient to build clients, servers, and everything in between -- using modern C++ and the coroutine-first abstractions that Corosio provides. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2a.how-you-connect.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2a.how-you-connect.adoc new file mode 100644 index 00000000..91ea8ae0 --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2a.how-you-connect.adoc @@ -0,0 +1,78 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += What Happens When You Connect + +Your program calls `connect`, and a moment later bytes arrive on a machine halfway around the world. Between those two events, a handful of protocols cooperate to make it happen. Understanding what each one does -- and where your code fits in the picture -- is the foundation for everything that follows. + +== The Journey of a Message + +Suppose your program wants to send the string `"hello"` to a server at address `192.0.2.50` on port `8080`. Here is what happens, roughly, from the moment you call `send` to the moment the server's application reads the data: + +. Your application hands `"hello"` to TCP through the operating system's socket interface. +. TCP prepends its own header -- containing the destination port (`8080`), a sequence number to track byte position, and a checksum. The five bytes of `"hello"` are now wrapped inside a TCP *segment*. +. The TCP segment is passed down to IP, which prepends another header -- containing the destination address (`192.0.2.50`), the source address of your machine, and a time-to-live field that prevents the packet from circling the network forever. The whole thing is now an IP *packet*. +. The IP packet is handed to the network hardware, which frames it for whatever physical medium connects your machine to the next hop -- Ethernet, Wi-Fi, or something else. This frame goes out on the wire. + +At each stage, the original data gets wrapped in another header, like putting a letter in an envelope, then putting that envelope in a shipping box. This is called *encapsulation*: each protocol adds its own metadata around the payload it received from above. + +== Arriving at the Other End + +When the packet reaches the destination machine, the process runs in reverse: + +. The network hardware strips the link-level framing and hands the IP packet to the operating system. +. IP examines its header, confirms the packet is addressed to this machine, and looks at the *protocol* field to determine whether the payload belongs to TCP or UDP. It strips the IP header and passes the segment up. +. TCP examines the destination port number in its header. Port `8080` identifies which application should receive the data. TCP strips its own header and delivers `"hello"` to the server's socket. + +This reverse process is called *demultiplexing*. The destination machine receives a stream of raw packets from the network and uses the headers to sort each one to the correct protocol, then to the correct application. Without demultiplexing, every program on the machine would see every packet, which would be chaos. + +== Only a Few Protocols Matter + +The full roster of internet protocols is enormous, but for application development you only need to know a handful: + +*IP* (Internet Protocol):: Handles addressing and routing. Every packet carries a source and destination IP address, and routers use these addresses to forward the packet toward its destination one hop at a time. IP makes no guarantees about delivery -- packets can arrive out of order, arrive twice, or not arrive at all. + +*TCP* (Transmission Control Protocol):: Builds a reliable, ordered byte stream on top of IP. Your program writes bytes into one end and they come out in the same order at the other end, even if the underlying packets took different routes or needed to be retransmitted. TCP also handles flow control so a fast sender does not overwhelm a slow receiver. + +*UDP* (User Datagram Protocol):: A thinner alternative to TCP. Each send produces exactly one datagram, and each receive consumes exactly one. There are no guarantees about delivery or ordering. UDP is the right choice when speed matters more than completeness -- things like real-time audio, video, or DNS lookups. + +*DNS* (Domain Name System):: Translates human-readable names like `www.example.com` into IP addresses like `93.184.215.14`. Almost every connection your program makes begins with a DNS query, even if you never see it explicitly. + +These four, plus the hardware underneath, account for nearly everything that happens when your program communicates over the internet. Later sections examine each one in detail. + +== Encapsulation in Practice + +To make this concrete, consider the bytes on the wire when your program sends `"hello"` over TCP. The final packet contains, from outermost to innermost: + +[cols="1,3"] +|=== +| Component | Contents + +| Ethernet header +| Source and destination MAC addresses, EtherType field indicating IP + +| IP header +| Version, header length, total length, TTL, protocol (TCP), source IP, destination IP + +| TCP header +| Source port, destination port, sequence number, acknowledgment number, flags, window size, checksum + +| Application data +| The five bytes: `h`, `e`, `l`, `l`, `o` +|=== + +Each protocol only reads and removes its own header. TCP never inspects the IP header. IP never inspects the Ethernet header. This separation is what allows protocols to be mixed and matched independently -- you can run TCP over Wi-Fi or over a fiber-optic link without changing a single line of your application code. + +== Where Your Code Lives + +As an application developer, you operate at the top of this stack. You open a socket, connect to an address and port, and read or write data. The operating system handles TCP segmentation, IP routing, and hardware framing on your behalf. You never construct an IP header or compute a TCP checksum by hand. + +This is a good thing. The protocols below your application are mature, heavily optimized, and implemented in the kernel. Your job is to use them correctly -- to understand what TCP promises and what it does not, to know when UDP is a better fit, and to handle the errors that arise when the network does not cooperate. + +That understanding begins with the next section: how machines are identified on the internet. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2b.internet-addresses.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2b.internet-addresses.adoc new file mode 100644 index 00000000..c5ec2a05 --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2b.internet-addresses.adoc @@ -0,0 +1,90 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Addressing Machines on the Internet + +Every packet traveling the internet carries two addresses: where it came from and where it is going. These addresses are not arbitrary labels. They have internal structure that routers use to make forwarding decisions, and understanding that structure explains why networks behave the way they do. + +== IPv4 Addresses + +An IPv4 address is a 32-bit number, written as four decimal values separated by dots. Each value represents one byte, so it ranges from 0 to 255: + +---- +192.168.1.42 +---- + +That is the human-readable form. Under the hood it is just 32 bits: `11000000.10101000.00000001.00101010`. Four billion possible addresses sounds like a lot until you consider that every phone, laptop, thermostat, and security camera on the planet needs one. The world ran out of fresh IPv4 addresses years ago, and workarounds like network address translation (NAT) keep things functioning -- but the shortage is real. + +== Structure Inside the Address + +An IP address is not a flat identifier like a serial number. It is split into two parts: a *network* portion and a *host* portion. The network portion identifies which network the machine belongs to. The host portion identifies the specific machine within that network. + +Consider a company with the address range `10.4.0.0` through `10.4.255.255`. The first two bytes (`10.4`) identify the company's network. The last two bytes identify individual machines. A router does not need to know about every machine -- it only needs to know how to reach the `10.4` network, and then the local network handles the rest. + +This division is what makes routing scalable. The internet has billions of devices, but routers only need a few hundred thousand routing entries because they route to *networks*, not to individual machines. + +== Subnet Masks + +The *subnet mask* tells you where the network portion ends and the host portion begins. It is a 32-bit value where all the network bits are set to 1 and all the host bits are set to 0: + +---- +Address: 192.168.1.42 +Subnet mask: 255.255.255.0 +---- + +In this example, the first 24 bits are the network (`192.168.1`) and the last 8 bits are the host (`42`). This is often written in shorthand as `192.168.1.42/24`, where `/24` means "the first 24 bits are the network." + +A `/16` mask (`255.255.0.0`) gives you a larger network with up to 65,534 usable host addresses. A `/28` mask (`255.255.255.240`) gives you a tiny network with just 14 usable hosts. Network administrators choose the mask based on how many machines they need to support. + +Subnet masks also let a single organization divide its address space into smaller pieces. A company allocated `172.20.0.0/16` might carve it into subnets: `172.20.1.0/24` for the engineering floor, `172.20.2.0/24` for the sales office, and so on. Each subnet functions as its own small network with its own router, reducing broadcast traffic and improving security. + +== Special Addresses + +Several address ranges have reserved meanings: + +`127.0.0.1` (loopback):: Traffic sent here never leaves the machine. It goes down through the protocol stack, turns around, and comes back up. This is how your program talks to a server running on the same computer. The entire `127.0.0.0/8` range is reserved for loopback, but `127.0.0.1` is the one you will see in practice. + +`0.0.0.0`:: When used as a source address, it means "this machine, but I don't know my address yet." When used to bind a socket, it means "listen on every available network interface." + +Private address ranges:: Three ranges are set aside for internal networks that do not route on the public internet: +* `10.0.0.0/8` -- roughly 16 million addresses +* `172.16.0.0/12` -- roughly 1 million addresses +* `192.168.0.0/16` -- roughly 65,000 addresses + +If you have ever connected to a home Wi-Fi network and received an address like `192.168.0.105`, you were using a private address. Your router translates it to a public address using NAT before forwarding your traffic to the internet. + +Broadcast:: The address `255.255.255.255` sends a packet to every machine on the local network. Subnet-specific broadcasts exist too -- on the `192.168.1.0/24` network, the broadcast address is `192.168.1.255`. + +== IPv6 + +IPv6 replaces the 32-bit address with a 128-bit one, written as eight groups of four hexadecimal digits separated by colons: + +---- +2001:0db8:85a3:0000:0000:8a2e:0370:7334 +---- + +Leading zeros within a group can be dropped, and a single run of consecutive all-zero groups can be replaced with `::`: + +---- +2001:db8:85a3::8a2e:370:7334 +---- + +128 bits gives roughly 3.4 x 10^38^ addresses -- enough to assign one to every atom on the surface of the earth and still have room left over. The exhaustion problem goes away entirely. + +The concepts carry over directly. IPv6 addresses still have network and host portions, determined by a prefix length (typically `/64` for a single subnet). Routers still forward based on the network prefix. Loopback is `::1`. The mechanics are the same; only the size changed. + +For the rest of this tutorial, examples use IPv4 notation for brevity. Everything discussed applies equally to IPv6 unless noted otherwise. + +== Why This Matters to You + +When your program connects to `192.168.1.42`, the operating system does not search the entire internet for that address. It examines the destination, compares it against the subnet masks of the local interfaces, and determines whether the target is on the local network or reachable through a gateway. That decision -- local or remote -- is the first routing choice, and it happens because addresses carry structural information. + +Understanding addresses also explains common errors. Connecting to `127.0.0.1` always reaches your own machine. Connecting to a `192.168.x` address from outside the local network fails because private addresses do not route publicly. Binding a server to `0.0.0.0` makes it reachable on all interfaces; binding to `127.0.0.1` restricts it to local connections only. + +The next section covers how you avoid typing IP addresses altogether, using the Domain Name System to turn human-readable names into the numbers that machines actually use. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2c.domain-name-system.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2c.domain-name-system.adoc new file mode 100644 index 00000000..c84e8b9b --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2c.domain-name-system.adoc @@ -0,0 +1,86 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Turning Names into Addresses + +Nobody memorizes `93.184.215.14`. People remember `www.example.com`. But the network stack operates on IP addresses, not names. Something has to translate between the two, and that something is the Domain Name System -- a distributed database spread across thousands of servers worldwide, answering billions of queries a day. + +== The Problem DNS Solves + +IP addresses change. Companies move hosting providers. Servers get replaced. A name like `api.myservice.com` provides a stable identifier that can be pointed at whatever address is current. Your program connects to the name, DNS resolves it to an address behind the scenes, and the connection proceeds. + +Without DNS, every configuration file, bookmark, and link would contain raw IP addresses. Changing a server's address would break every client. DNS decouples the name from the address, and that indirection is what makes the internet manageable. + +== How Resolution Works + +DNS is organized as a tree. At the top is the root, represented by a dot you rarely see. Below the root are the top-level domains (TLDs): `com`, `org`, `net`, `uk`, `io`, and hundreds of others. Below each TLD are the domains registered by organizations: `example.com`, `mycompany.org`. Those domains can contain further subdomains: `api.mycompany.org`, `mail.mycompany.org`. + +When your program asks for the address of `api.mycompany.org`, the resolution proceeds through this tree: + +. Your machine contacts a *recursive resolver* -- usually provided by your ISP or a public service like `8.8.8.8`. This resolver does the heavy lifting on your behalf. +. The resolver asks a *root server*: "Who handles `.org`?" The root server responds with the addresses of the `.org` TLD servers. +. The resolver asks a `.org` TLD server: "Who handles `mycompany.org`?" The TLD server responds with the addresses of the *authoritative name servers* for `mycompany.org`. +. The resolver asks `mycompany.org`'s authoritative server: "What is the address of `api.mycompany.org`?" The authoritative server answers with the IP address. +. The resolver returns the answer to your machine. + +This looks like a lot of round trips, but in practice most of them are skipped thanks to caching. The resolver almost certainly already knows the `.org` TLD servers. It might already have the authoritative servers for `mycompany.org` cached from a previous query. A full walk from the root happens rarely. + +== Resource Records + +DNS does not just map names to addresses. A name can have several types of records associated with it, each serving a different purpose: + +A:: Maps a name to an IPv4 address. This is the most common record type. If you query the A record for `www.example.com`, you get back something like `93.184.215.14`. + +AAAA:: Maps a name to an IPv6 address. Same idea as an A record, but returns a 128-bit address instead of a 32-bit one. + +CNAME:: An alias. It says "this name is actually another name -- go look up that one instead." For example, `www.mycompany.org` might be a CNAME pointing to `lb.hosting-provider.net`, which in turn has an A record with the actual address. + +MX:: Identifies the mail servers responsible for a domain. When someone sends email to `user@mycompany.org`, the sending mail server queries the MX records for `mycompany.org` to find out where to deliver it. + +TXT:: A free-form text record. Often used for domain verification, email authentication (SPF, DKIM), and other administrative purposes. + +NS:: Identifies the authoritative name servers for a domain. These are the servers the resolver contacts in the final step of resolution. + +Your application code rarely deals with individual record types directly. When you resolve a hostname for a TCP connection, the system resolver queries A and AAAA records on your behalf and returns a list of addresses to try. + +== Caching and TTLs + +Every DNS response includes a *time-to-live* (TTL) value, measured in seconds. The TTL tells resolvers and clients how long they can cache the answer before asking again. + +A TTL of 3600 means the answer is good for one hour. A TTL of 60 means it expires in a minute. Domain operators set the TTL based on how frequently the address might change. A stable website might use a TTL of 86400 (one day). A service that needs rapid failover might use a TTL of 30 seconds. + +Caching is what makes DNS fast. Your first connection to `api.mycompany.org` triggers a real lookup, but subsequent connections within the TTL window use the cached result instantly. Your operating system maintains a local cache, your recursive resolver maintains a larger one, and intermediate resolvers along the chain do the same. By the time a popular domain's record expires from one cache, it has already been refreshed by some other query. + +The flip side is that changes do not take effect immediately. If a domain's address changes, clients with a cached copy of the old answer continue using it until the TTL expires. This is why DNS propagation "takes time" -- it is really just caches aging out. + +== DNS Transport + +Most DNS queries travel over UDP. A typical query and response fit within a single datagram, making UDP the natural fit: fast, no connection setup, minimal overhead. Port 53 is the standard port for DNS traffic. + +When a response is too large for a single UDP datagram -- which can happen with domains that have many records or with DNSSEC signatures -- the server sets a flag indicating truncation. The client then retries the query over TCP, which can handle arbitrarily large responses by streaming the data. + +The choice between UDP and TCP is handled automatically by the DNS resolver. Your application never needs to think about it. + +== Reverse Lookups + +Standard DNS maps a name to an address. A *reverse lookup* goes the other direction: given an IP address, it returns the associated hostname. + +Reverse lookups use a special domain called `in-addr.arpa` for IPv4 (and `ip6.arpa` for IPv6). The IP address is written in reverse order and appended to this domain. To look up the name for `192.0.2.10`, you query `10.2.0.192.in-addr.arpa` and ask for its PTR (pointer) record. + +Reverse lookups are not guaranteed to work. The owner of an IP address block has to set up PTR records deliberately, and many do not bother. They are mostly used for logging, diagnostics, and email server verification -- not for establishing connections. + +== Why This Matters to You + +When your program resolves a hostname, the operating system performs the DNS lookup and returns a list of IP addresses. If the name has both A and AAAA records, you may get IPv4 and IPv6 addresses back. A well-written client tries them in order until one succeeds. + +DNS failures are among the most common causes of connection errors. "Could not resolve host" means DNS returned nothing -- either the name does not exist, the authoritative server is down, or the network path to the resolver is broken. Understanding the resolution chain helps you diagnose where the failure occurred. + +DNS also explains behaviors that otherwise seem mysterious. A server migration that changes the IP address behind a name may take hours to propagate because caches hold onto the old answer until the TTL expires. A name that resolves fine from one machine but not another may be hitting different recursive resolvers with different cache states. + +The next section looks at URLs -- the format that combines a hostname, a port, and a resource path into a single string your program can act on. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2d.urls.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2d.urls.adoc new file mode 100644 index 00000000..b2d1a104 --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2d.urls.adoc @@ -0,0 +1,105 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += URLs and Resource Identification + +A URL packs everything your program needs to reach a resource into a single string: which protocol to speak, which machine to contact, and what to ask for once connected. You see URLs constantly -- in browser address bars, configuration files, API documentation, and log messages. Understanding their structure turns an opaque string into a set of actionable instructions. + +== Anatomy of a URL + +Consider this URL: + +---- +https://api.weather.co:443/v2/forecast?city=tokyo&days=5#summary +---- + +It breaks down into six components: + +Scheme (`https`):: The protocol your program uses to communicate. `https` means HTTP over TLS (encrypted). `http` means HTTP without encryption. Other schemes exist -- `ftp`, `ssh`, `ws` (WebSocket) -- each implying a different protocol and set of rules. + +Host (`api.weather.co`):: The machine to connect to. This is a domain name that DNS resolves to an IP address. It could also be a raw IP address like `192.0.2.50`, but domain names are far more common. + +Port (`443`):: The port number on the destination machine. This identifies which process should handle the connection. When omitted, the port is inferred from the scheme: `80` for `http`, `443` for `https`. Most URLs omit the port because the defaults are correct. + +Path (`/v2/forecast`):: The specific resource being requested. The server interprets this however it chooses -- it might map to a file on disk, a database query, or a function call. The path is sent to the server as part of the request. + +Query (`city=tokyo&days=5`):: Parameters passed to the server, formatted as key-value pairs separated by `&`. The query string follows a `?` and provides additional input for the request. Not every URL has one. + +Fragment (`summary`):: A client-side marker. The fragment is *not* sent to the server. Browsers use it to scroll to a specific section of a page. In API contexts it is rarely used. + +== The Authority Section + +The host and optional port together form the *authority* of the URL. In the example above, the authority is `api.weather.co:443`. This is the part that determines which machine your program connects to. + +When the host is a domain name, your program resolves it through DNS (as described in the previous section) to obtain an IP address. When the host is a literal IPv6 address, it must be enclosed in square brackets to avoid ambiguity with the colons: + +---- +http://[2001:db8::1]:8080/status +---- + +The authority can also include user credentials in the form `user:password@host`, but this is deprecated for security reasons and you should not rely on it. + +== How a URL Drives a Connection + +A URL is a recipe, and following it produces a network connection. The steps are: + +. *Parse the scheme* to determine the protocol. `https` means you will need a TLS handshake after connecting. +. *Resolve the host* through DNS. `api.weather.co` becomes an IP address, or possibly a list of addresses. +. *Connect to the port*. If the URL specifies one, use it. Otherwise, use the default for the scheme. +. *Send the request*. For HTTP, this means sending the method, path, query string, and headers. For other protocols, the format differs. + +Each step uses a piece of knowledge from the earlier sections: DNS resolution turns the host into an address, the port selects a process on the server, and the scheme determines how the conversation proceeds. + +== Percent-Encoding + +URLs can only contain a limited set of characters. Letters, digits, hyphens, dots, underscores, and tildes are safe. Everything else -- spaces, non-ASCII characters, reserved characters like `?`, `&`, `#`, `/` -- must be replaced with a percent sign followed by the character's hexadecimal byte value. + +A space becomes `%20`. A forward slash in a query parameter value (where it is not meant as a path separator) becomes `%2F`. The Japanese character for "east" (東) encoded in UTF-8 becomes `%E6%9D%B1`. + +Some examples: + +[cols="1,1"] +|=== +| Raw value | Encoded form + +| `hello world` +| `hello%20world` + +| `price=10&tax=2` +| `price%3D10%26tax%3D2` (when embedded inside another query value) + +| `café` +| `caf%C3%A9` +|=== + +URL parsing libraries handle encoding and decoding for you. The important thing is to recognize that `%XX` sequences are not garbage -- they are properly encoded characters. + +== URLs vs. URIs + +You will sometimes see the term URI (Uniform Resource Identifier) used alongside or instead of URL. The distinction is mostly academic: a URI is the broader category, and a URL is a URI that also tells you *how* to access the resource (via the scheme). A URN (Uniform Resource Name) is a URI that names a resource without providing a location, like an ISBN for a book. + +In practice, nearly every URI you encounter is a URL. The terms are used interchangeably in most documentation and APIs, and treating them as equivalent will not cause problems. + +== Relative URLs + +Not every URL contains all six components. A *relative URL* omits the scheme and authority and is interpreted relative to some base URL. If you are already connected to `https://api.weather.co`, the relative URL `/v2/forecast?city=london` resolves to: + +---- +https://api.weather.co/v2/forecast?city=london +---- + +Relative URLs are common in HTML (where links are relative to the page's URL) and in HTTP redirect responses. Your program resolves them by combining the relative path with the base URL's scheme, host, and port. + +== Why This Matters to You + +A URL is often the first thing your program receives when it needs to make a network request. Parsing it correctly gives you everything required to proceed: the protocol to speak, the host to resolve, the port to connect to, and the path to request. + +Misunderstanding URL structure leads to subtle bugs. Forgetting to percent-encode a query parameter produces malformed requests. Using the fragment in server-side logic fails silently because the fragment is never transmitted. Omitting the port when the server runs on a non-standard one results in a connection refused error. + +The next section covers the two roles in every network conversation -- the client that initiates and the server that listens -- and explains how port numbers keep their conversations separate. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2e.client-server-model.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2e.client-server-model.adoc new file mode 100644 index 00000000..7f19e263 --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2e.client-server-model.adoc @@ -0,0 +1,106 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Clients, Servers, and Ports + +Every network conversation has two sides. One side initiates the connection -- that is the *client*. The other side waits for someone to connect -- that is the *server*. This asymmetry shapes how you write networked code, and port numbers are the mechanism that keeps it all organized. + +== Who Calls Whom + +A server picks a port number, binds to it, and starts listening. It sits there, waiting, ready to accept connections from anyone who knows its address and port. A client knows (or discovers) the server's address and port, then connects to it. The server accepts the connection, and the two sides begin exchanging data. + +This is the fundamental pattern. A web browser (client) connects to a web server. A mail client connects to a mail server. A game client connects to a game server. The client always initiates; the server always listens. Even in protocols where both sides send and receive data freely after the connection is established, the initial roles are fixed. + +A single server typically handles many clients at once. A web server might have ten thousand active connections. Each connection is independent -- the server reads from one, writes to another, and the operating system keeps them all separate. + +== Port Numbers + +A machine might run dozens of services simultaneously: a web server, a database, a mail server, an SSH daemon. All of them share the same IP address. Port numbers distinguish between them. + +A port is a 16-bit unsigned integer, giving a range of 0 to 65535. When a client connects to a server, it specifies both the IP address and the port number. The operating system delivers the connection to whichever program is listening on that port. + +Ports fall into three ranges: + +Well-known ports (0-1023):: Reserved for standard services. HTTP runs on port `80`. HTTPS runs on `443`. SSH uses `22`. DNS uses `53`. FTP uses `21`. On most operating systems, binding to a port in this range requires elevated privileges. + +Registered ports (1024-49151):: Available for applications, with some conventional assignments. MySQL commonly uses `3306`. PostgreSQL uses `5432`. These are conventions, not hard rules -- any program can bind to any available port. + +Ephemeral ports (49152-65535):: Assigned temporarily by the operating system. When your client program connects to a server, the OS picks an ephemeral port for your end of the connection. You do not choose it and usually do not need to know what it is. + +== The Four-Tuple + +A single TCP connection is uniquely identified by four values: + +---- +(source IP, source port, destination IP, destination port) +---- + +Consider a laptop at address `10.0.0.5` connecting to a web server at `203.0.113.80` on port `443`. The operating system assigns ephemeral port `51234` to the client side. The connection's four-tuple is: + +---- +(10.0.0.5, 51234, 203.0.113.80, 443) +---- + +If the same laptop opens a second connection to the same server, the OS assigns a different ephemeral port -- say `51235`. The second connection's four-tuple is: + +---- +(10.0.0.5, 51235, 203.0.113.80, 443) +---- + +Both connections reach the same server on the same port, but they are distinct because the source port differs. This is how a server handles thousands of clients simultaneously on a single port -- every connection has a unique four-tuple. + +It also means that two different client machines can connect to the same server port at the same time without conflict, because their source IP addresses differ. + +== Sockets + +The *socket* is the programming interface your application uses to interact with the network. A socket represents one end of a network connection (or, for UDP, a communication endpoint). You create a socket, configure it, and then either connect it to a remote address (client) or bind it to a local address and listen for connections (server). + +For a TCP client, the typical sequence is: + +. Create a socket. +. Connect to the server's address and port. +. Read and write data through the socket. +. Close the socket when finished. + +For a TCP server: + +. Create a socket. +. Bind it to a local address and port. +. Start listening for incoming connections. +. Accept each incoming connection, which produces a new socket dedicated to that client. +. Read and write data on the accepted socket. +. Close the accepted socket when the conversation ends. + +The listening socket and the accepted sockets are different objects. The listening socket remains open, waiting for more clients. Each accepted socket handles one client's conversation. + +== Binding to Addresses + +When a server binds to a port, it must also specify which local IP address to listen on. A machine with multiple network interfaces has multiple addresses. Binding to a specific address restricts the server to connections arriving on that interface. + +Binding to `0.0.0.0` (for IPv4) or `::` (for IPv6) means "accept connections on any interface." This is the common case for servers meant to be reachable from the network. + +Binding to `127.0.0.1` restricts the server to connections from the same machine -- useful for services that should not be network-accessible, like a local development database. + +== Common Patterns + +Request-response:: The client sends a request, the server sends a response, and the cycle repeats. HTTP follows this pattern. Each request is independent: the client asks for a page, the server returns it. + +Long-lived connections:: The client connects once and the connection stays open for an extended period. Database connections, WebSockets, and chat protocols work this way. Data flows in both directions as needed. + +One connection per request:: The client opens a connection, sends one request, reads one response, and closes. This was the original HTTP/1.0 model. It is inefficient because TCP connection setup has overhead, so modern protocols reuse connections. + +Fire and forget:: The client sends data without expecting a response. Some logging and metrics systems work this way, often over UDP rather than TCP. + +== Why This Matters to You + +The client-server model and port numbers are the foundation of every connection your program makes. When you see "connection refused," it means no process was listening on the target port. When you see "address already in use," it means another process (or a lingering socket in TIME_WAIT) is already bound to the port you requested. + +Understanding the four-tuple explains why a server can accept many connections on a single port, and why the OS assigns ephemeral ports automatically on the client side. Understanding the socket interface tells you what system calls your networking library wraps on your behalf. + +The next section descends into IP itself -- how packets actually travel from your machine to the server, one router hop at a time. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2f.internet-protocol.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2f.internet-protocol.adoc new file mode 100644 index 00000000..976aca59 --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2f.internet-protocol.adoc @@ -0,0 +1,88 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += IP: Moving Packets Across Networks + +IP is the workhorse of the internet. Every piece of data your program sends -- whether over TCP or UDP -- travels inside an IP packet. IP's job is limited but essential: take a packet, figure out where it needs to go, and forward it one hop closer to its destination. It makes no promises about whether the packet arrives, whether it arrives once or twice, or whether it arrives before or after the packet sent before it. That deliberate minimalism is what makes the internet scalable. + +== What IP Does + +IP provides three things: + +Addressing:: Every packet carries a source IP address and a destination IP address, identifying who sent it and where it should go. + +Routing:: Routers examine the destination address and forward the packet toward the correct network. Each router makes an independent, local decision about the next hop. + +Fragmentation:: When a packet is too large for a link along the path, IP can split it into smaller pieces that are reassembled at the destination. + +That is the complete list. IP does not provide reliability, ordering, congestion control, or flow control. Those features exist in TCP, which sits above IP. IP is a delivery truck that drives a package from warehouse to warehouse without tracking whether it arrives. + +== The IP Header + +Every IP packet begins with a header that contains the information routers and the destination host need to process it. The most important fields are: + +Version:: Identifies whether this is IPv4 (value 4) or IPv6 (value 6). A router uses this to determine how to interpret the rest of the header. + +Total length:: The size of the entire packet -- header plus payload -- in bytes. The maximum for IPv4 is 65,535 bytes, though packets this large are rare in practice. + +Time to Live (TTL):: A counter, typically set to 64 or 128 by the sender, that decrements by one at every router. When it reaches zero, the router discards the packet and sends an error message back to the sender. TTL prevents misrouted packets from circling the network forever. + +Protocol:: A number identifying what sits inside the IP payload. The value `6` means TCP. The value `17` means UDP. The receiving host uses this field to hand the payload to the correct protocol handler. + +Source address:: The IP address of the machine that sent the packet. + +Destination address:: The IP address of the machine that should receive the packet. + +Header checksum:: (IPv4 only.) A checksum covering the header fields, verified at each hop. If the checksum fails, the packet is silently discarded. IPv6 drops this field entirely and relies on link-level and transport-level checksums instead. + +There are additional fields -- identification, flags, and fragment offset for fragmentation; options for special features; differentiated services for quality-of-service marking -- but the ones listed above are the ones that matter for understanding how packets move through the network. + +== How Routing Works + +Your program sends a packet destined for `203.0.113.80`. The packet does not travel directly from your machine to that address. Instead, it passes through a series of routers, each making a local forwarding decision. + +. Your machine consults its routing table. It determines that `203.0.113.80` is not on the local network, so it forwards the packet to the default gateway -- the router connecting your network to the broader internet. +. The gateway examines the destination address and compares it against its own routing table, which has entries for many networks. It finds a match and forwards the packet to the next router. +. This process repeats at each router along the path. Every router knows about the networks reachable through its various interfaces, picks the best match for the destination, and forwards the packet on. +. Eventually, the packet reaches a router that knows the destination network directly. It delivers the packet to the destination machine. + +No single router knows the entire path. Each one knows only its immediate neighbors and which networks are reachable through each neighbor. This distributed, hop-by-hop design is what allows the internet to scale to billions of devices without a central authority coordinating every path. + +Routes can change in real time. If a link between two routers goes down, routing protocols detect the failure and recalculate paths within seconds. Your packet might take a different route than the one before it, and neither your program nor the destination will notice -- IP treats every packet independently. + +== MTU: Maximum Transmission Unit + +Every network link has a maximum frame size -- the largest chunk of data it can carry in a single transmission. This limit is called the Maximum Transmission Unit, or MTU. + +Ethernet, the most common link type, has a standard MTU of 1500 bytes. This means an Ethernet frame can carry an IP packet of up to 1500 bytes. Some network links have smaller MTUs (older technologies, certain VPN tunnels) and some support larger ones (jumbo frames at 9000 bytes, used in data centers). + +The *path MTU* is the smallest MTU of any link along the entire route from source to destination. If your machine sends a 1500-byte packet but one link along the way has an MTU of 1400, that link cannot carry your packet as-is. + +== Fragmentation + +When an IP packet exceeds the MTU of the next link, one of two things happens: + +In IPv4:: The router can *fragment* the packet -- splitting it into smaller pieces that each fit within the link's MTU. Each fragment carries enough information (an identification field and a fragment offset) for the destination to reassemble the original packet. Fragments travel independently and may arrive out of order. The destination waits until all fragments arrive, then reconstructs the original packet. + +In IPv6:: Routers do not fragment. If a packet is too large, the router drops it and sends an error back to the sender, telling it the maximum size the link can handle. The sender then reduces the packet size and tries again. This is called *path MTU discovery*, and it shifts the responsibility for sizing packets from routers to endpoints. + +Fragmentation has costs. If any single fragment is lost, the entire original packet must be retransmitted -- the destination cannot use a partially assembled packet. Reassembly also consumes memory and CPU on the receiving host. For these reasons, modern practice avoids fragmentation whenever possible. TCP negotiates a maximum segment size during connection setup to keep packets below the path MTU. UDP applications that care about performance should do the same. + +== Why This Matters to You + +As an application developer, you rarely interact with IP directly. TCP and UDP sit between your code and IP, and the operating system handles routing and fragmentation transparently. But IP's behavior explains several things you will encounter: + +* *Why packets can arrive out of order*: each IP packet is routed independently, and different packets may take different paths. +* *Why packets can be lost*: routers discard packets when queues overflow, checksums fail, or TTL expires. There is no notification to the sender. +* *Why there is a maximum useful size for a UDP datagram*: sending more than the path MTU triggers fragmentation, which increases the chance of total loss. +* *Why TCP negotiates segment sizes*: to avoid fragmentation entirely. + +IP's minimalism is not a flaw. Keeping the core protocol lean allows it to run on everything from undersea cables to satellite links to wireless networks. The complexity lives in TCP and UDP, where it can be adapted to the needs of each application. + +The next section introduces UDP -- the thinner of the two transport protocols, and the one closest in spirit to IP itself. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2g.udp.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2g.udp.adoc new file mode 100644 index 00000000..a861eaca --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2g.udp.adoc @@ -0,0 +1,103 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += UDP: Fast, Simple, Unreliable + +UDP is the simplest transport protocol you will encounter. It takes your data, slaps on a small header with source and destination port numbers, and hands it to IP. No connection setup. No acknowledgment. No retransmission. No ordering guarantees. If you want any of those things, you build them yourself. + +That sounds like a limitation, and it is -- but it is also the point. UDP exists for situations where the overhead of reliability is worse than the occasional lost packet. + +== What UDP Provides + +UDP adds exactly two things to IP: + +Port numbers:: Just like TCP, UDP uses 16-bit source and destination port numbers to direct data to the correct application. A UDP socket bound to port `5353` only receives datagrams addressed to that port. + +Checksum:: A checksum covers the UDP header and payload. If the data was corrupted in transit, the operating system discards the datagram silently. (In IPv4 the checksum is optional, though universally used in practice. In IPv6 it is mandatory.) + +That is the full list. UDP does not establish a connection, does not track what has been sent, and does not guarantee that anything arrives. Each `send` call produces one datagram. Each `recv` call consumes one datagram. The datagrams are independent. + +== The UDP Header + +The UDP header is eight bytes: + +[cols="1,3"] +|=== +| Field | Description + +| Source port (16 bits) +| The sender's port number. Optional in theory; in practice, always present. + +| Destination port (16 bits) +| The port on the receiving machine where the datagram should be delivered. + +| Length (16 bits) +| Total length of the UDP header plus payload, in bytes. The minimum is 8 (header only, no data). + +| Checksum (16 bits) +| Covers a pseudo-header (source and destination IP addresses, protocol, length), the UDP header, and the payload. +|=== + +Eight bytes. Compare that to TCP's minimum 20-byte header with sequence numbers, acknowledgment numbers, window sizes, and flags. UDP's overhead is minimal, which means more of every packet is your actual data. + +== Message Boundaries + +This is one of the most important differences between UDP and TCP, and it trips people up regularly. + +UDP preserves message boundaries. If you send a 200-byte datagram followed by a 300-byte datagram, the receiver gets two separate datagrams: one of 200 bytes and one of 300 bytes. They arrive as discrete units. A single `recv` call returns exactly one datagram. + +TCP, by contrast, is a byte stream. Send 200 bytes followed by 300 bytes, and the receiver might get 500 bytes in one read, or 100 and 400, or any other combination. TCP does not preserve the boundaries between your writes. + +For protocols where message framing matters -- where each datagram is a self-contained unit -- UDP's boundary preservation is a genuine advantage. DNS is a good example: each query is a single datagram, and each response is a single datagram. There is no need to figure out where one message ends and another begins. + +== Fragmentation and UDP + +A UDP datagram can theoretically be up to 65,535 bytes (the maximum IP packet size, minus the IP and UDP headers). In practice, sending anything close to this size is a bad idea. + +When a UDP datagram exceeds the path MTU, IP fragments it into smaller pieces. These fragments travel independently through the network. If every fragment arrives, the destination reassembles the original datagram and delivers it to your application. If *any* fragment is lost, the entire datagram is discarded. Your application receives nothing -- not even the fragments that did arrive. + +On a typical internet path with a 1500-byte MTU, the practical limit for a UDP datagram you can send without risking fragmentation is about 1472 bytes (1500 minus the 20-byte IP header and 8-byte UDP header). Many applications choose an even smaller limit to account for paths with lower MTUs or encapsulation overhead from VPNs and tunnels. + +The takeaway: keep UDP datagrams small. If your message does not fit in a single unfragmented packet, you either need to split it yourself or consider whether TCP is a better fit. + +== No Connection, No State + +A TCP server creates a dedicated socket for each connected client, maintaining per-connection state in the kernel: sequence numbers, window sizes, retransmission timers. A busy TCP server with ten thousand clients has ten thousand sockets, each tracking its own connection. + +A UDP server typically uses a single socket to serve all clients. Datagrams arrive from different source addresses and ports, and the server handles each one independently. There is no "connection" to accept, no state to maintain in the kernel, and no connection to tear down when the client goes away. + +This makes UDP servers simpler in some respects. A DNS server processes each query as an independent event: a datagram arrives, the server looks up the answer, and it sends a response datagram back to the source address. The server does not track which clients have connected or maintain session state between queries. + +The downside is that UDP gives you no help with reliability. If the response datagram is lost, the client has to notice (usually via a timeout) and resend the query. The server has no idea whether its response arrived. + +== When UDP Is the Right Choice + +UDP fits well in several categories: + +Request-response with retries:: Protocols like DNS send a small query and expect a small response. If no response arrives within a timeout, the client retries. The cost of an occasional lost packet is far less than the cost of establishing a TCP connection for every lookup. + +Real-time media:: Audio and video streams are time-sensitive. A packet that arrives late is useless -- you cannot rewind live audio to insert a delayed sample. Retransmitting lost packets would add latency without improving the experience. UDP lets the application skip missing data and keep playing. + +Broadcast and multicast:: UDP supports sending a single datagram to multiple recipients simultaneously. TCP, being connection-oriented, has no equivalent. Network discovery protocols, service announcements, and some gaming systems use UDP multicast. + +Application-managed reliability:: Some applications need reliability but with different trade-offs than TCP provides. They build their own retransmission and ordering logic on top of UDP. The QUIC protocol is a prominent example: it runs over UDP but provides its own reliability, congestion control, and stream multiplexing. + +== When UDP Is the Wrong Choice + +If your application needs every byte to arrive, in order, without duplicates, TCP is almost certainly the right choice. Reimplementing TCP's reliability on top of UDP is a substantial engineering effort, and the result is rarely better than what TCP already provides. + +File transfers, database queries, HTTP requests, and any protocol where data integrity matters should use TCP. The connection setup overhead is negligible compared to the cost of getting reliability wrong. + +== Why This Matters to You + +UDP is the faster, leaner transport protocol, and understanding it helps you make informed decisions about when to use it. But its simplicity is a double-edged sword: it gives you freedom and leaves you responsible for everything TCP would handle automatically. + +For most application developers, TCP is the default choice. UDP is the right tool for specific problems: latency-sensitive media, lightweight query-response protocols, and situations where the application knows better than TCP what "reliable" means. + +The next section introduces TCP -- the protocol that takes IP's unreliable packet delivery and turns it into a reliable byte stream. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2h.tcp-fundamentals.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2h.tcp-fundamentals.adoc new file mode 100644 index 00000000..629cc41e --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2h.tcp-fundamentals.adoc @@ -0,0 +1,118 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += TCP: Reliable Byte Streams + +TCP is the protocol that makes the internet useful for most applications. It takes the unreliable, unordered packet delivery that IP provides and builds something much more powerful on top: a reliable, ordered stream of bytes that flows in both directions between two machines. Your program writes bytes into one end, and they come out at the other end in exactly the same order, even if the underlying packets were lost, duplicated, reordered, or delayed. + +Nearly every protocol you interact with daily -- HTTP, database wire protocols, email, SSH, TLS -- runs over TCP. Understanding what it guarantees, how it works, and where its limits are is essential knowledge for networked programming. + +== What TCP Guarantees + +TCP provides four properties that IP and UDP do not: + +Reliable delivery:: Every byte you send eventually arrives at the destination, or you receive an error indicating the connection failed. TCP detects lost packets and retransmits them automatically. Your application never has to implement its own retry logic. + +Ordered delivery:: Bytes arrive at the receiver in the same order the sender transmitted them. If packets arrive out of order (which is common -- IP routes each packet independently), TCP buffers the early arrivals and delivers them to your application only when the sequence is complete. + +Flow control:: The receiver tells the sender how much data it can accept. If the receiver's buffers are filling up, it advertises a smaller window and the sender slows down. This prevents a fast producer from overwhelming a slow consumer. + +Congestion control:: TCP monitors the network for signs of congestion (dropped packets, increasing delays) and reduces its sending rate in response. This is not just polite -- it is essential. Without congestion control, every TCP connection would blast data as fast as possible, and the shared network infrastructure would collapse under the load. + +These guarantees come at a cost: connection setup takes time (a round trip before any data flows), per-connection state consumes kernel memory, and the reliability machinery adds latency when packets are lost. For most applications, that cost is negligible compared to the value of not having to build reliability yourself. + +== TCP Is a Byte Stream + +This is the single most important thing to understand about TCP, and the source of countless bugs in networking code. + +TCP is a *byte stream*, not a *message stream*. When you call `send` with 500 bytes, TCP does not guarantee that the receiver gets those 500 bytes in one `recv` call. The receiver might get 200 bytes in one call and 300 in the next. Or all 500 at once. Or 1 byte at a time. TCP makes no promises about how many bytes each read returns -- only that all the bytes arrive, in order. + +If your protocol has messages with defined boundaries -- a request followed by a response, for example -- you must frame them yourself. Common approaches include: + +* Prefixing each message with its length (a 4-byte integer followed by that many bytes of payload). +* Using a delimiter like a newline character to mark the end of each message. +* Using a fixed-size message format where every message is the same length. + +The protocol defines the framing. TCP delivers the bytes. Your code is responsible for reassembling them into meaningful units. + +== The TCP Header + +Every TCP segment carries a header with the information needed for reliable, ordered delivery. The key fields are: + +Source port (16 bits):: The sender's port number. + +Destination port (16 bits):: The receiver's port number. Together with the IP addresses, these form the four-tuple that identifies the connection. + +Sequence number (32 bits):: The byte offset of the first byte in this segment's payload, relative to the initial sequence number established during the handshake. If the initial sequence number was 1000 and this segment carries bytes 1000 through 1499, the sequence number is 1000. + +Acknowledgment number (32 bits):: The next byte the sender expects to receive from the other side. If the receiver has gotten bytes 0 through 499, the acknowledgment number is 500. This tells the other side "I have everything up to byte 499; send me byte 500 next." + +Flags:: Single-bit indicators that control the connection: +* *SYN*: initiates a connection (used during the handshake). +* *ACK*: indicates the acknowledgment number field is valid (set on nearly every segment after the handshake). +* *FIN*: the sender is done transmitting data (used during teardown). +* *RST*: abruptly resets the connection. +* *PSH*: requests that the receiver deliver the data to the application immediately rather than buffering. + +Window size (16 bits):: The number of bytes the sender is willing to accept. This is the flow control mechanism: the receiver advertises how much buffer space it has, and the sender limits itself to that amount. + +Checksum (16 bits):: Covers the TCP header, the payload, and a pseudo-header derived from the IP addresses and protocol number. If the checksum fails, the segment is discarded silently. + +The sequence and acknowledgment numbers are the core of TCP's reliability. By tracking which bytes have been sent and which have been acknowledged, TCP can detect gaps (lost packets) and fill them with retransmissions. + +== TCP vs. UDP: Choosing + +The choice between TCP and UDP is usually obvious: + +[cols="1,1,1"] +|=== +| Property | TCP | UDP + +| Reliability +| Guaranteed delivery +| Best effort + +| Ordering +| Bytes arrive in order +| Datagrams may arrive in any order + +| Connection +| Yes (handshake required) +| No + +| Message boundaries +| Not preserved (byte stream) +| Preserved (datagram) + +| Flow control +| Yes (window-based) +| No + +| Congestion control +| Yes +| No + +| Overhead +| Higher (20+ byte header, per-connection state) +| Lower (8-byte header, no state) +|=== + +Use TCP when data must arrive completely and in order: HTTP, database protocols, file transfers, email, remote shells. Use UDP when latency matters more than completeness: real-time audio/video, DNS lookups, game state updates, or when you need multicast. + +If you are unsure, start with TCP. Its guarantees eliminate an enormous class of bugs, and its overhead is negligible for most applications. + +== The Illusion of a Perfect Pipe + +The internet between your machine and the server is anything but reliable. Packets get dropped when router buffers overflow. They arrive out of order because different packets take different paths. They get corrupted by electrical interference. Links fail and recover. None of this is exceptional -- it is the normal operating condition of a global packet-switched network. + +TCP hides all of it. From your application's perspective, the connection is a clean, bidirectional pipe: bytes go in one end and come out the other, in order, exactly once. That illusion is constructed from sequence numbers, acknowledgments, retransmissions, timers, and window management -- machinery that runs entirely inside the operating system, transparent to your code. + +But it is an illusion, not magic. TCP cannot fix a dead link. It cannot deliver data faster than the network allows. It cannot prevent the other side from crashing. When the illusion breaks, your application sees an error on the socket -- and understanding the machinery behind the illusion helps you diagnose what went wrong. + +The next section examines how that illusion begins and ends: the TCP connection lifecycle. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2i.tcp-connections.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2i.tcp-connections.adoc new file mode 100644 index 00000000..bb4fc33d --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2i.tcp-connections.adoc @@ -0,0 +1,152 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Opening and Closing TCP Connections + +A TCP connection is not a physical wire. It is an agreement between two machines to track a shared conversation. That agreement begins with a handshake, ends with a teardown, and passes through a series of well-defined states in between. Knowing these states explains errors you will encounter in every networked application you write. + +== The Three-Way Handshake + +Before any data flows, TCP establishes the connection with three segments: + +. *SYN*: The client sends a segment with the SYN flag set, along with a randomly chosen initial sequence number (ISN). This says "I want to connect, and my byte numbering starts at this value." +. *SYN-ACK*: The server responds with both the SYN and ACK flags set. It acknowledges the client's ISN (by setting the acknowledgment number to the client's ISN + 1) and provides its own ISN. This says "I accept, I acknowledge your starting number, and my byte numbering starts here." +. *ACK*: The client sends a final acknowledgment of the server's ISN. At this point both sides have agreed on initial sequence numbers, and the connection is established. + +---- +Client Server + | | + |--- SYN (seq=100) ----------------->| + | | + |<-- SYN-ACK (seq=300, ack=101) -----| + | | + |--- ACK (ack=301) ----------------->| + | | + | Connection established | +---- + +Why three steps instead of two? Both sides need to agree on two things: the client's ISN and the server's ISN. A two-step handshake would let the server accept without the client confirming the server's ISN. The third step completes the exchange. + +The initial sequence numbers are chosen randomly (not starting from zero) to prevent segments from old, defunct connections from being mistaken for segments belonging to a new connection on the same port. If every connection started at sequence number zero, a delayed packet from a previous connection could be accepted as valid data in the current one. + +== Connection Timeout + +What happens when the server does not respond to the SYN? The client waits, retransmits the SYN after a short delay, and retransmits again with increasingly longer delays. After several retries spanning a total of roughly 30 to 75 seconds (depending on the operating system), the connection attempt gives up and your application receives a timeout error. + +This is the error you see when connecting to a host that is unreachable, firewalled, or simply not running a server on the target port. The long delay before the error appears is TCP being patient -- giving the network every chance to deliver the SYN. + +If the server is reachable but nothing is listening on the port, the response is faster: the server immediately sends a RST (reset) segment, and your application gets a "connection refused" error within milliseconds. + +== Graceful Teardown + +Closing a TCP connection takes four segments because each direction of the stream is shut down independently: + +. *FIN*: One side (say the client) sends a segment with the FIN flag set, indicating "I have no more data to send." +. *ACK*: The other side acknowledges the FIN. +. *FIN*: The other side sends its own FIN when it, too, has finished sending. +. *ACK*: The first side acknowledges the second FIN. + +---- +Client Server + | | + |--- FIN --------------------------->| + |<-- ACK ----------------------------| + | | + |<-- FIN ----------------------------| + |--- ACK --------------------------->| + | | + | Connection closed | +---- + +In practice, the server's ACK and FIN are often combined into a single segment, reducing the exchange to three segments. But logically, the four steps reflect two independent half-closes. + +== Half-Close + +Because each direction is closed separately, it is possible to shut down sending while still receiving. This is called a *half-close*. + +Consider an HTTP client that sends a request and then calls `shutdown(SHUT_WR)` on its socket. This sends a FIN to the server, signaling that the client is done writing. But the client's receive side remains open. The server sees the FIN, knows no more request data is coming, processes the request, sends the response, and then closes its end. + +Half-close is useful in protocols where the client needs to say "I am done sending, but keep your response coming." Without it, the server would have no way to distinguish "the client is done" from "the client is still thinking." + +== The State Machine + +A TCP connection passes through a series of states from creation to destruction. Understanding these states is the key to diagnosing connection issues: + +CLOSED:: No connection exists. This is the starting and ending state. + +LISTEN:: The server has called `listen()` on a socket and is waiting for incoming connections. + +SYN_SENT:: The client has sent a SYN and is waiting for the server's SYN-ACK. + +SYN_RECEIVED:: The server has received a SYN and sent a SYN-ACK, and is waiting for the client's final ACK. + +ESTABLISHED:: The handshake is complete. Both sides can send and receive data. This is where a connection spends most of its life. + +FIN_WAIT_1:: This side has sent a FIN and is waiting for an acknowledgment. + +FIN_WAIT_2:: The FIN has been acknowledged, but the other side has not yet sent its own FIN. This side is waiting for the remote FIN. + +CLOSE_WAIT:: This side has received a FIN from the remote end but has not yet sent its own FIN. If your server has many connections in CLOSE_WAIT, it means the application is not closing sockets promptly after the remote end has disconnected. + +LAST_ACK:: This side has sent its FIN (after receiving the remote FIN) and is waiting for the final ACK. + +TIME_WAIT:: The connection is fully closed, but the socket lingers for a period (typically 60 seconds to 2 minutes) before returning to CLOSED. This is the state that causes the most confusion. + +== TIME_WAIT + +After both sides have exchanged FINs and ACKs, you might expect the connection to disappear immediately. Instead, the side that initiated the close enters TIME_WAIT and holds the socket open for a period called 2MSL (twice the Maximum Segment Lifetime, typically 60 seconds). + +TIME_WAIT exists for two reasons: + +. *Reliable termination*: If the final ACK is lost, the remote side will retransmit its FIN. The TIME_WAIT state ensures that the local side is still around to re-acknowledge it. +. *Preventing stale segments*: If a new connection is established on the same four-tuple immediately after closing, delayed segments from the old connection might arrive and be misinterpreted as belonging to the new one. TIME_WAIT ensures that enough time passes for any lingering segments to expire. + +The practical consequence is the dreaded "address already in use" error. If your server shuts down and immediately restarts, it cannot bind to the same port because the old connections are still in TIME_WAIT. The standard solution is to set the `SO_REUSEADDR` socket option before binding, which tells the OS to allow binding to a port that has connections in TIME_WAIT. + +== Reset Segments + +A RST (reset) segment is the emergency stop. It terminates the connection immediately, without the graceful FIN exchange. The receiving side sees an error on the socket -- typically "connection reset by peer." + +RST is sent in several situations: + +* A SYN arrives for a port where nothing is listening. The server responds with RST to tell the client "no one is here." +* Data arrives on a connection that the receiving side considers closed. The receiver sends RST because it has no context for the segment. +* An application decides to abort the connection instead of closing gracefully (by setting the `SO_LINGER` option with a timeout of zero). + +RST segments are not acknowledged. Once sent, the connection is destroyed. Any unsent or unacknowledged data is discarded. + +== Maximum Segment Size + +During the three-way handshake, each side advertises its Maximum Segment Size (MSS) -- the largest chunk of data it can receive in a single TCP segment. This is communicated as a TCP option in the SYN and SYN-ACK segments. + +The MSS is typically set to the local interface's MTU minus the IP and TCP header sizes. For an Ethernet link with a 1500-byte MTU, the MSS is usually 1460 bytes (1500 - 20 bytes IP header - 20 bytes TCP header). + +Negotiating the MSS helps TCP avoid IP fragmentation. If both sides know the largest segment the other can handle, TCP can size its segments to fit without requiring IP to split them. + +== Server Design + +A TCP server uses two kinds of sockets: + +The *listening socket* is bound to a well-known port and calls `accept()` in a loop. Each call to `accept()` blocks until a client connects, then returns a *connected socket* dedicated to that client. + +The listening socket is never used for data transfer. It exists solely to create connected sockets. The connected socket carries all the data for a single client conversation. When the conversation ends, the connected socket is closed, but the listening socket remains, ready for the next client. + +A server handling multiple clients simultaneously needs a strategy for managing many connected sockets. Options include spawning a thread per connection, using an event loop with non-blocking I/O, or -- as Corosio provides -- using coroutines that `co_await` on socket operations. The mechanism differs, but the pattern is the same: one listening socket, many connected sockets, each managed independently. + +== Why This Matters to You + +The TCP connection lifecycle is not academic trivia. It is the explanation behind errors you will see regularly: + +* "Connection refused" means RST came back immediately -- nothing is listening on the port. +* "Connection timed out" means no response to the SYN -- the host is unreachable or firewalled. +* "Address already in use" means TIME_WAIT is holding the port -- set `SO_REUSEADDR`. +* "Connection reset by peer" means the other side sent RST -- it crashed, aborted, or your data arrived on a connection it considers closed. +* Many connections in CLOSE_WAIT mean your application is not closing sockets after the remote end disconnects. + +The next section looks at what happens during the ESTABLISHED state -- how TCP actually moves your data across the connection efficiently. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2j.tcp-data-flow.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2j.tcp-data-flow.adoc new file mode 100644 index 00000000..c92f3c97 --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2j.tcp-data-flow.adoc @@ -0,0 +1,115 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += How TCP Moves Your Data + +A naive approach to reliable delivery would be: send one segment, wait for the acknowledgment, send the next. That works, but it is painfully slow. If the round trip between your machine and the server takes 50 milliseconds, you can only send 20 segments per second -- regardless of how much bandwidth the network has. Most of the time is spent waiting. + +TCP solves this with the *sliding window*: a mechanism that lets the sender have multiple segments in flight simultaneously, pipelining data so the network is kept busy even while acknowledgments are still traveling back. The sliding window is what makes TCP fast, and understanding it is the key to understanding TCP performance. + +== Sending and Acknowledging + +At its core, TCP data transfer is a loop: + +. The sender transmits a segment containing some bytes of data, labeled with a sequence number. +. The receiver gets the segment, buffers the data, and sends an ACK back. The acknowledgment number in the ACK tells the sender "I have received everything up to this byte." +. The sender, having received the ACK, knows those bytes were delivered and can discard them from its send buffer. + +If both sides are transferring data, ACKs often piggyback on data segments traveling in the opposite direction. A single segment can carry both a chunk of data and an acknowledgment for previously received data, reducing the number of packets on the wire. + +== The Sliding Window + +The sender does not wait for each segment to be acknowledged before sending the next. Instead, it maintains a *window* of bytes it is allowed to send without having received an ACK. The size of this window determines how much data can be in flight at any given time. + +Imagine the sender has 10,000 bytes to transmit and the window size is 4,000 bytes. The sequence looks like this: + +. The sender transmits bytes 0-999, 1000-1999, 2000-2999, and 3000-3999. Four segments, filling the 4,000-byte window. +. The sender cannot send more until an ACK arrives. It has reached its window limit. +. An ACK arrives acknowledging bytes 0-999. The window slides forward: the sender can now send bytes 4000-4999. +. Another ACK arrives acknowledging bytes 1000-1999. The window slides again: bytes 5000-5999 become sendable. + +---- +Sent and ACKed | Sent, not ACKed | Sendable | Not yet sendable + [ window ] +---- + +The window "slides" to the right as ACKs arrive, always keeping a fixed amount of data in flight. The name comes directly from this behavior. + +The window size is not fixed for the lifetime of the connection. It changes dynamically based on two factors: how much buffer space the receiver has (flow control) and how congested the network appears to be (congestion control). + +== Flow Control: The Receiver's Window + +Every ACK the receiver sends includes a *window advertisement* -- a 16-bit field stating how many bytes the receiver is willing to accept right now. This value reflects the available space in the receiver's buffer. + +If the receiver's application is reading data quickly, the buffer stays mostly empty and the advertised window stays large. The sender keeps transmitting at full speed. + +If the receiver's application falls behind -- maybe it is busy processing a previous request -- the buffer fills up and the advertised window shrinks. The sender slows down in response. If the window reaches zero, the sender stops entirely and waits for the receiver to free up space. + +This feedback loop prevents a fast sender from flooding a slow receiver. It operates automatically, requiring no action from your application code. But it has a practical consequence: if your server reads from the socket slowly, the client's send calls will eventually stall. TCP is applying backpressure through the window mechanism. + +== Delayed Acknowledgments + +The receiver does not send an ACK for every single segment it receives. Instead, it often waits a short time -- typically 40 to 200 milliseconds -- hoping to piggyback the ACK on outgoing data. If no outgoing data appears within the delay, the receiver sends the ACK by itself. + +This optimization reduces the number of pure-ACK packets on the network, which carry no data and represent pure overhead. In a request-response protocol, the ACK for the request often rides along with the response, cutting the packet count almost in half. + +The downside is that delayed ACKs interact poorly with small writes, as described next. + +== The Nagle Algorithm + +When your application writes small chunks of data -- a few bytes at a time -- TCP faces a dilemma. Sending each tiny write as its own segment wastes bandwidth: a 1-byte payload in a segment with 40 bytes of headers (IP + TCP) is spectacularly inefficient. + +The Nagle algorithm addresses this by batching small writes. The rule is: + +* If there is no unacknowledged data in flight, send immediately, regardless of size. +* If there is unacknowledged data in flight, buffer subsequent small writes and send them as a single segment when the outstanding ACK arrives. + +This batches multiple small writes into a single, reasonably-sized segment. For bulk data transfer, it makes no difference -- the writes are already large. For interactive applications that send many small messages, it reduces overhead significantly. + +The catch is the interaction with delayed ACKs. Suppose a client sends a small request and then wants to send another small piece of data. The Nagle algorithm buffers the second write, waiting for the ACK of the first. Meanwhile, the server delays its ACK, waiting to piggyback it on a response. If the server cannot produce a response until it has all the data, everyone waits: the client waits for an ACK, the server waits for more data, and the delayed-ACK timer eventually fires and breaks the deadlock. The result is a mysterious 200-millisecond pause. + +The standard fix is to disable the Nagle algorithm by setting the `TCP_NODELAY` socket option. This tells TCP to send every write immediately, regardless of size. Most latency-sensitive applications -- game servers, interactive terminals, financial trading systems -- set `TCP_NODELAY` by default. + +== The PUSH Flag + +TCP's PSH (push) flag tells the receiving side to deliver the data to the application immediately rather than waiting for the buffer to fill up. In practice, most TCP implementations set PSH automatically on the last segment of each write operation, and most receiving implementations deliver data as soon as it arrives regardless of PSH. + +You rarely interact with PSH directly. It exists in the protocol but is largely handled by the operating system. The main thing to know is that it does not create message boundaries -- even with PSH set, the byte stream semantics remain. The receiver might still combine data from multiple segments into a single read. + +== Slow Start + +When a new TCP connection is established, neither side knows the capacity of the network path between them. If the sender immediately blasts data at the full window size, it might overwhelm a bottleneck link and cause packet loss. + +Slow start addresses this by beginning cautiously. The sender starts with a small *congestion window* (typically 10 segments on modern systems) and increases it rapidly as ACKs arrive: + +. Send the initial window's worth of data. +. For each ACK received, increase the congestion window by one segment. +. This effectively doubles the congestion window every round trip. + +The growth is exponential: 10 segments, then 20, then 40, then 80. Within a few round trips, the sender is transmitting at a rate that matches the network's capacity. + +Slow start continues until one of two things happens: + +* The congestion window reaches the receiver's advertised window, at which point flow control takes over. +* Packet loss is detected, which TCP interprets as a sign of congestion. At that point, TCP switches to a more conservative growth strategy (covered in the next section). + +The practical effect is that a new TCP connection takes a few round trips to ramp up to full speed. Short-lived connections -- like individual HTTP requests -- may complete before slow start finishes ramping up, which is one reason HTTP connection reuse and HTTP/2 multiplexing improve performance. + +== Why This Matters to You + +The sliding window is what makes TCP viable for high-throughput data transfer. Without it, every connection would be limited to one segment per round trip, and the internet as we know it would not exist. + +As an application developer, the mechanisms described here affect your code in concrete ways: + +* *Write in large chunks when possible.* Many small writes may trigger the Nagle algorithm and introduce latency. Buffering application data and writing it in a single call is generally better. +* *Set `TCP_NODELAY` for latency-sensitive protocols.* If your application sends small messages and expects immediate responses, disable Nagle. +* *Read promptly.* If your application does not read from the socket, the receiver's window shrinks and the sender stalls. TCP's backpressure mechanism is doing its job, but your application feels it as a throughput drop. +* *Expect slow start on new connections.* The first few round trips on a fresh connection are slower than steady-state throughput. Connection reuse matters. + +The next section covers what happens when the network fails to deliver -- how TCP detects lost packets and responds to congestion. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2k.tcp-reliability.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2k.tcp-reliability.adoc new file mode 100644 index 00000000..8b302f6d --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2k.tcp-reliability.adoc @@ -0,0 +1,124 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += When Packets Go Missing + +TCP promises reliable delivery, but the network underneath makes no such promise. Routers drop packets when their queues overflow. Links fail mid-transmission. Interference corrupts data. TCP's job is to detect these failures and recover from them -- transparently, without your application ever knowing a packet was lost. + +The strategy for doing this is more subtle than "notice it is missing and send it again." TCP has to decide *when* a packet is lost (as opposed to merely delayed), *how fast* to retransmit (too aggressive floods the network, too cautious wastes time), and *how to respond* to the underlying cause (is the network congested, or was it just a random bit flip?). Getting these decisions right is what separates a well-behaved TCP implementation from one that either stalls unnecessarily or makes congestion worse. + +== Measuring Round-Trip Time + +Before TCP can decide that a packet is lost, it needs to know how long a packet *should* take to be acknowledged. That requires measuring the round-trip time (RTT) -- the time between sending a segment and receiving its ACK. + +The RTT is not a fixed value. It fluctuates constantly as network conditions change: routing shifts, queues fill and drain, links become more or less loaded. TCP handles this by maintaining two running estimates: + +* A *smoothed RTT* (SRTT), which is a weighted average of recent measurements. New samples are blended into the average gradually, so a single outlier does not distort the estimate. +* An *RTT variance* estimate, which tracks how much the measurements fluctuate. A high variance means the network is unpredictable. + +The retransmission timeout (RTO) is calculated from these two values. A common formula sets the RTO to the smoothed RTT plus four times the variance. This ensures the timeout is long enough to accommodate normal variation but not so long that TCP waits forever when a packet is genuinely lost. + +If the actual RTT is around 30 milliseconds with low variance, the RTO might be set to 50 milliseconds. If the RTT jumps around between 20 and 200 milliseconds, the RTO adjusts upward to avoid false retransmissions. + +== Retransmission Timeout + +When TCP sends a segment, it starts a timer. If the ACK for that segment does not arrive before the timer expires, TCP assumes the segment was lost and retransmits it. + +After a timeout-based retransmission, TCP does two things: + +. It *doubles the RTO* for the next attempt. This is called exponential backoff. If the network is congested, retransmitting at the same rate would make things worse. Backing off gives the network time to recover. +. It drastically *reduces its sending rate* by resetting the congestion window to a single segment and re-entering slow start. This is the most aggressive response TCP has to packet loss. + +Timeout-based retransmission is the last resort. It works, but it is slow -- the sender sits idle for the entire timeout period before retransmitting. TCP has a faster mechanism for the common case, described next. + +== Fast Retransmit + +Most packet loss is not total. Typically, one segment is dropped while the segments that follow it arrive successfully. When the receiver gets a segment that is out of order -- say segment 5 arrives but segment 4 did not -- it cannot deliver anything new to the application (TCP requires in-order delivery). Instead, it re-sends an ACK for the last contiguous byte it has received. This is called a *duplicate ACK*. + +If segment 4 is lost and segments 5, 6, and 7 arrive, the receiver sends three duplicate ACKs, all acknowledging the same byte position (the last byte of segment 3). + +TCP treats the arrival of three duplicate ACKs as strong evidence that a specific segment was lost. Rather than waiting for the retransmission timeout, it immediately retransmits the missing segment. This is *fast retransmit*, and it recovers from loss in roughly one round trip instead of waiting for the full RTO. + +---- +Sender Receiver + | | + |--- Segment 4 ------- (lost) ---X | + |--- Segment 5 ----------------->| | + |<-- Dup ACK (ack=4) ------------| | + |--- Segment 6 ----------------->| | + |<-- Dup ACK (ack=4) ------------| | + |--- Segment 7 ----------------->| | + |<-- Dup ACK (ack=4) ------------| | + | | + | (3 dup ACKs: fast retransmit) | + |--- Segment 4 (retransmit) ---->| | + |<-- ACK (ack=8) ----------------| | <- acknowledges 4,5,6,7 +---- + +The receiver's ACK after the retransmitted segment arrives acknowledges everything it has buffered -- not just segment 4, but also 5, 6, and 7 that it already held. The sender instantly knows all four segments were delivered. + +== Fast Recovery + +After a fast retransmit, TCP could reset the congestion window to one segment and re-enter slow start, just as it does after a timeout. But that would be overly conservative. The fact that duplicate ACKs are arriving means segments are still getting through -- the network is not completely broken, just slightly congested. + +Fast recovery takes a gentler approach. Instead of dropping the congestion window to one segment, TCP halves it. The sender continues transmitting at the reduced rate, and as ACKs for the retransmitted data arrive, the window gradually expands back toward its previous size. + +The combination of fast retransmit and fast recovery means TCP can handle occasional packet loss with minimal disruption to throughput. The sender detects the loss within a round trip, retransmits the missing segment, halves its speed briefly, and ramps back up. The connection barely stutters. + +== Congestion Avoidance + +Once TCP has exited slow start (either by reaching the receiver's window or by detecting loss), it enters *congestion avoidance* mode. The goal shifts from finding the network's capacity to staying just below it. + +In congestion avoidance, the congestion window grows linearly rather than exponentially: it increases by roughly one segment per round trip, instead of doubling. This cautious growth probes for additional capacity without overshooting. + +When loss is detected (via timeout or duplicate ACKs), TCP records half of the current congestion window as a *threshold*. If it re-enters slow start, exponential growth continues only until the window reaches this threshold, at which point it switches to linear growth. This prevents TCP from repeatedly overshooting the same capacity limit. + +The result is a sawtooth pattern: the congestion window grows linearly, hits a loss event, drops sharply, and grows linearly again. Over time, the window oscillates around the network's available capacity. This is not elegant, but it is remarkably effective at sharing bandwidth fairly among competing connections. + +== The Persist Timer + +Flow control, described in the previous section, allows the receiver to advertise a zero window: "I have no buffer space; stop sending." The sender obeys and stops transmitting. + +But what if the receiver frees up buffer space and sends an updated window advertisement, and that ACK is lost? The sender would wait forever, believing the window is still zero. The receiver would wait forever, believing it already told the sender to resume. + +The *persist timer* breaks this deadlock. When the sender sees a zero window, it starts a timer. When the timer fires, the sender transmits a tiny *window probe* -- a segment with one byte of data. If the receiver's window has opened, the ACK will contain the updated window size and the sender resumes. If the window is still zero, the receiver re-advertises zero and the sender sets the timer again. + +Persist probes use exponential backoff, starting at the RTO value and increasing up to a maximum (typically 60 seconds). The sender will probe indefinitely -- it never gives up on a zero-window connection. + +== Silly Window Syndrome + +A related pathology occurs when the receiver advertises a very small window -- say, 10 bytes -- and the sender dutifully transmits a 10-byte segment. The overhead of the IP and TCP headers (at least 40 bytes) dwarfs the payload. The connection becomes grossly inefficient, with more bandwidth consumed by headers than data. + +This is called *silly window syndrome*, and both sides participate in preventing it: + +* The *receiver* avoids advertising small window updates. It waits until it can advertise at least one full-sized segment (or half its buffer) before sending a window update. +* The *sender* avoids sending tiny segments. The Nagle algorithm (described in the previous section) helps here by batching small writes. + +Together, these rules ensure that data flows in reasonably-sized chunks even when the receiver is slow. + +== Keepalive + +What happens when a TCP connection is idle -- no data flowing in either direction? The answer is: nothing. TCP sends no packets during idle periods. The connection can sit open for hours, days, or weeks with no traffic. + +This is usually fine, but it creates a problem: if the remote machine crashes, reboots, or loses network connectivity during an idle period, the local side has no way to discover it. The connection appears healthy, but the first attempt to send data will fail -- possibly after a long timeout. + +*Keepalive* probes address this. When enabled, TCP periodically sends a tiny probe on idle connections -- typically every two hours. If the remote side responds, the connection is healthy. If no response arrives after several probes, TCP declares the connection dead and reports an error to the application. + +Keepalive is not enabled by default on most systems. Applications that need it set the `SO_KEEPALIVE` socket option and often adjust the probe interval, count, and idle timeout to match their requirements. Two hours is too long for many use cases; a chat server or database client might want to detect dead connections within 30 seconds. + +== Why This Matters to You + +TCP's reliability mechanisms run inside the kernel, invisible to your application. But understanding them explains behaviors you will observe: + +* *Brief stalls during data transfer* are often fast retransmit and recovery in action. A single lost packet causes the sender to pause momentarily while it detects the loss and retransmits. +* *Sudden throughput drops* happen when TCP detects congestion and halves its sending rate. The sawtooth pattern of congestion avoidance is normal, not a bug. +* *Long pauses after severe loss* indicate timeout-based retransmission. The sender waited for the RTO to expire because duplicate ACKs did not arrive (perhaps multiple consecutive segments were lost). +* *Connections that hang indefinitely* may be waiting on a peer that crashed during an idle period. Enable keepalive or implement application-level heartbeats. + +The reliability machinery is not "just retransmit on loss." It is a carefully tuned feedback loop between sender, receiver, and network. The next and final section looks at the extensions that push TCP's performance beyond what the original protocol could achieve. diff --git a/doc/modules/ROOT/pages/2.networking-tutorial/2l.tcp-performance.adoc b/doc/modules/ROOT/pages/2.networking-tutorial/2l.tcp-performance.adoc new file mode 100644 index 00000000..ea6399fd --- /dev/null +++ b/doc/modules/ROOT/pages/2.networking-tutorial/2l.tcp-performance.adoc @@ -0,0 +1,88 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Making TCP Fast + +TCP was designed when networks ran at kilobits per second and the entire internet fit on a single backbone. The core protocol -- sequence numbers, acknowledgments, sliding windows -- scales remarkably well, but some of its original parameters do not. A 16-bit window field caps the amount of data in flight at 65,535 bytes. Sequence numbers wrap around on fast links. RTT measurements lose precision when segments fly faster than the clock ticks. + +Over the decades, a set of extensions has been added to TCP to remove these bottlenecks. They are negotiated during the handshake and are transparent to your application code, but understanding them tells you where TCP performance comes from and where the limits still lie. + +== Path MTU Discovery + +As described in the IP section, packets that exceed a link's MTU get fragmented. Fragmentation is bad for TCP: if any fragment is lost, the entire segment must be retransmitted. It also adds reassembly overhead on the receiver. + +Path MTU discovery lets TCP find the largest segment it can send without triggering fragmentation anywhere along the path. The mechanism works like this: + +. The sender sets the "Don't Fragment" (DF) flag on every IP packet. +. If a router along the path cannot forward the packet because it exceeds the link's MTU, the router drops the packet and sends back an ICMP error message: "Fragmentation needed, but DF is set." The message includes the MTU of the link that rejected the packet. +. The sender reduces its segment size to fit the reported MTU and retransmits. + +Over the first few segments, the sender discovers the path MTU and adjusts accordingly. From that point on, segments are sized to avoid fragmentation entirely. Most modern operating systems perform path MTU discovery by default. + +The result is measurable: segments are as large as the path allows, maximizing the ratio of payload to headers, and fragmentation-related losses disappear. + +== The Bandwidth-Delay Product + +The maximum throughput of a TCP connection is limited by how much data can be in flight at any given time. That amount is determined by the *bandwidth-delay product* (BDP): the link's bandwidth multiplied by the round-trip time. + +Consider a 100 Mbps link with a 50-millisecond RTT. The bandwidth-delay product is: + +---- +100,000,000 bits/sec × 0.050 sec = 5,000,000 bits = 625,000 bytes +---- + +To fully utilize this link, the sender must have 625,000 bytes of data in flight simultaneously. If the TCP window is smaller than the BDP, the sender will finish transmitting its window and then sit idle waiting for ACKs, leaving bandwidth unused. + +Networks with high bandwidth and high latency -- satellite links, transcontinental fiber, data center interconnects -- have large BDPs. These are sometimes called *long fat networks*, and they expose the original TCP window's 65,535-byte limit as a severe bottleneck. + +On a transoceanic link at 10 Gbps with a 100-millisecond RTT, the BDP is 125 megabytes. A 64 KB window would utilize less than 0.05% of the available bandwidth. Without the window scale extension, such a link would be essentially unusable for a single TCP connection. + +== Window Scaling + +The original TCP header allocates 16 bits for the window size, giving a maximum of 65,535 bytes. This was generous in 1981. It is completely inadequate for modern networks. + +The *window scale* option, negotiated during the three-way handshake, multiplies the window field by a power of two. Each side includes a window scale option in its SYN segment, specifying a shift count from 0 to 14. A shift count of 7 means the window field is multiplied by 128; a value of 4,096 in the header represents an actual window of 524,288 bytes. + +The maximum shift count of 14 allows a window of up to 1,073,725,440 bytes -- over one gigabyte. This is sufficient to fill even the fastest networks with the highest latencies. + +Window scaling is negotiated once during the handshake and applies for the lifetime of the connection. Both sides must support it; if either side's SYN does not include the option, window scaling is not used. In practice, every modern operating system enables it by default. + +== Timestamps + +TCP segments can carry a *timestamp option*: the sender includes its current clock value, and the receiver echoes it back in the ACK. This serves two purposes: + +Improved RTT measurement:: In the original protocol, TCP could only measure the RTT of one segment per window. With timestamps, every ACK carries an echo of the original send time, giving TCP a precise RTT sample for every segment. More samples mean a more accurate smoothed RTT and a tighter retransmission timeout. + +Protection against wrapped sequence numbers:: On fast links, the 32-bit sequence number space can wrap around quickly. A 10 Gbps link exhausts all four billion sequence numbers in about 3.4 seconds. If a delayed segment from a previous wrap-around arrives, its sequence number might match a valid position in the current stream. The timestamp detects this: the delayed segment carries an old timestamp, and TCP rejects it. + +This second use is called *PAWS* (Protection Against Wrapped Sequence Numbers). Without it, high-speed connections would be vulnerable to data corruption from stale segments. With it, the timestamp acts as an additional dimension of validation beyond the sequence number. + +Like window scaling, timestamps are negotiated during the handshake. Both sides must agree to use them. The overhead is 12 bytes per segment (10 bytes for the option plus 2 bytes of padding), which is negligible on modern networks. + +== Practical Performance Considerations + +The extensions described above operate transparently inside the kernel. Your application does not set window scale factors or insert timestamps. But there are application-level decisions that affect TCP performance significantly: + +Buffer sizing:: The operating system maintains send and receive buffers for each socket. If the receive buffer is too small, the receiver cannot advertise a large enough window to fill the pipe. If the send buffer is too small, the application may block on write calls before TCP has finished transmitting the previous batch. Most operating systems auto-tune these buffers, but high-throughput applications sometimes benefit from explicitly setting `SO_SNDBUF` and `SO_RCVBUF` to match the BDP. + +Avoiding small writes:: As discussed in the data flow section, many small write calls interact poorly with the Nagle algorithm and produce unnecessary overhead. Buffering application data and writing it in larger chunks -- or setting `TCP_NODELAY` -- avoids this. + +Connection reuse:: Slow start means a new TCP connection takes several round trips to ramp up to full throughput. For protocols like HTTP, reusing connections across multiple requests amortizes the slow start cost. HTTP/2 goes further by multiplexing many requests over a single connection. + +TLS overhead:: When TCP carries encrypted traffic (TLS), the handshake adds additional round trips before application data flows. TLS 1.3 reduces this to one round trip (or zero for resumed sessions), but the cost still matters for short-lived connections. + +Kernel tuning:: For specialized workloads -- high-frequency trading, large-scale file transfer, or high-connection-count servers -- kernel parameters like the maximum receive window, congestion control algorithm, and SYN backlog size can be tuned for better performance. These are operating-system-specific and should be adjusted based on measurement, not guesswork. + +== The Achievement + +TCP was designed for a network that measured bandwidth in kilobits and latency in single-digit milliseconds. The same protocol now saturates 100-gigabit links across continents, handles billions of concurrent connections, and underpins virtually every application on the internet. + +That longevity comes from two design choices: keeping the core protocol minimal and making it extensible. The original TCP header has room for options. The options mechanism enabled window scaling, timestamps, selective acknowledgments, and dozens of other enhancements -- all negotiated at connection time, all backward-compatible with implementations that do not support them. + +Your application benefits from all of this without doing anything special. You open a socket, write data, read data, and close the socket. The operating system handles the rest. But when performance matters -- when you are diagnosing a slow transfer, tuning a high-throughput server, or choosing between TCP and UDP -- understanding the machinery behind the socket is what lets you make informed decisions instead of guessing. diff --git a/doc/modules/ROOT/pages/3.tutorials/3.intro.adoc b/doc/modules/ROOT/pages/3.tutorials/3.intro.adoc new file mode 100644 index 00000000..4f0cccc4 --- /dev/null +++ b/doc/modules/ROOT/pages/3.tutorials/3.intro.adoc @@ -0,0 +1,39 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Tutorials + +Networked applications come alive when you build them yourself. Reading +about sockets, protocols, and concurrency only goes so far — the real +understanding comes from writing code that opens connections, sends data, +and handles the responses. These tutorials give you that hands-on experience +with Corosio. + +Each tutorial produces a complete, runnable program. You start with source +code, compile it, and interact with the result. Along the way you encounter +the patterns that recur throughout real networking code: listening for +incoming connections, connecting to remote services, formatting and parsing +protocol messages, and layering encryption on top of a transport. + +The progression moves from straightforward client-server interactions toward +more involved topics. Early tutorials focus on raw TCP communication — reading +and writing bytes over a connection. From there you move into higher-level +protocols where message framing and request-response semantics matter. +Security appears naturally as you add TLS to protect data in transit, covering +certificate management and encrypted streams. + +Throughout the tutorials you will see how Corosio's coroutine-based model +keeps asynchronous code readable. Operations that would require callbacks +or state machines in traditional networking code read as sequential steps +in a coroutine body. Error handling follows consistent patterns whether +you prefer structured bindings or exceptions. + +NOTE: These tutorials assume familiarity with Capy tasks and basic C++20 +coroutine concepts such as `co_await` and `co_return`. If you are new to +coroutines, review the Capy documentation before proceeding. diff --git a/doc/modules/ROOT/pages/tutorials/echo-server.adoc b/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc similarity index 91% rename from doc/modules/ROOT/pages/tutorials/echo-server.adoc rename to doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc index 6b0fe326..2f98dfe6 100644 --- a/doc/modules/ROOT/pages/tutorials/echo-server.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc @@ -1,264 +1,264 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= Echo Server Tutorial - -This tutorial builds a production-quality echo server using the `tcp_server` -framework. We'll explore worker pools, connection lifecycle, and the launcher -pattern. - -NOTE: Code snippets assume: -[source,cpp] ----- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; ----- - -== Overview - -An echo server accepts TCP connections and sends back whatever data clients -send. While simple, this pattern demonstrates core concepts: - -* Using `tcp_server` for connection management -* Implementing workers with `worker_base` -* Launching session coroutines with `launcher` -* Reading and writing data with sockets - -== Architecture - -The `tcp_server` framework uses a worker pool pattern: - -1. Derive from `tcp_server` and define your worker type -2. Preallocate workers during construction -3. The framework accepts connections and dispatches them to idle workers -4. Workers run session coroutines and return to the pool when done - -This avoids allocation during operation and limits resource usage. - -== Worker Implementation - -Workers derive from `worker_base` and implement two methods: - -[source,cpp] ----- -class echo_server : public corosio::tcp_server -{ - class worker : public worker_base - { - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string buf_; - - public: - explicit worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - buf_.reserve(4096); - } - - corosio::tcp_socket& socket() override - { - return sock_; - } - - void run(launcher launch) override - { - launch(ctx_.get_executor(), do_session()); - } - - capy::task<> do_session(); - }; ----- - -Each worker: - -* Stores a reference to the `io_context` for executor access -* Owns its socket (returned via `socket()`) -* Owns any per-connection state (like the buffer) -* Implements `run()` to launch the session coroutine - -== Session Coroutine - -The session coroutine handles one connection: - -[source,cpp] ----- -capy::task<> echo_server::worker::do_session() -{ - for (;;) - { - buf_.resize(4096); - - // Read some data - auto [ec, n] = co_await sock_.read_some( - capy::mutable_buffer(buf_.data(), buf_.size())); - - if (ec || n == 0) - break; - - buf_.resize(n); - - // Echo it back - auto [wec, wn] = co_await corosio::write( - sock_, capy::const_buffer(buf_.data(), buf_.size())); - - if (wec) - break; - } - - sock_.close(); -} ----- - -Notice: - -* We reuse the worker's buffer across reads -* `read_some()` returns when _any_ data arrives -* `corosio::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: - -[source,cpp] ----- -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(ctx); - } -}; ----- - -Workers are stored polymorphically via `wv_.emplace()`, allowing different -worker types if needed. - -== Main Function - -[source,cpp] ----- -int main(int argc, char* argv[]) -{ - if (argc != 3) - { - std::cerr << "Usage: echo_server \n"; - return 1; - } - - auto port = static_cast(std::atoi(argv[1])); - int max_workers = std::atoi(argv[2]); - - corosio::io_context ioc; - - echo_server server(ioc, max_workers); - - auto ec = server.bind(corosio::endpoint(port)); - if (ec) - { - std::cerr << "Bind failed: " << ec.message() << "\n"; - return 1; - } - - std::cout << "Echo server listening on port " << port - << " with " << max_workers << " workers\n"; - - server.start(); - ioc.run(); -} ----- - -== Key Design Decisions - -=== Why tcp_server? - -The `tcp_server` framework provides: - -* **Automatic pool management**: Workers cycle between idle and active states -* **Safe lifecycle**: The launcher ensures workers return to the pool -* **Multiple ports**: Bind to several endpoints sharing one worker pool - -=== Why Worker Pooling? - -* **Bounded memory**: Fixed number of connections -* **No allocation**: Sockets and buffers preallocated -* **Simple accounting**: Framework tracks worker availability - -=== Why Composed Write? - -The `corosio::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 ----- - -For echo servers, we want complete message delivery. - -=== Why Not Use Exceptions? - -The session loop needs to handle EOF gracefully. Using structured bindings: - -[source,cpp] ----- -auto [ec, n] = co_await sock.read_some(buf); -if (ec || n == 0) - break; // Normal termination path ----- - -With exceptions, EOF would require a try-catch: - -[source,cpp] ----- -try { - auto n = (co_await sock.read_some(buf)).value(); -} catch (...) { - // EOF is an exception here -} ----- - -== Testing - -Start the server: - -[source,bash] ----- -$ ./echo_server 8080 10 -Echo server listening on port 8080 with 10 workers ----- - -Connect with netcat: - -[source,bash] ----- -$ nc localhost 8080 -Hello -Hello -World -World ----- - -== Next Steps - -* xref:http-client.adoc[HTTP Client] — Build an HTTP client -* xref:../guide/tcp-server.adoc[TCP Server Guide] — Deep dive into tcp_server -* xref:../guide/sockets.adoc[Sockets Guide] — Deep dive into socket operations -* xref:../guide/composed-operations.adoc[Composed Operations] — Understanding read/write +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Echo Server Tutorial + +This tutorial builds a production-quality echo server using the `tcp_server` +framework. We'll explore worker pools, connection lifecycle, and the launcher +pattern. + +NOTE: Code snippets assume: +[source,cpp] +---- +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +---- + +== Overview + +An echo server accepts TCP connections and sends back whatever data clients +send. While simple, this pattern demonstrates core concepts: + +* Using `tcp_server` for connection management +* Implementing workers with `worker_base` +* Launching session coroutines with `launcher` +* Reading and writing data with sockets + +== Architecture + +The `tcp_server` framework uses a worker pool pattern: + +1. Derive from `tcp_server` and define your worker type +2. Preallocate workers during construction +3. The framework accepts connections and dispatches them to idle workers +4. Workers run session coroutines and return to the pool when done + +This avoids allocation during operation and limits resource usage. + +== Worker Implementation + +Workers derive from `worker_base` and implement two methods: + +[source,cpp] +---- +class echo_server : public corosio::tcp_server +{ + class worker : public worker_base + { + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string buf_; + + public: + explicit worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) + { + buf_.reserve(4096); + } + + corosio::tcp_socket& socket() override + { + return sock_; + } + + void run(launcher launch) override + { + launch(ctx_.get_executor(), do_session()); + } + + capy::task<> do_session(); + }; +---- + +Each worker: + +* Stores a reference to the `io_context` for executor access +* Owns its socket (returned via `socket()`) +* Owns any per-connection state (like the buffer) +* Implements `run()` to launch the session coroutine + +== Session Coroutine + +The session coroutine handles one connection: + +[source,cpp] +---- +capy::task<> echo_server::worker::do_session() +{ + for (;;) + { + buf_.resize(4096); + + // Read some data + auto [ec, n] = co_await sock_.read_some( + capy::mutable_buffer(buf_.data(), buf_.size())); + + if (ec || n == 0) + break; + + buf_.resize(n); + + // Echo it back + auto [wec, wn] = co_await corosio::write( + sock_, capy::const_buffer(buf_.data(), buf_.size())); + + if (wec) + break; + } + + sock_.close(); +} +---- + +Notice: + +* We reuse the worker's buffer across reads +* `read_some()` returns when _any_ data arrives +* `corosio::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: + +[source,cpp] +---- +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(ctx); + } +}; +---- + +Workers are stored polymorphically via `wv_.emplace()`, allowing different +worker types if needed. + +== Main Function + +[source,cpp] +---- +int main(int argc, char* argv[]) +{ + if (argc != 3) + { + std::cerr << "Usage: echo_server \n"; + return 1; + } + + auto port = static_cast(std::atoi(argv[1])); + int max_workers = std::atoi(argv[2]); + + corosio::io_context ioc; + + echo_server server(ioc, max_workers); + + auto ec = server.bind(corosio::endpoint(port)); + if (ec) + { + std::cerr << "Bind failed: " << ec.message() << "\n"; + return 1; + } + + std::cout << "Echo server listening on port " << port + << " with " << max_workers << " workers\n"; + + server.start(); + ioc.run(); +} +---- + +== Key Design Decisions + +=== Why tcp_server? + +The `tcp_server` framework provides: + +* **Automatic pool management**: Workers cycle between idle and active states +* **Safe lifecycle**: The launcher ensures workers return to the pool +* **Multiple ports**: Bind to several endpoints sharing one worker pool + +=== Why Worker Pooling? + +* **Bounded memory**: Fixed number of connections +* **No allocation**: Sockets and buffers preallocated +* **Simple accounting**: Framework tracks worker availability + +=== Why Composed Write? + +The `corosio::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 +---- + +For echo servers, we want complete message delivery. + +=== Why Not Use Exceptions? + +The session loop needs to handle EOF gracefully. Using structured bindings: + +[source,cpp] +---- +auto [ec, n] = co_await sock.read_some(buf); +if (ec || n == 0) + break; // Normal termination path +---- + +With exceptions, EOF would require a try-catch: + +[source,cpp] +---- +try { + auto n = (co_await sock.read_some(buf)).value(); +} catch (...) { + // EOF is an exception here +} +---- + +== Testing + +Start the server: + +[source,bash] +---- +$ ./echo_server 8080 10 +Echo server listening on port 8080 with 10 workers +---- + +Connect with netcat: + +[source,bash] +---- +$ nc localhost 8080 +Hello +Hello +World +World +---- + +== Next Steps + +* xref: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 diff --git a/doc/modules/ROOT/pages/tutorials/http-client.adoc b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc similarity index 95% rename from doc/modules/ROOT/pages/tutorials/http-client.adoc rename to doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc index e9960b10..f1ace714 100644 --- a/doc/modules/ROOT/pages/tutorials/http-client.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc @@ -243,6 +243,6 @@ The `do_request` function works unchanged because both `socket` and == Next Steps -* xref:dns-lookup.adoc[DNS Lookup] — Resolve hostnames to addresses -* xref:../guide/tls.adoc[TLS Guide] — WolfSSL integration details -* xref:../guide/composed-operations.adoc[Composed Operations] — How read/write work +* xref: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 diff --git a/doc/modules/ROOT/pages/tutorials/dns-lookup.adoc b/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc similarity index 91% rename from doc/modules/ROOT/pages/tutorials/dns-lookup.adoc rename to doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc index 22be14ee..57a846c0 100644 --- a/doc/modules/ROOT/pages/tutorials/dns-lookup.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc @@ -1,246 +1,246 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= DNS Lookup Tutorial - -This tutorial builds a command-line DNS lookup tool similar to `nslookup`. -You'll learn to use the asynchronous resolver to convert hostnames to IP -addresses. - -NOTE: Code snippets assume: -[source,cpp] ----- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; ----- - -== Overview - -DNS resolution converts a hostname like `www.example.com` to one or more IP -addresses. The `resolver` class performs this asynchronously: - -[source,cpp] ----- -corosio::resolver r(ioc); -auto [ec, results] = co_await r.resolve("www.example.com", "https"); ----- - -The second argument is the service name (or port number as a string). It -determines the port in the returned endpoints. - -== The Lookup Coroutine - -[source,cpp] ----- -capy::task do_lookup( - corosio::io_context& ioc, - std::string_view host, - std::string_view service) -{ - corosio::resolver r(ioc); - - auto [ec, results] = co_await r.resolve(host, service); - if (ec) - { - std::cerr << "Resolve failed: " << ec.message() << "\n"; - co_return; - } - - std::cout << "Results for " << host; - if (!service.empty()) - std::cout << ":" << service; - std::cout << "\n"; - - for (auto const& entry : results) - { - auto ep = entry.get_endpoint(); - if (ep.is_v4()) - { - std::cout << " IPv4: " << ep.v4_address().to_string() - << ":" << ep.port() << "\n"; - } - else - { - std::cout << " IPv6: " << ep.v6_address().to_string() - << ":" << ep.port() << "\n"; - } - } - - std::cout << "\nTotal: " << results.size() << " addresses\n"; -} ----- - -== Understanding Results - -The resolver returns a `resolver_results` object containing `resolver_entry` -elements. Each entry provides: - -* `get_endpoint()` — The resolved endpoint (address + port) -* `host_name()` — The queried hostname -* `service_name()` — The queried service - -The `endpoint` class supports both IPv4 and IPv6: - -[source,cpp] ----- -auto ep = entry.get_endpoint(); - -if (ep.is_v4()) -{ - // IPv4 address - boost::urls::ipv4_address addr = ep.v4_address(); -} -else -{ - // IPv6 address - boost::urls::ipv6_address addr = ep.v6_address(); -} - -std::uint16_t port = ep.port(); ----- - -== Main Function - -[source,cpp] ----- -int main(int argc, char* argv[]) -{ - if (argc < 2 || argc > 3) - { - std::cerr << "Usage: nslookup [service]\n" - << "Examples:\n" - << " nslookup www.google.com\n" - << " nslookup www.google.com https\n" - << " nslookup localhost 8080\n"; - return 1; - } - - std::string_view host = argv[1]; - std::string_view service = (argc == 3) ? argv[2] : ""; - - corosio::io_context ioc; - capy::run_async(ioc.get_executor())(do_lookup(ioc, host, service)); - ioc.run(); -} ----- - -== Resolver Flags - -The resolver accepts optional flags to control behavior: - -[source,cpp] ----- -auto [ec, results] = co_await r.resolve( - host, service, - corosio::resolve_flags::numeric_host | - corosio::resolve_flags::numeric_service); ----- - -Available flags: - -[cols="1,3"] -|=== -| Flag | Description - -| `passive` -| Return endpoints suitable for binding (server use) - -| `numeric_host` -| Host is a numeric address string, skip DNS - -| `numeric_service` -| Service is a port number string - -| `address_configured` -| Only return addresses if configured on the system - -| `v4_mapped` -| Return IPv4-mapped IPv6 addresses if no IPv6 found - -| `all_matching` -| With `v4_mapped`, return all matching addresses -|=== - -== Connecting to Resolved Addresses - -After resolving, iterate through results to find a working connection: - -[source,cpp] ----- -capy::task connect_to_host( - corosio::io_context& ioc, - std::string_view host, - std::string_view service) -{ - corosio::resolver r(ioc); - auto [resolve_ec, results] = co_await r.resolve(host, service); - if (resolve_ec) - throw boost::system::system_error(resolve_ec); - - corosio::tcp_socket sock(ioc); - sock.open(); - - // Try each address until one works - boost::system::error_code last_ec; - for (auto const& entry : results) - { - auto [ec] = co_await sock.connect(entry.get_endpoint()); - if (!ec) - { - std::cout << "Connected to " << host << "\n"; - co_return; - } - last_ec = ec; - } - - throw boost::system::system_error(last_ec, "all addresses failed"); -} ----- - -== Running the Lookup Tool - -[source,bash] ----- -$ ./nslookup www.google.com https -Results for www.google.com:https - IPv4: 142.250.189.68:443 - IPv6: 2607:f8b0:4004:800::2004:443 - -Total: 2 addresses ----- - -[source,bash] ----- -$ ./nslookup localhost 8080 -Results for localhost:8080 - IPv4: 127.0.0.1:8080 - -Total: 1 addresses ----- - -== Cancellation - -Resolver operations support cancellation via `std::stop_token`: - -[source,cpp] ----- -r.cancel(); // Cancel pending operation ----- - -Or through the affine awaitable protocol when using `capy::jcancellable_task`. - -== Next Steps - -* xref:../guide/resolver.adoc[Resolver Guide] — Full resolver reference -* xref:../guide/endpoints.adoc[Endpoints Guide] — Working with addresses -* xref:http-client.adoc[HTTP Client] — Use resolved addresses for connections +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += DNS Lookup Tutorial + +This tutorial builds a command-line DNS lookup tool similar to `nslookup`. +You'll learn to use the asynchronous resolver to convert hostnames to IP +addresses. + +NOTE: Code snippets assume: +[source,cpp] +---- +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +---- + +== Overview + +DNS resolution converts a hostname like `www.example.com` to one or more IP +addresses. The `resolver` class performs this asynchronously: + +[source,cpp] +---- +corosio::resolver r(ioc); +auto [ec, results] = co_await r.resolve("www.example.com", "https"); +---- + +The second argument is the service name (or port number as a string). It +determines the port in the returned endpoints. + +== The Lookup Coroutine + +[source,cpp] +---- +capy::task do_lookup( + corosio::io_context& ioc, + std::string_view host, + std::string_view service) +{ + corosio::resolver r(ioc); + + auto [ec, results] = co_await r.resolve(host, service); + if (ec) + { + std::cerr << "Resolve failed: " << ec.message() << "\n"; + co_return; + } + + std::cout << "Results for " << host; + if (!service.empty()) + std::cout << ":" << service; + std::cout << "\n"; + + for (auto const& entry : results) + { + auto ep = entry.get_endpoint(); + if (ep.is_v4()) + { + std::cout << " IPv4: " << ep.v4_address().to_string() + << ":" << ep.port() << "\n"; + } + else + { + std::cout << " IPv6: " << ep.v6_address().to_string() + << ":" << ep.port() << "\n"; + } + } + + std::cout << "\nTotal: " << results.size() << " addresses\n"; +} +---- + +== Understanding Results + +The resolver returns a `resolver_results` object containing `resolver_entry` +elements. Each entry provides: + +* `get_endpoint()` — The resolved endpoint (address + port) +* `host_name()` — The queried hostname +* `service_name()` — The queried service + +The `endpoint` class supports both IPv4 and IPv6: + +[source,cpp] +---- +auto ep = entry.get_endpoint(); + +if (ep.is_v4()) +{ + // IPv4 address + boost::urls::ipv4_address addr = ep.v4_address(); +} +else +{ + // IPv6 address + boost::urls::ipv6_address addr = ep.v6_address(); +} + +std::uint16_t port = ep.port(); +---- + +== Main Function + +[source,cpp] +---- +int main(int argc, char* argv[]) +{ + if (argc < 2 || argc > 3) + { + std::cerr << "Usage: nslookup [service]\n" + << "Examples:\n" + << " nslookup www.google.com\n" + << " nslookup www.google.com https\n" + << " nslookup localhost 8080\n"; + return 1; + } + + std::string_view host = argv[1]; + std::string_view service = (argc == 3) ? argv[2] : ""; + + corosio::io_context ioc; + capy::run_async(ioc.get_executor())(do_lookup(ioc, host, service)); + ioc.run(); +} +---- + +== Resolver Flags + +The resolver accepts optional flags to control behavior: + +[source,cpp] +---- +auto [ec, results] = co_await r.resolve( + host, service, + corosio::resolve_flags::numeric_host | + corosio::resolve_flags::numeric_service); +---- + +Available flags: + +[cols="1,3"] +|=== +| Flag | Description + +| `passive` +| Return endpoints suitable for binding (server use) + +| `numeric_host` +| Host is a numeric address string, skip DNS + +| `numeric_service` +| Service is a port number string + +| `address_configured` +| Only return addresses if configured on the system + +| `v4_mapped` +| Return IPv4-mapped IPv6 addresses if no IPv6 found + +| `all_matching` +| With `v4_mapped`, return all matching addresses +|=== + +== Connecting to Resolved Addresses + +After resolving, iterate through results to find a working connection: + +[source,cpp] +---- +capy::task connect_to_host( + corosio::io_context& ioc, + std::string_view host, + std::string_view service) +{ + corosio::resolver r(ioc); + auto [resolve_ec, results] = co_await r.resolve(host, service); + if (resolve_ec) + throw boost::system::system_error(resolve_ec); + + corosio::tcp_socket sock(ioc); + sock.open(); + + // Try each address until one works + boost::system::error_code last_ec; + for (auto const& entry : results) + { + auto [ec] = co_await sock.connect(entry.get_endpoint()); + if (!ec) + { + std::cout << "Connected to " << host << "\n"; + co_return; + } + last_ec = ec; + } + + throw boost::system::system_error(last_ec, "all addresses failed"); +} +---- + +== Running the Lookup Tool + +[source,bash] +---- +$ ./nslookup www.google.com https +Results for www.google.com:https + IPv4: 142.250.189.68:443 + IPv6: 2607:f8b0:4004:800::2004:443 + +Total: 2 addresses +---- + +[source,bash] +---- +$ ./nslookup localhost 8080 +Results for localhost:8080 + IPv4: 127.0.0.1:8080 + +Total: 1 addresses +---- + +== Cancellation + +Resolver operations support cancellation via `std::stop_token`: + +[source,cpp] +---- +r.cancel(); // Cancel pending operation +---- + +Or through the affine awaitable protocol when using `capy::jcancellable_task`. + +== Next Steps + +* xref:../4.guide/4j.resolver.adoc[Resolver Guide] — Full resolver reference +* xref:../4.guide/4f.endpoints.adoc[Endpoints Guide] — Working with addresses +* xref:3b.http-client.adoc[HTTP Client] — Use resolved addresses for connections diff --git a/doc/modules/ROOT/pages/tutorials/tls-context.adoc b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc similarity index 99% rename from doc/modules/ROOT/pages/tutorials/tls-context.adoc rename to doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc index b021c840..a1e44ff8 100644 --- a/doc/modules/ROOT/pages/tutorials/tls-context.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc @@ -573,5 +573,5 @@ Common errors include: == Next Steps -* xref:../guide/tls.adoc[TLS Encryption] — Using TLS streams -* xref:http-client.adoc[HTTP Client Tutorial] — HTTPS example +* xref:../4.guide/4l.tls.adoc[TLS Encryption] — Using TLS streams +* xref:3b.http-client.adoc[HTTP Client Tutorial] — HTTPS example diff --git a/doc/modules/ROOT/pages/4.guide/4.intro.adoc b/doc/modules/ROOT/pages/4.guide/4.intro.adoc new file mode 100644 index 00000000..16022191 --- /dev/null +++ b/doc/modules/ROOT/pages/4.guide/4.intro.adoc @@ -0,0 +1,42 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Guide + +This guide provides a comprehensive reference for Corosio's components and the +concepts behind them. By the time you finish, you will have a deep understanding +of how each piece works, when to use it, and how the pieces fit together to +build robust networked applications. + +The guide begins with the foundations of event-driven I/O. You will learn how +operating systems handle asynchronous operations, how the event loop processes +work, and how coroutines provide concurrency without the complexity of threads. +These foundational topics establish the mental model you need to reason about +asynchronous code confidently. + +From there, the guide moves into networking primitives. You will explore how TCP +connections are established and managed, how addresses and ports identify +communicating processes, and how data flows through sockets as byte streams. +Each component is covered in detail so you understand not just the API, but the +underlying mechanics that inform correct usage. + +More advanced topics build on these foundations. You will encounter patterns for +building scalable servers, managing connection lifecycles, composing operations +for reliable data transfer, and securing connections with encryption. The guide +also covers practical concerns like error handling strategies, timer management, +signal handling, and name resolution. + +Each topic builds naturally on previous concepts, but the sections are designed +to be read independently. If you already understand a particular area, you can +skip ahead to the topics that interest you most. + +The guide complements the tutorials with thorough API coverage and in-depth +design explanations. Where the tutorials focus on building complete working +programs, the guide provides the detailed understanding you need to adapt those +patterns to your own applications and troubleshoot issues when they arise. diff --git a/doc/modules/ROOT/pages/guide/tcp-networking.adoc b/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc similarity index 98% rename from doc/modules/ROOT/pages/guide/tcp-networking.adoc rename to doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc index 2e18dc74..4696ecb2 100644 --- a/doc/modules/ROOT/pages/guide/tcp-networking.adoc +++ b/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc @@ -11,7 +11,7 @@ This chapter introduces the networking concepts you need to understand before using Corosio. If you're already comfortable with TCP/IP, sockets, and the -client-server model, you can skip to xref:io-context.adoc[I/O Context]. +client-server model, you can skip to xref:4c.io-context.adoc[I/O Context]. == What is a Network? @@ -752,6 +752,6 @@ For a deeper understanding of TCP/IP: == Next Steps -* xref:concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands -* xref:io-context.adoc[I/O Context] — The event loop -* xref:sockets.adoc[Sockets] — Socket operations in detail +* xref:4b.concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands +* xref:4c.io-context.adoc[I/O Context] — The event loop +* xref:4d.sockets.adoc[Sockets] — Socket operations in detail diff --git a/doc/modules/ROOT/pages/guide/concurrent-programming.adoc b/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc similarity index 97% rename from doc/modules/ROOT/pages/guide/concurrent-programming.adoc rename to doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc index 21c5f6bc..8beabf05 100644 --- a/doc/modules/ROOT/pages/guide/concurrent-programming.adoc +++ b/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc @@ -328,8 +328,6 @@ Corosio operations implement the affine awaitable protocol. When you `co_await` an I/O operation, it captures your executor and resumes through it. This happens automatically—you don't need explicit dispatch calls. -See xref:../concepts/affine-awaitables.adoc[Affine Awaitables] for details. - == Strands: Synchronization Without Locks A _strand_ guarantees that handlers posted to it don't run concurrently. Even @@ -536,7 +534,7 @@ for (int i = 0; i < max_workers; ++i) ---- Corosio's `tcp_server` class implements this pattern—see -xref:tcp-server.adoc[TCP Server] for details. +xref:4k.tcp-server.adoc[TCP Server] for details. === Pipelines @@ -632,6 +630,5 @@ provides excellent performance with simple, race-free code. == Next Steps -* xref:io-context.adoc[I/O Context] — The event loop in detail -* xref:../concepts/affine-awaitables.adoc[Affine Awaitables] — How affinity propagates -* xref:../tutorials/echo-server.adoc[Echo Server] — Practical concurrency example +* xref:4c.io-context.adoc[I/O Context] — The event loop in detail +* xref:../3.tutorials/3a.echo-server.adoc[Echo Server] — Practical concurrency example diff --git a/doc/modules/ROOT/pages/guide/io-context.adoc b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc similarity index 95% rename from doc/modules/ROOT/pages/guide/io-context.adoc rename to doc/modules/ROOT/pages/4.guide/4c.io-context.adoc index 7f503b25..f0645700 100644 --- a/doc/modules/ROOT/pages/guide/io-context.adoc +++ b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc @@ -299,7 +299,6 @@ Future macOS support will use kqueue for: == Next Steps -* xref:sockets.adoc[Sockets] — I/O with TCP sockets -* xref:tcp_acceptor.adoc[Acceptors] — Accept incoming connections -* xref:timers.adoc[Timers] — Async delays and timeouts -* xref:../concepts/affine-awaitables.adoc[Affine Awaitables] — The dispatch protocol +* xref:4d.sockets.adoc[Sockets] — I/O with TCP sockets +* xref:4e.tcp-acceptor.adoc[Acceptors] — Accept incoming connections +* xref:4h.timers.adoc[Timers] — Async delays and timeouts diff --git a/doc/modules/ROOT/pages/guide/sockets.adoc b/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc similarity index 93% rename from doc/modules/ROOT/pages/guide/sockets.adoc rename to doc/modules/ROOT/pages/4.guide/4d.sockets.adoc index 60a03d2d..a4a1b61c 100644 --- a/doc/modules/ROOT/pages/guide/sockets.adoc +++ b/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc @@ -174,7 +174,7 @@ auto [ec, n] = co_await corosio::read(s, buf); // n == buffer_size(buf) or error occurred ---- -See xref:composed-operations.adoc[Composed Operations] for details. +See xref:4g.composed-operations.adoc[Composed Operations] for details. == Writing Data @@ -296,7 +296,7 @@ std::array bufs = { co_await s.read_some(bufs); ---- -See xref:buffers.adoc[Buffer Sequences] for details. +See xref:4n.buffers.adoc[Buffer Sequences] for details. == Thread Safety @@ -342,7 +342,7 @@ capy::task echo_client(corosio::io_context& ioc) == Next Steps -* xref:tcp_acceptor.adoc[Acceptors] — Accept incoming connections -* xref:endpoints.adoc[Endpoints] — IP addresses and ports -* xref:composed-operations.adoc[Composed Operations] — read() and write() -* xref:../tutorials/echo-server.adoc[Echo Server Tutorial] — Server example +* xref:4e.tcp-acceptor.adoc[Acceptors] — Accept incoming connections +* xref:4f.endpoints.adoc[Endpoints] — IP addresses and ports +* xref:4g.composed-operations.adoc[Composed Operations] — read() and write() +* xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Server example diff --git a/doc/modules/ROOT/pages/guide/tcp_acceptor.adoc b/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc similarity index 95% rename from doc/modules/ROOT/pages/guide/tcp_acceptor.adoc rename to doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc index 0ec70be0..3a835bdd 100644 --- a/doc/modules/ROOT/pages/guide/tcp_acceptor.adoc +++ b/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc @@ -313,7 +313,7 @@ capy::task run_server(corosio::io_context& ioc) == Relationship to tcp_server -For production servers, consider using xref:tcp-server.adoc[tcp_server] which +For production servers, consider using xref:4k.tcp-server.adoc[tcp_server] which provides: * Worker pool management @@ -326,6 +326,6 @@ upon. == Next Steps -* xref:sockets.adoc[Sockets] — Using accepted connections -* xref:tcp-server.adoc[TCP Server] — Higher-level server framework -* xref:../tutorials/echo-server.adoc[Echo Server Tutorial] — Complete example +* xref:4d.sockets.adoc[Sockets] — Using accepted connections +* xref:4k.tcp-server.adoc[TCP Server] — Higher-level server framework +* xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Complete example diff --git a/doc/modules/ROOT/pages/guide/endpoints.adoc b/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc similarity index 95% rename from doc/modules/ROOT/pages/guide/endpoints.adoc rename to doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc index 4cedd0da..40b0f297 100644 --- a/doc/modules/ROOT/pages/guide/endpoints.adoc +++ b/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc @@ -253,6 +253,6 @@ use from any thread. == Next Steps -* xref:sockets.adoc[Sockets] — Connect to endpoints -* xref:resolver.adoc[Name Resolution] — Convert hostnames to endpoints -* xref:../tutorials/dns-lookup.adoc[DNS Lookup Tutorial] — Practical resolution +* xref:4d.sockets.adoc[Sockets] — Connect to endpoints +* xref:4j.resolver.adoc[Name Resolution] — Convert hostnames to endpoints +* xref:../3.tutorials/3c.dns-lookup.adoc[DNS Lookup Tutorial] — Practical resolution diff --git a/doc/modules/ROOT/pages/guide/composed-operations.adoc b/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc similarity index 96% rename from doc/modules/ROOT/pages/guide/composed-operations.adoc rename to doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc index 29b33632..bd9e57fe 100644 --- a/doc/modules/ROOT/pages/guide/composed-operations.adoc +++ b/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc @@ -276,6 +276,6 @@ capy::task read_http_response(corosio::io_stream& stream) == Next Steps -* xref:sockets.adoc[Sockets] — The underlying stream interface -* xref:buffers.adoc[Buffer Sequences] — Working with buffers -* xref:../tutorials/http-client.adoc[HTTP Client Tutorial] — Practical example +* xref:4d.sockets.adoc[Sockets] — The underlying stream interface +* xref:4n.buffers.adoc[Buffer Sequences] — Working with buffers +* xref:../3.tutorials/3b.http-client.adoc[HTTP Client Tutorial] — Practical example diff --git a/doc/modules/ROOT/pages/guide/timers.adoc b/doc/modules/ROOT/pages/4.guide/4h.timers.adoc similarity index 91% rename from doc/modules/ROOT/pages/guide/timers.adoc rename to doc/modules/ROOT/pages/4.guide/4h.timers.adoc index c7c09adf..4260361b 100644 --- a/doc/modules/ROOT/pages/guide/timers.adoc +++ b/doc/modules/ROOT/pages/4.guide/4h.timers.adoc @@ -1,282 +1,282 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= Timers - -The `timer` class provides asynchronous delays and timeouts. It integrates -with the I/O context to schedule operations at specific times or after -durations. - -NOTE: Code snippets assume: -[source,cpp] ----- -#include -namespace corosio = boost::corosio; -using namespace std::chrono_literals; ----- - -== Overview - -Timers let you pause execution for a duration: - -[source,cpp] ----- -corosio::timer t(ioc); -t.expires_after(5s); -co_await t.wait(); // Suspends for 5 seconds ----- - -== Construction - -[source,cpp] ----- -corosio::io_context ioc; -corosio::timer t(ioc); // From execution context ----- - -== Setting Expiry Time - -=== Relative Time (Duration) - -[source,cpp] ----- -t.expires_after(100ms); // 100 milliseconds from now -t.expires_after(5s); // 5 seconds from now -t.expires_after(2min); // 2 minutes from now ----- - -Any `std::chrono::duration` type works. - -=== Absolute Time (Time Point) - -[source,cpp] ----- -auto deadline = std::chrono::steady_clock::now() + 10s; -t.expires_at(deadline); ----- - -=== Querying Expiry - -[source,cpp] ----- -corosio::timer::time_point when = t.expiry(); ----- - -== Waiting - -The `wait()` operation suspends until the timer expires: - -[source,cpp] ----- -t.expires_after(1s); -auto [ec] = co_await t.wait(); - -if (!ec) - std::cout << "Timer expired normally\n"; ----- - -=== Cancellation - -[source,cpp] ----- -t.cancel(); // Pending wait completes with capy::error::canceled ----- - -The wait completes immediately with an error: - -[source,cpp] ----- -auto [ec] = co_await t.wait(); -if (ec == capy::error::canceled) - std::cout << "Timer was cancelled\n"; ----- - -== Type Aliases - -[source,cpp] ----- -using clock_type = std::chrono::steady_clock; -using time_point = clock_type::time_point; -using duration = clock_type::duration; ----- - -The timer uses `steady_clock` for monotonic timing unaffected by system -clock adjustments. - -== Resetting Timers - -Setting a new expiry cancels any pending wait: - -[source,cpp] ----- -t.expires_after(10s); -// Later, before 10s elapses: -t.expires_after(5s); // Resets to 5s, cancels previous wait ----- - -== Use Cases - -=== Simple Delay - -[source,cpp] ----- -capy::task delayed_action(corosio::io_context& ioc) -{ - corosio::timer t(ioc); - t.expires_after(2s); - co_await t.wait(); - - std::cout << "2 seconds have passed\n"; -} ----- - -=== Periodic Timer - -[source,cpp] ----- -capy::task periodic_task(corosio::io_context& ioc) -{ - corosio::timer t(ioc); - - for (int i = 0; i < 10; ++i) - { - t.expires_after(1s); - co_await t.wait(); - std::cout << "Tick " << i << "\n"; - } -} ----- - -=== Operation Timeout - -[source,cpp] ----- -capy::task with_timeout( - corosio::io_context& ioc, - corosio::tcp_socket& sock) -{ - corosio::timer timeout(ioc); - timeout.expires_after(30s); - - // Start both operations - auto read_task = sock.read_some(buffer); - auto timeout_task = timeout.wait(); - - // In practice, use parallel composition utilities - // This is simplified for illustration -} ----- - -NOTE: For proper timeout handling, use Capy's parallel composition utilities -like `when_any` or cancellation tokens. - -=== Rate Limiting - -[source,cpp] ----- -capy::task rate_limited_work(corosio::io_context& ioc) -{ - corosio::timer t(ioc); - auto next_time = std::chrono::steady_clock::now(); - - for (int i = 0; i < 100; ++i) - { - // Do work - process_item(i); - - // Wait until next interval - next_time += 100ms; - t.expires_at(next_time); - auto [ec] = co_await t.wait(); - if (ec) - break; - } -} ----- - -Using absolute time points prevents drift in periodic operations. - -== Stop Token Cancellation - -Timer waits support stop token cancellation through the affine protocol: - -[source,cpp] ----- -// Inside a cancellable task: -auto [ec] = co_await t.wait(); -// Completes with capy::error::canceled if stop requested ----- - -== Move Semantics - -Timers are move-only: - -[source,cpp] ----- -corosio::timer t1(ioc); -corosio::timer t2 = std::move(t1); // OK - -corosio::timer t3 = t2; // Error: deleted copy constructor ----- - -Move assignment cancels any pending wait on the destination timer. - -IMPORTANT: Source and destination must share the same execution context. - -== Thread Safety - -[cols="1,2"] -|=== -| Operation | Thread Safety - -| Distinct timers -| Safe from different threads - -| Same timer -| NOT safe for concurrent operations -|=== - -Don't call `wait()`, `expires_after()`, or `cancel()` concurrently on the -same timer. - -== Example: Heartbeat - -[source,cpp] ----- -capy::task heartbeat( - corosio::io_context& ioc, - corosio::tcp_socket& sock, - std::atomic& running) -{ - corosio::timer t(ioc); - - while (running) - { - t.expires_after(30s); - auto [ec] = co_await t.wait(); - - if (ec) - break; - - // Send heartbeat - std::string ping = "PING\r\n"; - auto [wec, n] = co_await corosio::write( - sock, capy::const_buffer(ping.data(), ping.size())); - - if (wec) - break; - } -} ----- - -== Next Steps - -* xref:signals.adoc[Signal Handling] — Respond to OS signals -* xref:io-context.adoc[I/O Context] — The event loop -* xref:error-handling.adoc[Error Handling] — Cancellation patterns +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Timers + +The `timer` class provides asynchronous delays and timeouts. It integrates +with the I/O context to schedule operations at specific times or after +durations. + +NOTE: Code snippets assume: +[source,cpp] +---- +#include +namespace corosio = boost::corosio; +using namespace std::chrono_literals; +---- + +== Overview + +Timers let you pause execution for a duration: + +[source,cpp] +---- +corosio::timer t(ioc); +t.expires_after(5s); +co_await t.wait(); // Suspends for 5 seconds +---- + +== Construction + +[source,cpp] +---- +corosio::io_context ioc; +corosio::timer t(ioc); // From execution context +---- + +== Setting Expiry Time + +=== Relative Time (Duration) + +[source,cpp] +---- +t.expires_after(100ms); // 100 milliseconds from now +t.expires_after(5s); // 5 seconds from now +t.expires_after(2min); // 2 minutes from now +---- + +Any `std::chrono::duration` type works. + +=== Absolute Time (Time Point) + +[source,cpp] +---- +auto deadline = std::chrono::steady_clock::now() + 10s; +t.expires_at(deadline); +---- + +=== Querying Expiry + +[source,cpp] +---- +corosio::timer::time_point when = t.expiry(); +---- + +== Waiting + +The `wait()` operation suspends until the timer expires: + +[source,cpp] +---- +t.expires_after(1s); +auto [ec] = co_await t.wait(); + +if (!ec) + std::cout << "Timer expired normally\n"; +---- + +=== Cancellation + +[source,cpp] +---- +t.cancel(); // Pending wait completes with capy::error::canceled +---- + +The wait completes immediately with an error: + +[source,cpp] +---- +auto [ec] = co_await t.wait(); +if (ec == capy::error::canceled) + std::cout << "Timer was cancelled\n"; +---- + +== Type Aliases + +[source,cpp] +---- +using clock_type = std::chrono::steady_clock; +using time_point = clock_type::time_point; +using duration = clock_type::duration; +---- + +The timer uses `steady_clock` for monotonic timing unaffected by system +clock adjustments. + +== Resetting Timers + +Setting a new expiry cancels any pending wait: + +[source,cpp] +---- +t.expires_after(10s); +// Later, before 10s elapses: +t.expires_after(5s); // Resets to 5s, cancels previous wait +---- + +== Use Cases + +=== Simple Delay + +[source,cpp] +---- +capy::task delayed_action(corosio::io_context& ioc) +{ + corosio::timer t(ioc); + t.expires_after(2s); + co_await t.wait(); + + std::cout << "2 seconds have passed\n"; +} +---- + +=== Periodic Timer + +[source,cpp] +---- +capy::task periodic_task(corosio::io_context& ioc) +{ + corosio::timer t(ioc); + + for (int i = 0; i < 10; ++i) + { + t.expires_after(1s); + co_await t.wait(); + std::cout << "Tick " << i << "\n"; + } +} +---- + +=== Operation Timeout + +[source,cpp] +---- +capy::task with_timeout( + corosio::io_context& ioc, + corosio::tcp_socket& sock) +{ + corosio::timer timeout(ioc); + timeout.expires_after(30s); + + // Start both operations + auto read_task = sock.read_some(buffer); + auto timeout_task = timeout.wait(); + + // In practice, use parallel composition utilities + // This is simplified for illustration +} +---- + +NOTE: For proper timeout handling, use Capy's parallel composition utilities +like `when_any` or cancellation tokens. + +=== Rate Limiting + +[source,cpp] +---- +capy::task rate_limited_work(corosio::io_context& ioc) +{ + corosio::timer t(ioc); + auto next_time = std::chrono::steady_clock::now(); + + for (int i = 0; i < 100; ++i) + { + // Do work + process_item(i); + + // Wait until next interval + next_time += 100ms; + t.expires_at(next_time); + auto [ec] = co_await t.wait(); + if (ec) + break; + } +} +---- + +Using absolute time points prevents drift in periodic operations. + +== Stop Token Cancellation + +Timer waits support stop token cancellation through the affine protocol: + +[source,cpp] +---- +// Inside a cancellable task: +auto [ec] = co_await t.wait(); +// Completes with capy::error::canceled if stop requested +---- + +== Move Semantics + +Timers are move-only: + +[source,cpp] +---- +corosio::timer t1(ioc); +corosio::timer t2 = std::move(t1); // OK + +corosio::timer t3 = t2; // Error: deleted copy constructor +---- + +Move assignment cancels any pending wait on the destination timer. + +IMPORTANT: Source and destination must share the same execution context. + +== Thread Safety + +[cols="1,2"] +|=== +| Operation | Thread Safety + +| Distinct timers +| Safe from different threads + +| Same timer +| NOT safe for concurrent operations +|=== + +Don't call `wait()`, `expires_after()`, or `cancel()` concurrently on the +same timer. + +== Example: Heartbeat + +[source,cpp] +---- +capy::task heartbeat( + corosio::io_context& ioc, + corosio::tcp_socket& sock, + std::atomic& running) +{ + corosio::timer t(ioc); + + while (running) + { + t.expires_after(30s); + auto [ec] = co_await t.wait(); + + if (ec) + break; + + // Send heartbeat + std::string ping = "PING\r\n"; + auto [wec, n] = co_await corosio::write( + sock, capy::const_buffer(ping.data(), ping.size())); + + if (wec) + break; + } +} +---- + +== Next Steps + +* xref:4i.signals.adoc[Signal Handling] — Respond to OS signals +* xref:4c.io-context.adoc[I/O Context] — The event loop +* xref:4m.error-handling.adoc[Error Handling] — Cancellation patterns diff --git a/doc/modules/ROOT/pages/guide/signals.adoc b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc similarity index 97% rename from doc/modules/ROOT/pages/guide/signals.adoc rename to doc/modules/ROOT/pages/4.guide/4i.signals.adoc index 1365ed5c..eb43f2ba 100644 --- a/doc/modules/ROOT/pages/guide/signals.adoc +++ b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc @@ -425,6 +425,6 @@ The `restart` flag is particularly useful—without it, blocking calls like == Next Steps -* xref:timers.adoc[Timers] — Timed operations -* xref:io-context.adoc[I/O Context] — The event loop -* xref:../tutorials/echo-server.adoc[Echo Server Tutorial] — Server example +* xref:4h.timers.adoc[Timers] — Timed operations +* xref:4c.io-context.adoc[I/O Context] — The event loop +* xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Server example diff --git a/doc/modules/ROOT/pages/guide/resolver.adoc b/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc similarity index 93% rename from doc/modules/ROOT/pages/guide/resolver.adoc rename to doc/modules/ROOT/pages/4.guide/4j.resolver.adoc index daf9f7e4..40a212a2 100644 --- a/doc/modules/ROOT/pages/guide/resolver.adoc +++ b/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc @@ -1,364 +1,364 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= Name Resolution - -The `resolver` class performs asynchronous DNS lookups, converting hostnames -to IP addresses. It wraps the system's `getaddrinfo()` function with an -asynchronous interface. - -NOTE: Code snippets assume: -[source,cpp] ----- -#include -namespace corosio = boost::corosio; ----- - -== Overview - -[source,cpp] ----- -corosio::resolver r(ioc); -auto [ec, results] = co_await r.resolve("www.example.com", "https"); - -for (auto const& entry : results) -{ - auto ep = entry.get_endpoint(); - std::cout << ep.v4_address().to_string() << ":" << ep.port() << "\n"; -} ----- - -== Construction - -[source,cpp] ----- -corosio::io_context ioc; -corosio::resolver r(ioc); // From execution context ----- - -== Resolving Names - -=== Basic Resolution - -[source,cpp] ----- -auto [ec, results] = co_await r.resolve("www.example.com", "80"); ----- - -The host can be: - -* A hostname: `"www.example.com"` -* An IPv4 address string: `"192.168.1.1"` -* An IPv6 address string: `"::1"` or `"2001:db8::1"` - -The service can be: - -* A service name: `"http"`, `"https"`, `"ssh"` -* A port number string: `"80"`, `"443"`, `"22"` -* An empty string: `""` (returns port 0) - -=== With Flags - -[source,cpp] ----- -auto [ec, results] = co_await r.resolve( - "www.example.com", - "https", - corosio::resolve_flags::address_configured); ----- - -== Resolve Flags - -[cols="1,3"] -|=== -| Flag | Description - -| `none` -| No special behavior (default) - -| `passive` -| Return addresses suitable for `bind()` (server use) - -| `numeric_host` -| Host is a numeric address string, don't perform DNS lookup - -| `numeric_service` -| Service is a port number string, don't look up service name - -| `address_configured` -| Only return IPv4 if the system has IPv4 configured, same for IPv6 - -| `v4_mapped` -| If no IPv6 addresses found, return IPv4-mapped IPv6 addresses - -| `all_matching` -| With `v4_mapped`, return all matching IPv4 and IPv6 addresses -|=== - -Flags can be combined: - -[source,cpp] ----- -auto flags = - corosio::resolve_flags::numeric_host | - corosio::resolve_flags::numeric_service; - -auto [ec, results] = co_await r.resolve("127.0.0.1", "8080", flags); ----- - -== Working with Results - -The `resolver_results` class is a range of `resolver_entry` objects: - -[source,cpp] ----- -for (auto const& entry : results) -{ - corosio::endpoint ep = entry.get_endpoint(); - - if (ep.is_v4()) - std::cout << "IPv4: " << ep.v4_address().to_string(); - else - std::cout << "IPv6: " << ep.v6_address().to_string(); - - std::cout << ":" << ep.port() << "\n"; -} ----- - -=== resolver_results Interface - -[source,cpp] ----- -class resolver_results -{ -public: - using iterator = /* ... */; - using const_iterator = /* ... */; - - std::size_t size() const; - bool empty() const; - - const_iterator begin() const; - const_iterator end() const; -}; ----- - -=== resolver_entry Interface - -[source,cpp] ----- -class resolver_entry -{ -public: - corosio::endpoint get_endpoint() const; - - // Implicit conversion to endpoint - operator corosio::endpoint() const; - - // Query strings used in the resolution - std::string const& host_name() const; - std::string const& service_name() const; -}; ----- - -== Connecting to Resolved Addresses - -Try each address until one works: - -[source,cpp] ----- -capy::task connect_to_service( - corosio::io_context& ioc, - std::string_view host, - std::string_view service) -{ - corosio::resolver r(ioc); - auto [resolve_ec, results] = co_await r.resolve(host, service); - - if (resolve_ec) - throw boost::system::system_error(resolve_ec); - - if (results.empty()) - throw std::runtime_error("No addresses found"); - - corosio::tcp_socket sock(ioc); - sock.open(); - - boost::system::error_code last_error; - for (auto const& entry : results) - { - auto [ec] = co_await sock.connect(entry.get_endpoint()); - if (!ec) - co_return; // Connected successfully - - last_error = ec; - sock.close(); - sock.open(); - } - - throw boost::system::system_error(last_error); -} ----- - -== Cancellation - -=== cancel() - -Cancel pending resolution: - -[source,cpp] ----- -r.cancel(); ----- - -The resolution completes with `operation_canceled`. - -=== Stop Token Cancellation - -Resolver operations support stop token cancellation through the affine -protocol. - -== Error Handling - -Common resolution errors: - -[cols="1,2"] -|=== -| Error | Meaning - -| `host_not_found` -| Hostname doesn't exist - -| `no_data` -| Hostname exists but has no addresses - -| `service_not_found` -| Unknown service name - -| `operation_canceled` -| Resolution was cancelled -|=== - -== Move Semantics - -Resolvers are move-only: - -[source,cpp] ----- -corosio::resolver r1(ioc); -corosio::resolver r2 = std::move(r1); // OK - -corosio::resolver r3 = r2; // Error: deleted copy constructor ----- - -IMPORTANT: Source and destination must share the same execution context. - -== Thread Safety - -[cols="1,2"] -|=== -| Operation | Thread Safety - -| Distinct resolvers -| Safe from different threads - -| Same resolver -| NOT safe for concurrent operations -|=== - -=== Single-Inflight Constraint - -Each resolver can only have ONE resolve operation in progress at a time. -Starting a second resolve() while the first is still pending results in -undefined behavior. - -[source,cpp] ----- -// CORRECT: Sequential resolves on same resolver -auto [ec1, r1] = co_await resolver.resolve("host1", "80"); -auto [ec2, r2] = co_await resolver.resolve("host2", "80"); - -// CORRECT: Parallel resolves with separate resolver instances -corosio::resolver r1(ioc), r2(ioc); -// In separate coroutines: -auto [ec1, res1] = co_await r1.resolve("host1", "80"); -auto [ec2, res2] = co_await r2.resolve("host2", "80"); - -// WRONG: Concurrent resolves on same resolver - UNDEFINED BEHAVIOR -auto f1 = resolver.resolve("host1", "80"); -auto f2 = resolver.resolve("host2", "80"); // BAD: overlaps with f1 ----- - -If you need to resolve multiple hostnames concurrently, create a separate -resolver instance for each. - -== Example: HTTP Client with Resolution - -[source,cpp] ----- -capy::task http_get( - corosio::io_context& ioc, - std::string_view hostname) -{ - // Resolve hostname - corosio::resolver r(ioc); - auto [resolve_ec, results] = co_await r.resolve(hostname, "80"); - - if (resolve_ec) - { - std::cerr << "Resolution failed: " << resolve_ec.message() << "\n"; - co_return; - } - - // Connect to first address - corosio::tcp_socket sock(ioc); - sock.open(); - - for (auto const& entry : results) - { - auto [ec] = co_await sock.connect(entry); - if (!ec) - break; - } - - if (!sock.is_open()) - { - std::cerr << "Failed to connect\n"; - co_return; - } - - // Send HTTP request - std::string request = - "GET / HTTP/1.1\r\n" - "Host: " + std::string(hostname) + "\r\n" - "Connection: close\r\n" - "\r\n"; - - (co_await corosio::write( - sock, capy::const_buffer(request.data(), request.size()))).value(); - - // Read response - std::string response; - co_await corosio::read(sock, response); - - std::cout << response << "\n"; -} ----- - -== Platform Notes - -The resolver uses the system's `getaddrinfo()` function. On most platforms, -this is a blocking call executed on a thread pool to avoid blocking the -I/O context. - -== Next Steps - -* xref:endpoints.adoc[Endpoints] — Working with resolved addresses -* xref:sockets.adoc[Sockets] — Connecting to endpoints -* xref:../tutorials/dns-lookup.adoc[DNS Lookup Tutorial] — Complete example +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Name Resolution + +The `resolver` class performs asynchronous DNS lookups, converting hostnames +to IP addresses. It wraps the system's `getaddrinfo()` function with an +asynchronous interface. + +NOTE: Code snippets assume: +[source,cpp] +---- +#include +namespace corosio = boost::corosio; +---- + +== Overview + +[source,cpp] +---- +corosio::resolver r(ioc); +auto [ec, results] = co_await r.resolve("www.example.com", "https"); + +for (auto const& entry : results) +{ + auto ep = entry.get_endpoint(); + std::cout << ep.v4_address().to_string() << ":" << ep.port() << "\n"; +} +---- + +== Construction + +[source,cpp] +---- +corosio::io_context ioc; +corosio::resolver r(ioc); // From execution context +---- + +== Resolving Names + +=== Basic Resolution + +[source,cpp] +---- +auto [ec, results] = co_await r.resolve("www.example.com", "80"); +---- + +The host can be: + +* A hostname: `"www.example.com"` +* An IPv4 address string: `"192.168.1.1"` +* An IPv6 address string: `"::1"` or `"2001:db8::1"` + +The service can be: + +* A service name: `"http"`, `"https"`, `"ssh"` +* A port number string: `"80"`, `"443"`, `"22"` +* An empty string: `""` (returns port 0) + +=== With Flags + +[source,cpp] +---- +auto [ec, results] = co_await r.resolve( + "www.example.com", + "https", + corosio::resolve_flags::address_configured); +---- + +== Resolve Flags + +[cols="1,3"] +|=== +| Flag | Description + +| `none` +| No special behavior (default) + +| `passive` +| Return addresses suitable for `bind()` (server use) + +| `numeric_host` +| Host is a numeric address string, don't perform DNS lookup + +| `numeric_service` +| Service is a port number string, don't look up service name + +| `address_configured` +| Only return IPv4 if the system has IPv4 configured, same for IPv6 + +| `v4_mapped` +| If no IPv6 addresses found, return IPv4-mapped IPv6 addresses + +| `all_matching` +| With `v4_mapped`, return all matching IPv4 and IPv6 addresses +|=== + +Flags can be combined: + +[source,cpp] +---- +auto flags = + corosio::resolve_flags::numeric_host | + corosio::resolve_flags::numeric_service; + +auto [ec, results] = co_await r.resolve("127.0.0.1", "8080", flags); +---- + +== Working with Results + +The `resolver_results` class is a range of `resolver_entry` objects: + +[source,cpp] +---- +for (auto const& entry : results) +{ + corosio::endpoint ep = entry.get_endpoint(); + + if (ep.is_v4()) + std::cout << "IPv4: " << ep.v4_address().to_string(); + else + std::cout << "IPv6: " << ep.v6_address().to_string(); + + std::cout << ":" << ep.port() << "\n"; +} +---- + +=== resolver_results Interface + +[source,cpp] +---- +class resolver_results +{ +public: + using iterator = /* ... */; + using const_iterator = /* ... */; + + std::size_t size() const; + bool empty() const; + + const_iterator begin() const; + const_iterator end() const; +}; +---- + +=== resolver_entry Interface + +[source,cpp] +---- +class resolver_entry +{ +public: + corosio::endpoint get_endpoint() const; + + // Implicit conversion to endpoint + operator corosio::endpoint() const; + + // Query strings used in the resolution + std::string const& host_name() const; + std::string const& service_name() const; +}; +---- + +== Connecting to Resolved Addresses + +Try each address until one works: + +[source,cpp] +---- +capy::task connect_to_service( + corosio::io_context& ioc, + std::string_view host, + std::string_view service) +{ + corosio::resolver r(ioc); + auto [resolve_ec, results] = co_await r.resolve(host, service); + + if (resolve_ec) + throw boost::system::system_error(resolve_ec); + + if (results.empty()) + throw std::runtime_error("No addresses found"); + + corosio::tcp_socket sock(ioc); + sock.open(); + + boost::system::error_code last_error; + for (auto const& entry : results) + { + auto [ec] = co_await sock.connect(entry.get_endpoint()); + if (!ec) + co_return; // Connected successfully + + last_error = ec; + sock.close(); + sock.open(); + } + + throw boost::system::system_error(last_error); +} +---- + +== Cancellation + +=== cancel() + +Cancel pending resolution: + +[source,cpp] +---- +r.cancel(); +---- + +The resolution completes with `operation_canceled`. + +=== Stop Token Cancellation + +Resolver operations support stop token cancellation through the affine +protocol. + +== Error Handling + +Common resolution errors: + +[cols="1,2"] +|=== +| Error | Meaning + +| `host_not_found` +| Hostname doesn't exist + +| `no_data` +| Hostname exists but has no addresses + +| `service_not_found` +| Unknown service name + +| `operation_canceled` +| Resolution was cancelled +|=== + +== Move Semantics + +Resolvers are move-only: + +[source,cpp] +---- +corosio::resolver r1(ioc); +corosio::resolver r2 = std::move(r1); // OK + +corosio::resolver r3 = r2; // Error: deleted copy constructor +---- + +IMPORTANT: Source and destination must share the same execution context. + +== Thread Safety + +[cols="1,2"] +|=== +| Operation | Thread Safety + +| Distinct resolvers +| Safe from different threads + +| Same resolver +| NOT safe for concurrent operations +|=== + +=== Single-Inflight Constraint + +Each resolver can only have ONE resolve operation in progress at a time. +Starting a second resolve() while the first is still pending results in +undefined behavior. + +[source,cpp] +---- +// CORRECT: Sequential resolves on same resolver +auto [ec1, r1] = co_await resolver.resolve("host1", "80"); +auto [ec2, r2] = co_await resolver.resolve("host2", "80"); + +// CORRECT: Parallel resolves with separate resolver instances +corosio::resolver r1(ioc), r2(ioc); +// In separate coroutines: +auto [ec1, res1] = co_await r1.resolve("host1", "80"); +auto [ec2, res2] = co_await r2.resolve("host2", "80"); + +// WRONG: Concurrent resolves on same resolver - UNDEFINED BEHAVIOR +auto f1 = resolver.resolve("host1", "80"); +auto f2 = resolver.resolve("host2", "80"); // BAD: overlaps with f1 +---- + +If you need to resolve multiple hostnames concurrently, create a separate +resolver instance for each. + +== Example: HTTP Client with Resolution + +[source,cpp] +---- +capy::task http_get( + corosio::io_context& ioc, + std::string_view hostname) +{ + // Resolve hostname + corosio::resolver r(ioc); + auto [resolve_ec, results] = co_await r.resolve(hostname, "80"); + + if (resolve_ec) + { + std::cerr << "Resolution failed: " << resolve_ec.message() << "\n"; + co_return; + } + + // Connect to first address + corosio::tcp_socket sock(ioc); + sock.open(); + + for (auto const& entry : results) + { + auto [ec] = co_await sock.connect(entry); + if (!ec) + break; + } + + if (!sock.is_open()) + { + std::cerr << "Failed to connect\n"; + co_return; + } + + // Send HTTP request + std::string request = + "GET / HTTP/1.1\r\n" + "Host: " + std::string(hostname) + "\r\n" + "Connection: close\r\n" + "\r\n"; + + (co_await corosio::write( + sock, capy::const_buffer(request.data(), request.size()))).value(); + + // Read response + std::string response; + co_await corosio::read(sock, response); + + std::cout << response << "\n"; +} +---- + +== Platform Notes + +The resolver uses the system's `getaddrinfo()` function. On most platforms, +this is a blocking call executed on a thread pool to avoid blocking the +I/O context. + +== Next Steps + +* xref:4f.endpoints.adoc[Endpoints] — Working with resolved addresses +* xref:4d.sockets.adoc[Sockets] — Connecting to endpoints +* xref:../3.tutorials/3c.dns-lookup.adoc[DNS Lookup Tutorial] — Complete example diff --git a/doc/modules/ROOT/pages/guide/tcp-server.adoc b/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc similarity index 93% rename from doc/modules/ROOT/pages/guide/tcp-server.adoc rename to doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc index 2beb642b..ed0dc780 100644 --- a/doc/modules/ROOT/pages/guide/tcp-server.adoc +++ b/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc @@ -1,391 +1,391 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= TCP Server - -The `tcp_server` class provides a framework for building TCP servers with -connection pooling. It manages acceptors, worker pools, and connection -lifecycle automatically. - -NOTE: Code snippets assume: -[source,cpp] ----- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; ----- - -== Overview - -`tcp_server` is a base class designed for inheritance. You derive from it, -define your worker type, and implement the connection handling logic. The -framework handles: - -* Listening on multiple ports -* Accepting connections -* Worker pool management -* Coroutine lifecycle - -[source,cpp] ----- -class echo_server : public corosio::tcp_server -{ - struct worker : worker_base - { - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string buf; - - explicit worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - buf.reserve(4096); - } - - corosio::tcp_socket& socket() override { return sock_; } - - void run(launcher launch) override - { - launch(ctx_.get_executor(), do_echo()); - } - - capy::task do_echo(); - }; - -public: - echo_server(corosio::io_context& ioc) - : tcp_server(ioc, ioc.get_executor()) - { - wv_.reserve(100); - for (int i = 0; i < 100; ++i) - wv_.emplace(ioc); - } -}; ----- - -== The Worker Pattern - -Workers are preallocated objects that handle connections. Each worker contains -a socket and any state needed for a session. - -=== worker_base - -The `worker_base` class is the foundation: - -[source,cpp] ----- -class worker_base -{ -public: - virtual ~worker_base() = default; - virtual void run(launcher launch) = 0; - virtual corosio::tcp_socket& socket() = 0; -}; ----- - -Your worker inherits from `worker_base`, owns its socket, and implements the -required methods: - -[source,cpp] ----- -struct my_worker : tcp_server::worker_base -{ - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string request_buf; - std::string response_buf; - - explicit my_worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - {} - - corosio::tcp_socket& socket() override { return sock_; } - - void run(launcher launch) override - { - launch(ctx_.get_executor(), handle_connection()); - } - - capy::task handle_connection() - { - // Handle the connection using sock_ - // Worker is automatically returned to pool when coroutine ends - } -}; ----- - -=== The workers Container - -The `workers` class manages the worker pool: - -[source,cpp] ----- -class workers -{ -public: - template - T& emplace(Args&&... args); - - void reserve(std::size_t n); - std::size_t size() const noexcept; -}; ----- - -Use `emplace()` to add workers during construction: - -[source,cpp] ----- -my_server(corosio::io_context& ioc) - : tcp_server(ioc, ioc.get_executor()) -{ - wv_.reserve(max_workers); - for (int i = 0; i < max_workers; ++i) - wv_.emplace(ioc); -} ----- - -Workers are stored polymorphically, allowing different worker types if needed. - -== The Launcher - -When a connection is accepted, `tcp_server` calls your worker's `run()` -method with a `launcher` object. The launcher manages the coroutine lifecycle: - -[source,cpp] ----- -void run(launcher launch) override -{ - // Create and launch the session coroutine - launch(executor, my_coroutine()); -} ----- - -The launcher: - -1. Starts your coroutine on the specified executor -2. Tracks the worker as in-use -3. Returns the worker to the pool when the coroutine completes - -You must call the launcher exactly once. Failure to call it returns the -worker immediately. Calling it multiple times throws `std::logic_error`. - -=== Launcher Signature - -[source,cpp] ----- -template -void operator()(Executor const& ex, capy::task task); ----- - -The executor determines where the coroutine runs. Typically you use the -I/O context's executor: - -[source,cpp] ----- -launch(ctx_.get_executor(), handle_connection()); ----- - -== Binding and Starting - -=== bind() - -Bind to a local endpoint: - -[source,cpp] ----- -auto ec = server.bind(corosio::endpoint(8080)); -if (ec) - std::cerr << "Bind failed: " << ec.message() << "\n"; ----- - -You can bind to multiple ports: - -[source,cpp] ----- -server.bind(corosio::endpoint(80)); -server.bind(corosio::endpoint(443)); ----- - -=== start() - -Begin accepting connections: - -[source,cpp] ----- -server.start(); ----- - -After `start()`, the server: - -1. Listens on all bound ports -2. Accepts incoming connections -3. Assigns connections to available workers -4. Calls each worker's `run()` method - -The accept loop runs until the `io_context` stops. - -== Complete Example - -[source,cpp] ----- -#include -#include -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; - -class echo_server : public corosio::tcp_server -{ - struct worker : worker_base - { - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string buf; - - explicit worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - buf.reserve(4096); - } - - corosio::tcp_socket& socket() override { return sock_; } - - void run(launcher launch) override - { - launch(ctx_.get_executor(), do_session()); - } - - capy::task do_session() - { - for (;;) - { - buf.resize(4096); - auto [ec, n] = co_await sock_.read_some( - capy::mutable_buffer(buf.data(), buf.size())); - - if (ec || n == 0) - break; - - buf.resize(n); - auto [wec, wn] = co_await corosio::write( - sock_, capy::const_buffer(buf.data(), buf.size())); - - if (wec) - break; - } - - sock_.close(); - } - }; - -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(ctx); - } -}; - -int main() -{ - corosio::io_context ioc; - - echo_server server(ioc, 100); - - auto ec = server.bind(corosio::endpoint(8080)); - if (ec) - { - std::cerr << "Bind failed: " << ec.message() << "\n"; - return 1; - } - - std::cout << "Echo server listening on port 8080\n"; - - server.start(); - ioc.run(); -} ----- - -== Design Considerations - -=== Why a Worker Pool? - -A worker pool provides: - -* **Bounded resources**: Fixed maximum connections -* **No per-connection allocation**: Sockets and buffers preallocated -* **Simple lifecycle**: Workers cycle between idle and active states - -=== Worker Reuse - -When a session coroutine completes, its worker automatically returns to the -idle pool. The next accepted connection receives this worker. Ensure your -worker's state is properly reset between connections: - -[source,cpp] ----- -capy::task do_session() -{ - // Reset state at session start - request_.clear(); - response_.clear(); - - // ... handle connection ... - - // Socket closed, worker returns to pool -} ----- - -=== Multiple Ports - -`tcp_server` can listen on multiple ports simultaneously. All ports share -the same worker pool: - -[source,cpp] ----- -server.bind(corosio::endpoint(80)); // HTTP -server.bind(corosio::endpoint(443)); // HTTPS -server.start(); ----- - -=== Connection Rejection - -When all workers are busy, the server cannot accept new connections until -a worker becomes available. The TCP listen backlog holds pending connections -during this time. - -For high-traffic scenarios, size your worker pool appropriately or implement -connection limits at a higher layer. - -== Thread Safety - -The `tcp_server` class is not thread-safe. All operations on the server -must occur from coroutines running on its `io_context`. Workers may not be -accessed concurrently. - -For multi-threaded operation, create one server per thread, or use external -synchronization. - -== Next Steps - -* xref:sockets.adoc[Sockets] — Socket operations -* xref:concurrent-programming.adoc[Concurrent Programming] — Coroutine patterns -* xref:../tutorials/echo-server.adoc[Echo Server Tutorial] — Simpler approach +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += TCP Server + +The `tcp_server` class provides a framework for building TCP servers with +connection pooling. It manages acceptors, worker pools, and connection +lifecycle automatically. + +NOTE: Code snippets assume: +[source,cpp] +---- +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +---- + +== Overview + +`tcp_server` is a base class designed for inheritance. You derive from it, +define your worker type, and implement the connection handling logic. The +framework handles: + +* Listening on multiple ports +* Accepting connections +* Worker pool management +* Coroutine lifecycle + +[source,cpp] +---- +class echo_server : public corosio::tcp_server +{ + struct worker : worker_base + { + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string buf; + + explicit worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) + { + buf.reserve(4096); + } + + corosio::tcp_socket& socket() override { return sock_; } + + void run(launcher launch) override + { + launch(ctx_.get_executor(), do_echo()); + } + + capy::task do_echo(); + }; + +public: + echo_server(corosio::io_context& ioc) + : tcp_server(ioc, ioc.get_executor()) + { + wv_.reserve(100); + for (int i = 0; i < 100; ++i) + wv_.emplace(ioc); + } +}; +---- + +== The Worker Pattern + +Workers are preallocated objects that handle connections. Each worker contains +a socket and any state needed for a session. + +=== worker_base + +The `worker_base` class is the foundation: + +[source,cpp] +---- +class worker_base +{ +public: + virtual ~worker_base() = default; + virtual void run(launcher launch) = 0; + virtual corosio::tcp_socket& socket() = 0; +}; +---- + +Your worker inherits from `worker_base`, owns its socket, and implements the +required methods: + +[source,cpp] +---- +struct my_worker : tcp_server::worker_base +{ + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string request_buf; + std::string response_buf; + + explicit my_worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) + {} + + corosio::tcp_socket& socket() override { return sock_; } + + void run(launcher launch) override + { + launch(ctx_.get_executor(), handle_connection()); + } + + capy::task handle_connection() + { + // Handle the connection using sock_ + // Worker is automatically returned to pool when coroutine ends + } +}; +---- + +=== The workers Container + +The `workers` class manages the worker pool: + +[source,cpp] +---- +class workers +{ +public: + template + T& emplace(Args&&... args); + + void reserve(std::size_t n); + std::size_t size() const noexcept; +}; +---- + +Use `emplace()` to add workers during construction: + +[source,cpp] +---- +my_server(corosio::io_context& ioc) + : tcp_server(ioc, ioc.get_executor()) +{ + wv_.reserve(max_workers); + for (int i = 0; i < max_workers; ++i) + wv_.emplace(ioc); +} +---- + +Workers are stored polymorphically, allowing different worker types if needed. + +== The Launcher + +When a connection is accepted, `tcp_server` calls your worker's `run()` +method with a `launcher` object. The launcher manages the coroutine lifecycle: + +[source,cpp] +---- +void run(launcher launch) override +{ + // Create and launch the session coroutine + launch(executor, my_coroutine()); +} +---- + +The launcher: + +1. Starts your coroutine on the specified executor +2. Tracks the worker as in-use +3. Returns the worker to the pool when the coroutine completes + +You must call the launcher exactly once. Failure to call it returns the +worker immediately. Calling it multiple times throws `std::logic_error`. + +=== Launcher Signature + +[source,cpp] +---- +template +void operator()(Executor const& ex, capy::task task); +---- + +The executor determines where the coroutine runs. Typically you use the +I/O context's executor: + +[source,cpp] +---- +launch(ctx_.get_executor(), handle_connection()); +---- + +== Binding and Starting + +=== bind() + +Bind to a local endpoint: + +[source,cpp] +---- +auto ec = server.bind(corosio::endpoint(8080)); +if (ec) + std::cerr << "Bind failed: " << ec.message() << "\n"; +---- + +You can bind to multiple ports: + +[source,cpp] +---- +server.bind(corosio::endpoint(80)); +server.bind(corosio::endpoint(443)); +---- + +=== start() + +Begin accepting connections: + +[source,cpp] +---- +server.start(); +---- + +After `start()`, the server: + +1. Listens on all bound ports +2. Accepts incoming connections +3. Assigns connections to available workers +4. Calls each worker's `run()` method + +The accept loop runs until the `io_context` stops. + +== Complete Example + +[source,cpp] +---- +#include +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +class echo_server : public corosio::tcp_server +{ + struct worker : worker_base + { + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string buf; + + explicit worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) + { + buf.reserve(4096); + } + + corosio::tcp_socket& socket() override { return sock_; } + + void run(launcher launch) override + { + launch(ctx_.get_executor(), do_session()); + } + + capy::task do_session() + { + for (;;) + { + buf.resize(4096); + auto [ec, n] = co_await sock_.read_some( + capy::mutable_buffer(buf.data(), buf.size())); + + if (ec || n == 0) + break; + + buf.resize(n); + auto [wec, wn] = co_await corosio::write( + sock_, capy::const_buffer(buf.data(), buf.size())); + + if (wec) + break; + } + + sock_.close(); + } + }; + +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(ctx); + } +}; + +int main() +{ + corosio::io_context ioc; + + echo_server server(ioc, 100); + + auto ec = server.bind(corosio::endpoint(8080)); + if (ec) + { + std::cerr << "Bind failed: " << ec.message() << "\n"; + return 1; + } + + std::cout << "Echo server listening on port 8080\n"; + + server.start(); + ioc.run(); +} +---- + +== Design Considerations + +=== Why a Worker Pool? + +A worker pool provides: + +* **Bounded resources**: Fixed maximum connections +* **No per-connection allocation**: Sockets and buffers preallocated +* **Simple lifecycle**: Workers cycle between idle and active states + +=== Worker Reuse + +When a session coroutine completes, its worker automatically returns to the +idle pool. The next accepted connection receives this worker. Ensure your +worker's state is properly reset between connections: + +[source,cpp] +---- +capy::task do_session() +{ + // Reset state at session start + request_.clear(); + response_.clear(); + + // ... handle connection ... + + // Socket closed, worker returns to pool +} +---- + +=== Multiple Ports + +`tcp_server` can listen on multiple ports simultaneously. All ports share +the same worker pool: + +[source,cpp] +---- +server.bind(corosio::endpoint(80)); // HTTP +server.bind(corosio::endpoint(443)); // HTTPS +server.start(); +---- + +=== Connection Rejection + +When all workers are busy, the server cannot accept new connections until +a worker becomes available. The TCP listen backlog holds pending connections +during this time. + +For high-traffic scenarios, size your worker pool appropriately or implement +connection limits at a higher layer. + +== Thread Safety + +The `tcp_server` class is not thread-safe. All operations on the server +must occur from coroutines running on its `io_context`. Workers may not be +accessed concurrently. + +For multi-threaded operation, create one server per thread, or use external +synchronization. + +== Next Steps + +* xref:4d.sockets.adoc[Sockets] — Socket operations +* xref:4b.concurrent-programming.adoc[Concurrent Programming] — Coroutine patterns +* xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Simpler approach diff --git a/doc/modules/ROOT/pages/guide/tls.adoc b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc similarity index 98% rename from doc/modules/ROOT/pages/guide/tls.adoc rename to doc/modules/ROOT/pages/4.guide/4l.tls.adoc index c5269897..8659ca8c 100644 --- a/doc/modules/ROOT/pages/guide/tls.adoc +++ b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc @@ -660,6 +660,6 @@ target_link_libraries(my_target PRIVATE OpenSSL::SSL OpenSSL::Crypto) == Next Steps -* xref:sockets.adoc[Sockets] — The underlying stream -* xref:composed-operations.adoc[Composed Operations] — read() and write() -* xref:../tutorials/tls-context.adoc[TLS Context Tutorial] — Step-by-step configuration +* xref:4d.sockets.adoc[Sockets] — The underlying stream +* xref:4g.composed-operations.adoc[Composed Operations] — read() and write() +* xref:../3.tutorials/3d.tls-context.adoc[TLS Context Tutorial] — Step-by-step configuration diff --git a/doc/modules/ROOT/pages/guide/error-handling.adoc b/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc similarity index 92% rename from doc/modules/ROOT/pages/guide/error-handling.adoc rename to doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc index f2b840d0..2d637619 100644 --- a/doc/modules/ROOT/pages/guide/error-handling.adoc +++ b/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc @@ -1,323 +1,322 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= Error Handling - -Corosio provides flexible error handling through the `io_result` type, which -supports both error-code and exception-based patterns. - -NOTE: Code snippets assume: -[source,cpp] ----- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; ----- - -== The io_result Type - -Every I/O operation returns an `io_result` that contains: - -* An error code (always present) -* Additional values depending on the operation - -[source,cpp] ----- -// Void result (connect, handshake) -io_result<> // Contains: ec - -// Single value (read_some, write_some) -io_result // Contains: ec, n (bytes transferred) - -// Typed result (resolve) -io_result // Contains: ec, results ----- - -== Structured Bindings Pattern - -Use structured bindings to extract results: - -[source,cpp] ----- -// Void result -auto [ec] = co_await sock.connect(endpoint); -if (ec) - std::cerr << "Connect failed: " << ec.message() << "\n"; - -// Value result -auto [ec, n] = co_await sock.read_some(buffer); -if (ec) - std::cerr << "Read failed: " << ec.message() << "\n"; -else - std::cout << "Read " << n << " bytes\n"; ----- - -This pattern gives you full control over error handling. - -== Exception Pattern - -Call `.value()` to throw on error: - -[source,cpp] ----- -// Throws system_error if connect fails -(co_await sock.connect(endpoint)).value(); - -// Returns bytes transferred, throws on error -auto n = (co_await sock.read_some(buffer)).value(); ----- - -The `.value()` method: - -* Returns the value(s) if no error -* Throws `boost::system::system_error` if `ec` is set - -== Boolean Conversion - -`io_result` is contextually convertible to `bool`: - -[source,cpp] ----- -auto result = co_await sock.connect(endpoint); -if (result) - std::cout << "Connected successfully\n"; -else - std::cerr << "Failed: " << result.ec.message() << "\n"; ----- - -Returns `true` if the operation succeeded (no error). - -== Choosing a Pattern - -=== Use Structured Bindings When: - -* Errors are expected and need handling (EOF, timeout) -* You want to log errors without throwing -* Performance is critical (no exception overhead) -* You need partial success information (bytes transferred) - -[source,cpp] ----- -auto [ec, n] = co_await sock.read_some(buf); -if (ec == capy::error::eof) -{ - std::cout << "End of stream after " << n << " bytes\n"; - // Not an exceptional condition -} ----- - -=== Use Exceptions When: - -* Errors are truly exceptional -* You want concise, linear code -* Errors should propagate to a central handler -* You don't need partial success information - -[source,cpp] ----- -(co_await sock.connect(endpoint)).value(); -(co_await corosio::write(sock, request)).value(); -auto response = (co_await corosio::read(sock, buffer)).value(); -// Any error throws immediately ----- - -== Common Error Codes - -=== I/O Errors - -[cols="1,2"] -|=== -| Error | Meaning - -| `capy::error::eof` -| End of stream reached - -| `connection_refused` -| No server at endpoint - -| `connection_reset` -| Peer reset connection - -| `broken_pipe` -| Write to closed connection - -| `timed_out` -| Operation timed out - -| `network_unreachable` -| No route to host -|=== - -=== Cancellation - -[cols="1,2"] -|=== -| Error | Meaning - -| `capy::error::canceled` -| Cancelled via `cancel()` method - -| `operation_canceled` -| Cancelled via stop token -|=== - -Check cancellation portably: - -[source,cpp] ----- -if (ec == capy::cond::canceled) - std::cout << "Operation was cancelled\n"; ----- - -== EOF Handling - -End-of-stream is signaled by `capy::error::eof`: - -[source,cpp] ----- -auto [ec, n] = co_await corosio::read(stream, buffer); -if (ec == capy::error::eof) -{ - std::cout << "Stream ended, read " << n << " bytes total\n"; - // This is often expected, not an error -} -else if (ec) -{ - std::cerr << "Unexpected error: " << ec.message() << "\n"; -} ----- - -When using `.value()` on read operations, EOF throws an exception. Filter -it if expected: - -[source,cpp] ----- -auto [ec, n] = co_await corosio::read(stream, response); -if (ec && ec != capy::error::eof) - throw boost::system::system_error(ec); -// EOF is expected when server closes connection ----- - -== Partial Success - -Some operations may partially succeed before an error: - -[source,cpp] ----- -auto [ec, n] = co_await corosio::write(stream, large_buffer); -if (ec) -{ - std::cerr << "Error after writing " << n << " of " - << buffer_size(large_buffer) << " bytes\n"; - // Can potentially resume from here -} ----- - -The composed operations (`read()`, `write()`) return the total bytes -transferred even when returning an error. - -== Error Categories - -Corosio uses Boost.System error codes, which support categories: - -[source,cpp] ----- -if (ec.category() == boost::system::system_category()) - // Operating system error - -if (ec.category() == boost::system::generic_category()) - // Portable POSIX-style error - -if (ec.category() == capy::error_category()) - // Capy-specific error (eof, canceled, etc.) ----- - -== Comparing Errors - -Use error conditions for portable comparison: - -[source,cpp] ----- -// Specific error (platform-dependent) -if (ec == make_error_code(system::errc::connection_refused)) - // ... - -// Error condition (portable) -if (ec == capy::cond::canceled) - // Matches any cancellation error - -if (ec == capy::cond::eof) - // Matches end-of-stream ----- - -== Exception Safety in Coroutines - -When using exceptions in coroutines, caught exceptions don't leak: - -[source,cpp] ----- -capy::task safe_operation() -{ - try - { - (co_await sock.connect(endpoint)).value(); - } - catch (boost::system::system_error const& e) - { - std::cerr << "Connect failed: " << e.what() << "\n"; - // Exception handled here, doesn't propagate - } -} ----- - -Uncaught exceptions in a task are stored and rethrown when the task is -awaited. - -== Example: Robust Connection - -[source,cpp] ----- -capy::task connect_with_retry( - corosio::io_context& ioc, - corosio::endpoint ep, - int max_retries) -{ - corosio::tcp_socket sock(ioc); - corosio::timer delay(ioc); - - for (int attempt = 0; attempt < max_retries; ++attempt) - { - sock.open(); - auto [ec] = co_await sock.connect(ep); - - if (!ec) - co_return; // Success - - std::cerr << "Attempt " << (attempt + 1) - << " failed: " << ec.message() << "\n"; - - sock.close(); - - // Wait before retry (exponential backoff) - delay.expires_after(std::chrono::seconds(1 << attempt)); - co_await delay.wait(); - } - - throw std::runtime_error("Failed to connect after retries"); -} ----- - -== Next Steps - -* xref:sockets.adoc[Sockets] — Socket operations -* xref:composed-operations.adoc[Composed Operations] — read() and write() -* xref:../concepts/affine-awaitables.adoc[Affine Awaitables] — Cancellation support +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Error Handling + +Corosio provides flexible error handling through the `io_result` type, which +supports both error-code and exception-based patterns. + +NOTE: Code snippets assume: +[source,cpp] +---- +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +---- + +== The io_result Type + +Every I/O operation returns an `io_result` that contains: + +* An error code (always present) +* Additional values depending on the operation + +[source,cpp] +---- +// Void result (connect, handshake) +io_result<> // Contains: ec + +// Single value (read_some, write_some) +io_result // Contains: ec, n (bytes transferred) + +// Typed result (resolve) +io_result // Contains: ec, results +---- + +== Structured Bindings Pattern + +Use structured bindings to extract results: + +[source,cpp] +---- +// Void result +auto [ec] = co_await sock.connect(endpoint); +if (ec) + std::cerr << "Connect failed: " << ec.message() << "\n"; + +// Value result +auto [ec, n] = co_await sock.read_some(buffer); +if (ec) + std::cerr << "Read failed: " << ec.message() << "\n"; +else + std::cout << "Read " << n << " bytes\n"; +---- + +This pattern gives you full control over error handling. + +== Exception Pattern + +Call `.value()` to throw on error: + +[source,cpp] +---- +// Throws system_error if connect fails +(co_await sock.connect(endpoint)).value(); + +// Returns bytes transferred, throws on error +auto n = (co_await sock.read_some(buffer)).value(); +---- + +The `.value()` method: + +* Returns the value(s) if no error +* Throws `boost::system::system_error` if `ec` is set + +== Boolean Conversion + +`io_result` is contextually convertible to `bool`: + +[source,cpp] +---- +auto result = co_await sock.connect(endpoint); +if (result) + std::cout << "Connected successfully\n"; +else + std::cerr << "Failed: " << result.ec.message() << "\n"; +---- + +Returns `true` if the operation succeeded (no error). + +== Choosing a Pattern + +=== Use Structured Bindings When: + +* Errors are expected and need handling (EOF, timeout) +* You want to log errors without throwing +* Performance is critical (no exception overhead) +* You need partial success information (bytes transferred) + +[source,cpp] +---- +auto [ec, n] = co_await sock.read_some(buf); +if (ec == capy::error::eof) +{ + std::cout << "End of stream after " << n << " bytes\n"; + // Not an exceptional condition +} +---- + +=== Use Exceptions When: + +* Errors are truly exceptional +* You want concise, linear code +* Errors should propagate to a central handler +* You don't need partial success information + +[source,cpp] +---- +(co_await sock.connect(endpoint)).value(); +(co_await corosio::write(sock, request)).value(); +auto response = (co_await corosio::read(sock, buffer)).value(); +// Any error throws immediately +---- + +== Common Error Codes + +=== I/O Errors + +[cols="1,2"] +|=== +| Error | Meaning + +| `capy::error::eof` +| End of stream reached + +| `connection_refused` +| No server at endpoint + +| `connection_reset` +| Peer reset connection + +| `broken_pipe` +| Write to closed connection + +| `timed_out` +| Operation timed out + +| `network_unreachable` +| No route to host +|=== + +=== Cancellation + +[cols="1,2"] +|=== +| Error | Meaning + +| `capy::error::canceled` +| Cancelled via `cancel()` method + +| `operation_canceled` +| Cancelled via stop token +|=== + +Check cancellation portably: + +[source,cpp] +---- +if (ec == capy::cond::canceled) + std::cout << "Operation was cancelled\n"; +---- + +== EOF Handling + +End-of-stream is signaled by `capy::error::eof`: + +[source,cpp] +---- +auto [ec, n] = co_await corosio::read(stream, buffer); +if (ec == capy::error::eof) +{ + std::cout << "Stream ended, read " << n << " bytes total\n"; + // This is often expected, not an error +} +else if (ec) +{ + std::cerr << "Unexpected error: " << ec.message() << "\n"; +} +---- + +When using `.value()` on read operations, EOF throws an exception. Filter +it if expected: + +[source,cpp] +---- +auto [ec, n] = co_await corosio::read(stream, response); +if (ec && ec != capy::error::eof) + throw boost::system::system_error(ec); +// EOF is expected when server closes connection +---- + +== Partial Success + +Some operations may partially succeed before an error: + +[source,cpp] +---- +auto [ec, n] = co_await corosio::write(stream, large_buffer); +if (ec) +{ + std::cerr << "Error after writing " << n << " of " + << buffer_size(large_buffer) << " bytes\n"; + // Can potentially resume from here +} +---- + +The composed operations (`read()`, `write()`) return the total bytes +transferred even when returning an error. + +== Error Categories + +Corosio uses Boost.System error codes, which support categories: + +[source,cpp] +---- +if (ec.category() == boost::system::system_category()) + // Operating system error + +if (ec.category() == boost::system::generic_category()) + // Portable POSIX-style error + +if (ec.category() == capy::error_category()) + // Capy-specific error (eof, canceled, etc.) +---- + +== Comparing Errors + +Use error conditions for portable comparison: + +[source,cpp] +---- +// Specific error (platform-dependent) +if (ec == make_error_code(system::errc::connection_refused)) + // ... + +// Error condition (portable) +if (ec == capy::cond::canceled) + // Matches any cancellation error + +if (ec == capy::cond::eof) + // Matches end-of-stream +---- + +== Exception Safety in Coroutines + +When using exceptions in coroutines, caught exceptions don't leak: + +[source,cpp] +---- +capy::task safe_operation() +{ + try + { + (co_await sock.connect(endpoint)).value(); + } + catch (boost::system::system_error const& e) + { + std::cerr << "Connect failed: " << e.what() << "\n"; + // Exception handled here, doesn't propagate + } +} +---- + +Uncaught exceptions in a task are stored and rethrown when the task is +awaited. + +== Example: Robust Connection + +[source,cpp] +---- +capy::task connect_with_retry( + corosio::io_context& ioc, + corosio::endpoint ep, + int max_retries) +{ + corosio::tcp_socket sock(ioc); + corosio::timer delay(ioc); + + for (int attempt = 0; attempt < max_retries; ++attempt) + { + sock.open(); + auto [ec] = co_await sock.connect(ep); + + if (!ec) + co_return; // Success + + std::cerr << "Attempt " << (attempt + 1) + << " failed: " << ec.message() << "\n"; + + sock.close(); + + // Wait before retry (exponential backoff) + delay.expires_after(std::chrono::seconds(1 << attempt)); + co_await delay.wait(); + } + + throw std::runtime_error("Failed to connect after retries"); +} +---- + +== Next Steps + +* xref:4d.sockets.adoc[Sockets] — Socket operations +* xref:4g.composed-operations.adoc[Composed Operations] — read() and write() diff --git a/doc/modules/ROOT/pages/guide/buffers.adoc b/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc similarity index 96% rename from doc/modules/ROOT/pages/guide/buffers.adoc rename to doc/modules/ROOT/pages/4.guide/4n.buffers.adoc index 0eecbe96..24503138 100644 --- a/doc/modules/ROOT/pages/guide/buffers.adoc +++ b/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc @@ -293,6 +293,6 @@ capy::task read_header(corosio::io_stream& stream) == Next Steps -* xref:composed-operations.adoc[Composed Operations] — Using buffers with read/write -* xref:sockets.adoc[Sockets] — Socket I/O operations -* xref:../tutorials/echo-server.adoc[Echo Server Tutorial] — Practical usage +* xref:4g.composed-operations.adoc[Composed Operations] — Using buffers with read/write +* xref:4d.sockets.adoc[Sockets] — Socket I/O operations +* xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Practical usage diff --git a/doc/modules/ROOT/pages/5.testing/5.intro.adoc b/doc/modules/ROOT/pages/5.testing/5.intro.adoc new file mode 100644 index 00000000..1f8e2b0a --- /dev/null +++ b/doc/modules/ROOT/pages/5.testing/5.intro.adoc @@ -0,0 +1,12 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Testing + +Asynchronous I/O code is notoriously difficult to test. Real network operations introduce latency, non-determinism, and dependencies on external services—all of which make tests slow and fragile. Corosio provides test utilities that replace live networking with controllable, deterministic substitutes. You can stage data for reads, verify what your code writes, and inject errors at precise points—all without opening a single network connection. This section covers the tools and patterns that make thorough testing of I/O code practical and repeatable. diff --git a/doc/modules/ROOT/pages/testing/mocket.adoc b/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc similarity index 93% rename from doc/modules/ROOT/pages/testing/mocket.adoc rename to doc/modules/ROOT/pages/5.testing/5a.mocket.adoc index 37b8d6fb..cca81b28 100644 --- a/doc/modules/ROOT/pages/testing/mocket.adoc +++ b/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc @@ -1,259 +1,259 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= Mock Sockets - -The `mocket` class provides mock sockets for testing I/O code without -actual network operations. Mockets let you stage data for reading and -verify expected writes. - -NOTE: Code snippets assume: -[source,cpp] ----- -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; ----- - -== Overview - -Mockets are testable socket-like objects: - -[source,cpp] ----- -// Create connected pair -capy::test::fuse f; -auto [client, server] = corosio::test::make_mockets(ioc, f); - -// Stage data on server for client to read -server.provide("Hello from server"); - -// Stage expected data that client should write -client.expect("Hello from client"); - -// Now run your code that uses client/server as io_stream& ----- - -== Creating Mockets - -Mockets are created in connected pairs: - -[source,cpp] ----- -corosio::io_context ioc; -capy::test::fuse f; - -auto [m1, m2] = corosio::test::make_mockets(ioc, f); ----- - -The pair is connected via loopback TCP sockets. Data written to one can -be read from the other, plus you can use the staging/expectation API. - -== Staging Data for Reads - -Use `provide()` to stage data that the _peer_ will read: - -[source,cpp] ----- -// On server: stage data for client to read -server.provide("HTTP/1.1 200 OK\r\n\r\nHello"); - -// Now when client reads, it gets this data -auto [ec, n] = co_await client.read_some(buffer); -// buffer contains "HTTP/1.1 200 OK\r\n\r\nHello" ----- - -Multiple `provide()` calls append data: - -[source,cpp] ----- -server.provide("Part 1"); -server.provide("Part 2"); -// Client sees "Part 1Part 2" ----- - -== Setting Write Expectations - -Use `expect()` to verify what the caller writes: - -[source,cpp] ----- -// Client should send this exact data -client.expect("GET / HTTP/1.1\r\n\r\n"); - -// Now client writes -co_await corosio::write(client, request_buffer); - -// If written data doesn't match, fuse fails ----- - -=== How Matching Works - -When you write to a mocket with expectations: - -1. Written data is compared against the expect buffer -2. If it matches, the expect buffer is consumed -3. If it doesn't match, `fuse.fail()` is called - -After the expect buffer is exhausted, writes pass through to the real socket. - -== Closing and Verification - -Use `close()` to verify all expectations were met: - -[source,cpp] ----- -auto ec = client.close(); -if (ec) - std::cerr << "Test failed: " << ec.message() << "\n"; ----- - -The `close()` method: - -1. Closes the underlying socket -2. Checks that `provide()` buffer is empty (all data read) -3. Checks that `expect()` buffer is empty (all expected data written) -4. Returns error and calls `fuse.fail()` if verification fails - -== The Fuse - -Mockets work with `capy::test::fuse` for error injection: - -[source,cpp] ----- -capy::test::fuse f; -auto [m1, m2] = corosio::test::make_mockets(ioc, f); - -// The first mocket (m1) calls f.maybe_fail() on operations -// This enables systematic error injection testing ----- - -The second mocket (m2) doesn't call `maybe_fail()`, allowing asymmetric -testing. - -== Complete Example - -[source,cpp] ----- -#include -#include - -capy::task test_http_client() -{ - corosio::io_context ioc; - capy::test::fuse f; - - auto [client, server] = corosio::test::make_mockets(ioc, f); - - // Client should send this request - client.expect( - "GET / HTTP/1.1\r\n" - "Host: example.com\r\n" - "\r\n"); - - // Server will respond with this - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "Hello"); - - // Run the code under test - co_await my_http_get(client, "example.com", "/"); - - // Verify expectations - auto ec1 = client.close(); - auto ec2 = server.close(); - - if (ec1 || ec2) - throw std::runtime_error("Test failed"); -} ----- - -== Testing with io_stream Reference - -Since mocket inherits from `io_stream`, you can pass it to code expecting -streams: - -[source,cpp] ----- -// Your production code -capy::task send_message(corosio::io_stream& stream, std::string msg) -{ - co_await corosio::write( - stream, capy::const_buffer(msg.data(), msg.size())); -} - -// Test code -capy::task test_send_message() -{ - auto [client, server] = make_mockets(ioc, f); - - client.expect("Hello, World!"); - - co_await send_message(client, "Hello, World!"); - - auto ec = client.close(); - assert(!ec); -} ----- - -== Thread Safety - -Mockets are NOT thread-safe: - -* Use from a single thread only -* All coroutines must be suspended when calling `expect()` or `provide()` -* Designed for single-threaded, deterministic testing - -== Limitations - -* Data staging is one-way (provide on one side, read on the other) -* No simulation of partial writes or network delays -* Connection errors must be injected via fuse - -== Use Cases - -=== Unit Testing Protocol Code - -[source,cpp] ----- -// Test that your protocol parser handles responses correctly -server.provide("200 OK\r\nContent-Type: text/html\r\n\r\n..."); -co_await my_protocol_read(client); -// Verify parsed result ----- - -=== Verifying Request Format - -[source,cpp] ----- -// Ensure your code sends correctly formatted requests -client.expect("POST /api/v1/users HTTP/1.1\r\n..."); -co_await my_api_call(client, user_data); ----- - -=== Integration Testing Without Network - -[source,cpp] ----- -// Test client-server interaction without actual networking -server.provide(server_response); -client.expect(client_request); - -co_await run_client(client); -co_await run_server(server); ----- - -== Next Steps - -* xref:../guide/sockets.adoc[Sockets Guide] — The socket interface mockets implement -* xref:../guide/error-handling.adoc[Error Handling] — Testing error paths +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Mock Sockets + +The `mocket` class provides mock sockets for testing I/O code without +actual network operations. Mockets let you stage data for reading and +verify expected writes. + +NOTE: Code snippets assume: +[source,cpp] +---- +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +---- + +== Overview + +Mockets are testable socket-like objects: + +[source,cpp] +---- +// Create connected pair +capy::test::fuse f; +auto [client, server] = corosio::test::make_mockets(ioc, f); + +// Stage data on server for client to read +server.provide("Hello from server"); + +// Stage expected data that client should write +client.expect("Hello from client"); + +// Now run your code that uses client/server as io_stream& +---- + +== Creating Mockets + +Mockets are created in connected pairs: + +[source,cpp] +---- +corosio::io_context ioc; +capy::test::fuse f; + +auto [m1, m2] = corosio::test::make_mockets(ioc, f); +---- + +The pair is connected via loopback TCP sockets. Data written to one can +be read from the other, plus you can use the staging/expectation API. + +== Staging Data for Reads + +Use `provide()` to stage data that the _peer_ will read: + +[source,cpp] +---- +// On server: stage data for client to read +server.provide("HTTP/1.1 200 OK\r\n\r\nHello"); + +// Now when client reads, it gets this data +auto [ec, n] = co_await client.read_some(buffer); +// buffer contains "HTTP/1.1 200 OK\r\n\r\nHello" +---- + +Multiple `provide()` calls append data: + +[source,cpp] +---- +server.provide("Part 1"); +server.provide("Part 2"); +// Client sees "Part 1Part 2" +---- + +== Setting Write Expectations + +Use `expect()` to verify what the caller writes: + +[source,cpp] +---- +// Client should send this exact data +client.expect("GET / HTTP/1.1\r\n\r\n"); + +// Now client writes +co_await corosio::write(client, request_buffer); + +// If written data doesn't match, fuse fails +---- + +=== How Matching Works + +When you write to a mocket with expectations: + +1. Written data is compared against the expect buffer +2. If it matches, the expect buffer is consumed +3. If it doesn't match, `fuse.fail()` is called + +After the expect buffer is exhausted, writes pass through to the real socket. + +== Closing and Verification + +Use `close()` to verify all expectations were met: + +[source,cpp] +---- +auto ec = client.close(); +if (ec) + std::cerr << "Test failed: " << ec.message() << "\n"; +---- + +The `close()` method: + +1. Closes the underlying socket +2. Checks that `provide()` buffer is empty (all data read) +3. Checks that `expect()` buffer is empty (all expected data written) +4. Returns error and calls `fuse.fail()` if verification fails + +== The Fuse + +Mockets work with `capy::test::fuse` for error injection: + +[source,cpp] +---- +capy::test::fuse f; +auto [m1, m2] = corosio::test::make_mockets(ioc, f); + +// The first mocket (m1) calls f.maybe_fail() on operations +// This enables systematic error injection testing +---- + +The second mocket (m2) doesn't call `maybe_fail()`, allowing asymmetric +testing. + +== Complete Example + +[source,cpp] +---- +#include +#include + +capy::task test_http_client() +{ + corosio::io_context ioc; + capy::test::fuse f; + + auto [client, server] = corosio::test::make_mockets(ioc, f); + + // Client should send this request + client.expect( + "GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + + // Server will respond with this + server.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "Hello"); + + // Run the code under test + co_await my_http_get(client, "example.com", "/"); + + // Verify expectations + auto ec1 = client.close(); + auto ec2 = server.close(); + + if (ec1 || ec2) + throw std::runtime_error("Test failed"); +} +---- + +== Testing with io_stream Reference + +Since mocket inherits from `io_stream`, you can pass it to code expecting +streams: + +[source,cpp] +---- +// Your production code +capy::task send_message(corosio::io_stream& stream, std::string msg) +{ + co_await corosio::write( + stream, capy::const_buffer(msg.data(), msg.size())); +} + +// Test code +capy::task test_send_message() +{ + auto [client, server] = make_mockets(ioc, f); + + client.expect("Hello, World!"); + + co_await send_message(client, "Hello, World!"); + + auto ec = client.close(); + assert(!ec); +} +---- + +== Thread Safety + +Mockets are NOT thread-safe: + +* Use from a single thread only +* All coroutines must be suspended when calling `expect()` or `provide()` +* Designed for single-threaded, deterministic testing + +== Limitations + +* Data staging is one-way (provide on one side, read on the other) +* No simulation of partial writes or network delays +* Connection errors must be injected via fuse + +== Use Cases + +=== Unit Testing Protocol Code + +[source,cpp] +---- +// Test that your protocol parser handles responses correctly +server.provide("200 OK\r\nContent-Type: text/html\r\n\r\n..."); +co_await my_protocol_read(client); +// Verify parsed result +---- + +=== Verifying Request Format + +[source,cpp] +---- +// Ensure your code sends correctly formatted requests +client.expect("POST /api/v1/users HTTP/1.1\r\n..."); +co_await my_api_call(client, user_data); +---- + +=== Integration Testing Without Network + +[source,cpp] +---- +// Test client-server interaction without actual networking +server.provide(server_response); +client.expect(client_request); + +co_await run_client(client); +co_await run_server(server); +---- + +== Next Steps + +* xref:../4.guide/4d.sockets.adoc[Sockets Guide] — The socket interface mockets implement +* xref:../4.guide/4m.error-handling.adoc[Error Handling] — Testing error paths diff --git a/doc/modules/ROOT/pages/reference/benchmark-report.adoc b/doc/modules/ROOT/pages/benchmark-report.adoc similarity index 100% rename from doc/modules/ROOT/pages/reference/benchmark-report.adoc rename to doc/modules/ROOT/pages/benchmark-report.adoc diff --git a/doc/modules/ROOT/pages/concepts/affine-awaitables.adoc b/doc/modules/ROOT/pages/concepts/affine-awaitables.adoc deleted file mode 100644 index 471447a8..00000000 --- a/doc/modules/ROOT/pages/concepts/affine-awaitables.adoc +++ /dev/null @@ -1,316 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= Affine Awaitables - -The _affine awaitable protocol_ is a core concept in Corosio that enables -automatic executor affinity propagation through coroutine chains. This page -explains how it works and why it matters. - -== The Problem - -When an I/O operation completes, _some_ thread receives the completion -notification. Without affinity tracking: - ----- -Thread 1: coroutine starts → co_await read() → suspends -Thread 2: (I/O completes) → coroutine resumes here (surprise!) ----- - -Your coroutine might resume on an arbitrary thread, forcing you to add -synchronization everywhere. - -== The Solution: Executor Affinity - -Affinity means a coroutine is bound to a specific executor. All resumptions -occur through that executor: - ----- -Thread 1: coroutine starts → co_await read() → suspends -Thread 1: (executor dispatches) → coroutine resumes here (correct!) ----- - -When you launch a coroutine with `run_async(ex)`, it has affinity to executor -`ex`. All its I/O operations capture `ex` and resume through it. - -== How Affinity Propagates - -The affine awaitable protocol passes the executor through `co_await`: - -[source,cpp] ----- -capy::run_async(ex)(parent()); // parent has affinity to ex - -task parent() -{ - co_await child(); // child inherits ex -} - -task child() -{ - co_await sock.read_some(buf); // read captures ex, resumes through ex -} ----- - -Each `co_await` passes the current dispatcher to the awaited operation. - -== The Protocol in Detail - -An affine awaitable provides special `await_suspend` overloads that receive -the dispatcher: - -[source,cpp] ----- -struct my_awaitable -{ - bool await_ready() const noexcept; - Result await_resume() const noexcept; - - // Standard form (for compatibility) - void await_suspend(std::coroutine_handle<> h); - - // Affine form: receives dispatcher - template - auto await_suspend( - std::coroutine_handle<> h, - Dispatcher const& d) -> std::coroutine_handle<>; - - // Affine form with stop token: receives dispatcher and cancellation - template - auto await_suspend( - std::coroutine_handle<> h, - Dispatcher const& d, - std::stop_token token) -> std::coroutine_handle<>; -}; ----- - -The task's `await_transform` selects the appropriate overload based on -what the awaitable supports. - -== Corosio Awaitables - -All Corosio I/O operations return affine awaitables: - -[source,cpp] ----- -// socket::connect returns connect_awaitable -auto [ec] = co_await sock.connect(endpoint); - -// socket::read_some returns read_some_awaitable -auto [ec, n] = co_await sock.read_some(buffer); - -// timer::wait returns wait_awaitable -auto [ec] = co_await timer.wait(); ----- - -Each stores the dispatcher provided during `await_suspend` and uses it -to resume the coroutine when the operation completes. - -== Dispatcher Type Erasure - -Corosio uses `capy::any_dispatcher` for type erasure: - -[source,cpp] ----- -template -auto await_suspend( - std::coroutine_handle<> h, - Dispatcher const& d) -> std::coroutine_handle<> -{ - // Store type-erased dispatcher - impl_->do_operation(h, capy::any_dispatcher(d), ...); - return std::noop_coroutine(); -} ----- - -This allows the implementation to work with any executor type without -templating everything. - -== Symmetric Transfer - -When a child coroutine completes, it resumes its parent. If both have the -same executor, _symmetric transfer_ provides a direct tail call: - -[source,cpp] ----- -task parent() -{ - co_await child(); // child completes, transfers directly to parent -} ----- - -No executor involvement, no queuing—just a direct coroutine-to-coroutine -transfer. - -The mechanism: - -1. Child's final suspend awaitable returns parent's handle -2. Compiler generates tail call to `coroutine_handle::resume()` -3. Parent resumes immediately on same thread - -If executors differ, the child posts to the parent's executor instead. - -== Cancellation Support - -Affine awaitables can receive a stop token: - -[source,cpp] ----- -template -auto await_suspend( - std::coroutine_handle<> h, - Dispatcher const& d, - std::stop_token token) -> std::coroutine_handle<> -{ - // Can check token.stop_requested() - // Can register for stop notification -} ----- - -Corosio operations check `stop_requested()` in `await_ready()` and during -the operation for prompt cancellation. - -== Flow Diagram Notation - -To reason about affinity, use this compact notation: - -[cols="1,3"] -|=== -| Symbol | Meaning - -| `c`, `c1`, `c2` -| Coroutines (lazy tasks) - -| `io` -| I/O operation - -| `->` -| `co_await` leading to a coroutine or I/O - -| `!` -| Coroutine with explicit executor affinity - -| `ex`, `ex1`, `ex2` -| Executors -|=== - -=== Simple Chain - ----- -!c -> io ----- - -Coroutine `c` has affinity. The I/O captures that affinity and resumes -through it. - -=== Nested Coroutines - ----- -!c1 -> c2 -> io ----- - -* `c1` has explicit affinity to `ex` -* `c2` inherits affinity from `c1` -* I/O captures `ex` -* When I/O completes: resume through `ex` -* When `c2` completes: symmetric transfer to `c1` - -== Implementing Affine Awaitables - -To implement your own affine awaitable: - -[source,cpp] ----- -struct my_async_op -{ - // Required members - operation_state& state_; - - bool await_ready() const noexcept - { - return state_.is_complete(); - } - - Result await_resume() const noexcept - { - return state_.get_result(); - } - - // Affine suspend with dispatcher - template - auto await_suspend( - std::coroutine_handle<> h, - Dispatcher const& d) -> std::coroutine_handle<> - { - // Store h and d, start operation - state_.start(h, d); - return std::noop_coroutine(); - } - - // Affine suspend with dispatcher and stop token - template - auto await_suspend( - std::coroutine_handle<> h, - Dispatcher const& d, - std::stop_token token) -> std::coroutine_handle<> - { - state_.start(h, d, token); - return std::noop_coroutine(); - } -}; ----- - -When the operation completes, use the dispatcher to resume: - -[source,cpp] ----- -void complete() -{ - dispatcher_(continuation_); // Resume through dispatcher -} ----- - -== Legacy Awaitable Compatibility - -Not all awaitables support the affine protocol. Capy's task provides -automatic compatibility through `await_transform`: - -* If awaitable is affine: zero-overhead dispatch -* If awaitable is standard: wrap in trampoline coroutine - -The trampoline ensures correct affinity at the cost of one extra -coroutine frame. - -== Summary - -[cols="1,3"] -|=== -| Concept | Description - -| Executor affinity -| Coroutine bound to specific executor - -| Propagation -| Children inherit affinity via `co_await` - -| Affine protocol -| `await_suspend` receives dispatcher parameter - -| Symmetric transfer -| Zero-overhead resumption when executors match - -| any_dispatcher -| Type-erased dispatcher for implementation -|=== - -== Next Steps - -* xref:../guide/io-context.adoc[I/O Context] — The execution context -* xref:../guide/error-handling.adoc[Error Handling] — Cancellation patterns -* xref:../reference/design-rationale.adoc[Design Rationale] — Why this design diff --git a/doc/modules/ROOT/pages/reference/glossary.adoc b/doc/modules/ROOT/pages/glossary.adoc similarity index 93% rename from doc/modules/ROOT/pages/reference/glossary.adoc rename to doc/modules/ROOT/pages/glossary.adoc index 7af240a9..96aa52e1 100644 --- a/doc/modules/ROOT/pages/reference/glossary.adoc +++ b/doc/modules/ROOT/pages/glossary.adoc @@ -15,7 +15,7 @@ This glossary defines terms used throughout the Corosio documentation. Acceptor:: An I/O object that listens for and accepts incoming TCP connections. See -`corosio::tcp_acceptor` and xref:../guide/tcp_acceptor.adoc[Acceptors Guide]. +`corosio::tcp_acceptor` and xref:4.guide/4e.tcp-acceptor.adoc[Acceptors Guide]. Affine Awaitable:: An awaitable type that implements the affine protocol, receiving a dispatcher @@ -216,7 +216,7 @@ A mechanism for requesting cancellation. See `std::stop_token`. Strand:: A serialization mechanism that ensures handlers don't run concurrently. Operations posted to a strand execute one at a time, eliminating data -races without mutexes. See xref:../guide/concurrent-programming.adoc[Concurrent Programming]. +races without mutexes. See xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming]. Stream:: A sequence of bytes that can be read or written incrementally. @@ -238,7 +238,7 @@ A lazy coroutine that produces a value. See `capy::task`. TCP Server:: A framework class for building TCP servers with worker pools. See -`corosio::tcp_server` and xref:../guide/tcp-server.adoc[TCP Server Guide]. +`corosio::tcp_server` and xref:4.guide/4k.tcp-server.adoc[TCP Server Guide]. Thread Safety:: The ability to use an object safely from multiple threads. Individual I/O @@ -269,9 +269,8 @@ Pending operations that keep an I/O context running. Worker Pool:: A design pattern where a fixed number of worker objects are preallocated to handle connections. Provides bounded resource usage and avoids allocation -during operation. See xref:../guide/tcp-server.adoc[TCP Server]. +during operation. See xref:4.guide/4k.tcp-server.adoc[TCP Server]. == See Also -* xref:design-rationale.adoc[Design Rationale] — Why Corosio is designed this way -* xref:../concepts/affine-awaitables.adoc[Affine Awaitables] — The dispatch protocol +* xref:reference:boost/corosio.adoc[Reference] — API reference documentation diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index edb288c9..c62e4d3a 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -65,8 +65,8 @@ using modern asynchronous programming patterns. This documentation assumes: * **Understanding of C++20 coroutines** — `co_await`, `co_return`, awaitables * **Basic TCP/IP networking concepts** — clients, servers, ports, connections -If you're new to these topics, see xref:guide/tcp-networking.adoc[TCP/IP Networking] -and xref:guide/concurrent-programming.adoc[Concurrent Programming] for background. +If you're new to these topics, see xref:4.guide/4a.tcp-networking.adoc[TCP/IP Networking] +and xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] for background. == Requirements @@ -145,7 +145,7 @@ int main() == Next Steps * xref:quick-start.adoc[Quick Start] — Build a working echo server -* xref:guide/tcp-networking.adoc[TCP/IP Networking] — Networking fundamentals -* xref:guide/concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands -* xref:guide/io-context.adoc[I/O Context] — Understand the event loop -* xref:guide/sockets.adoc[Sockets] — Learn socket operations in detail +* xref:4.guide/4a.tcp-networking.adoc[TCP/IP Networking] — Networking fundamentals +* xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands +* xref:4.guide/4c.io-context.adoc[I/O Context] — Understand the event loop +* xref:4.guide/4d.sockets.adoc[Sockets] — Learn socket operations in detail diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc index d206d3c8..6cd16d42 100644 --- a/doc/modules/ROOT/pages/quick-start.adoc +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -200,9 +200,9 @@ failed. Now that you have a working echo server: -* xref:guide/tcp-server.adoc[TCP Server Guide] — Deep dive into tcp_server -* xref:guide/tcp-networking.adoc[TCP/IP Networking] — Networking fundamentals -* xref:guide/concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands -* xref:tutorials/http-client.adoc[HTTP Client Tutorial] — Make HTTP requests -* xref:guide/io-context.adoc[I/O Context Guide] — Understand the event loop -* xref:guide/sockets.adoc[Sockets Guide] — Deep dive into socket operations +* xref:4.guide/4k.tcp-server.adoc[TCP Server Guide] — Deep dive into tcp_server +* xref:4.guide/4a.tcp-networking.adoc[TCP/IP Networking] — Networking fundamentals +* xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands +* xref:3.tutorials/3b.http-client.adoc[HTTP Client Tutorial] — Make HTTP requests +* xref:4.guide/4c.io-context.adoc[I/O Context Guide] — Understand the event loop +* xref:4.guide/4d.sockets.adoc[Sockets Guide] — Deep dive into socket operations diff --git a/doc/modules/ROOT/pages/reference/design-rationale.adoc b/doc/modules/ROOT/pages/reference/design-rationale.adoc deleted file mode 100644 index dc8c389d..00000000 --- a/doc/modules/ROOT/pages/reference/design-rationale.adoc +++ /dev/null @@ -1,290 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -= Design Rationale - -This page explains the key design decisions in Corosio and the trade-offs -considered. - -== Coroutine-First Design - -=== Decision - -Every I/O operation returns an awaitable. There is no callback-based API. - -=== Rationale - -* **Simplicity**: One interface, not two parallel APIs -* **Optimal codegen**: No compatibility layer between callbacks and coroutines -* **Natural error handling**: Structured bindings and exceptions work directly -* **Composability**: Awaitables compose with standard coroutine patterns - -=== Trade-off - -Users without C++20 coroutine support cannot use the library. This is -intentional—Corosio targets modern C++ exclusively. - -== Affine Awaitable Protocol - -=== Decision - -Executor affinity propagates through `await_suspend` parameters rather than -thread-local storage or coroutine promise members. - -=== Rationale - -* **Explicit data flow**: The dispatcher visibly flows through the code -* **No hidden state**: No surprises from thread-local or global state -* **Compatibility**: Works with any coroutine framework that calls `await_suspend` -* **Efficient**: Symmetric transfer works automatically when appropriate - -=== Trade-off - -Implementing affine awaitables requires additional `await_suspend` overloads. -The complexity is contained in the library; users just `co_await`. - -== io_result Type - -=== Decision - -Operations return `io_result` which combines an error code with optional -values and supports both structured bindings and exceptions. - -=== Rationale - -* **Flexibility**: Users choose error handling style per-callsite -* **Zero overhead**: No exception overhead when using structured bindings -* **No information loss**: Byte count available even on error -* **Clean syntax**: `auto [ec, n] = co_await ...` is concise - -=== Trade-off - -The `.value()` method name might conflict with users' expectations from -`std::optional` (which throws on empty). Here it throws on error, which is -semantically similar but contextually different. - -== Type-Erased Dispatchers - -=== Decision - -Socket implementations use `capy::any_dispatcher` internally rather than -templating on the executor type. - -=== Rationale - -* **Binary size**: Only one implementation per I/O object -* **Compile time**: No template instantiation explosion -* **Virtual interface**: Enables platform-specific implementations - -=== Trade-off - -Small runtime overhead from type erasure. For I/O-bound code, this is -negligible compared to actual I/O latency (microseconds vs. nanoseconds). - -== Inheritance Hierarchy - -=== Decision - -`socket` inherits from `io_stream` which inherits from `io_object`. - ----- -io_object - ├── acceptor - ├── resolver - ├── timer - ├── signal_set - └── io_stream - ├── socket - └── wolfssl_stream ----- - -=== Rationale - -* **Polymorphism**: Code accepting `io_stream&` works with any stream type -* **Code reuse**: `read()` and `write()` free functions work with all streams -* **Future extensibility**: New stream types fit naturally - -=== Trade-off - -Virtual function overhead for `read_some()`/`write_some()`. Acceptable -because I/O operations are inherently expensive. - -== Buffer Type Erasure (buffer_param) - -=== Decision - -Buffer sequences are type-erased at the I/O boundary using `buffer_param`. - -=== Rationale - -* **Non-template implementations**: Scheduler and I/O objects aren't templates -* **ABI stability**: Buffer types can change without recompilation -* **Reduced binary size**: Single implementation handles all buffer types - -=== Trade-off - -One level of indirection when copying buffer descriptors. The copy is into -a small fixed-size array, so overhead is minimal. - -== consuming_buffers for Composed Operations - -=== Decision - -The `read()` and `write()` composed operations use `consuming_buffers` to -track progress through buffer sequences. - -=== Rationale - -* **Efficiency**: Avoids copying buffer sequences -* **Correctness**: Handles partial reads/writes across multiple buffers -* **Reusability**: Can be used directly by advanced users - -=== Trade-off - -More complex than repeatedly constructing sub-buffers, but more efficient -for multi-buffer sequences. - -== Separate open() and connect() - -=== Decision - -Sockets require explicit `open()` before `connect()`. - -=== Rationale - -* **Explicit resource management**: Clear when system resources are allocated -* **Error handling**: Open errors distinct from connect errors -* **Consistency**: Matches acceptor pattern (explicit `listen()`) - -=== Trade-off - -Two calls instead of one. A `connect(endpoint)` overload that opens -automatically could be added if users prefer. - -== Move-Only I/O Objects - -=== Decision - -Sockets, timers, and other I/O objects are move-only. - -=== Rationale - -* **Ownership semantics**: I/O objects own system resources -* **No accidental copies**: Prevents resource leaks -* **Efficient transfer**: Moving is cheap (pointer swap) - -=== Trade-off - -Cannot store in containers that require copyability. Use `std::unique_ptr` -or move-aware containers. - -== Context-Locked Move Assignment - -=== Decision - -Moving an I/O object to another with a different execution context throws. - -=== Rationale - -* **Safety**: Prevents dangling references to old context's services -* **Simplicity**: No need for detach/reattach mechanism - -=== Trade-off - -Cannot move objects between contexts. Create new objects instead. - -== Platform-Specific Backends - -=== Decision - -Windows uses IOCP directly. Linux will use io_uring. macOS will use kqueue. - -=== Rationale - -* **Performance**: Native backends are fastest -* **Scalability**: Platform-optimized for thousands of connections -* **Features**: Full access to platform capabilities - -=== Trade-off - -More implementation work per platform. Epoll fallback could be added for -broader Linux compatibility. - -== WolfSSL for TLS - -=== Decision - -TLS is provided through WolfSSL rather than OpenSSL. - -=== Rationale - -* **Small footprint**: WolfSSL is more compact -* **Clean API**: Modern C++ friendly -* **Licensing**: Flexible licensing options - -=== Trade-off - -OpenSSL is more widely deployed. Users who need OpenSSL can create their -own stream wrapper following the `io_stream` interface. - -== No UDP (Yet) - -=== Decision - -Only TCP is currently supported. - -=== Rationale - -* **Focus**: TCP covers most use cases -* **Complexity**: UDP requires different abstractions (datagrams vs. streams) -* **Priority**: Get TCP right first - -=== Trade-off - -Users needing UDP must use other libraries. UDP support is planned. - -== Single-Header Include - -=== Decision - -`` includes core functionality but not everything. - -=== Rationale - -* **Convenience**: Easy to get started -* **Control**: Advanced headers included explicitly -* **Compile time**: Full include not excessive - -The main header includes: - -* io_context -* socket -* endpoint -* resolver -* read/write - -Not included (explicit include required): - -* acceptor -* timer -* signal_set -* wolfssl_stream -* test/mocket - -== Summary - -Corosio's design prioritizes: - -1. **Simplicity**: One way to do things, not two -2. **Performance**: Zero-overhead abstractions where possible -3. **Safety**: Ownership semantics prevent resource leaks -4. **Composability**: Works with standard C++ patterns -5. **Extensibility**: Clean hierarchy for new types - -Trade-offs generally favor correctness and clarity over maximum flexibility. diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 71422848..20e5bbf1 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +# Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/example/client/CMakeLists.txt b/example/client/CMakeLists.txt index 7651fc5a..7531170d 100644 --- a/example/client/CMakeLists.txt +++ b/example/client/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +# Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/example/client/http_client.cpp b/example/client/http_client.cpp index 3004e849..53a9b3d7 100644 --- a/example/client/http_client.cpp +++ b/example/client/http_client.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/example/echo-server/CMakeLists.txt b/example/echo-server/CMakeLists.txt index 1b5fdf4f..10d41071 100644 --- a/example/echo-server/CMakeLists.txt +++ b/example/echo-server/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +# Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/example/echo-server/echo_server.cpp b/example/echo-server/echo_server.cpp index 41af481c..c0ace642 100644 --- a/example/echo-server/echo_server.cpp +++ b/example/echo-server/echo_server.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) @@ -53,7 +53,7 @@ class echo_worker : public corosio::tcp_server::worker_base auto [ec, n] = co_await sock_.read_some( capy::mutable_buffer(buf_.data(), buf_.size())); - if (ec || n == 0) + if (ec) break; buf_.resize(n); diff --git a/example/https-client/CMakeLists.txt b/example/https-client/CMakeLists.txt index c1ca46d8..8d1afd83 100644 --- a/example/https-client/CMakeLists.txt +++ b/example/https-client/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +# Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/example/https-client/https_client.cpp b/example/https-client/https_client.cpp index e263e836..24e36981 100644 --- a/example/https-client/https_client.cpp +++ b/example/https-client/https_client.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/example/nslookup/CMakeLists.txt b/example/nslookup/CMakeLists.txt index fd84fe30..ebffec5c 100644 --- a/example/nslookup/CMakeLists.txt +++ b/example/nslookup/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +# Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/example/nslookup/nslookup.cpp b/example/nslookup/nslookup.cpp index dc6cbc47..9508a3de 100644 --- a/example/nslookup/nslookup.cpp +++ b/example/nslookup/nslookup.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/example/tls_context_examples.cpp b/example/tls_context_examples.cpp index b4b75e31..04e9a639 100644 --- a/example/tls_context_examples.cpp +++ b/example/tls_context_examples.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio.hpp b/include/boost/corosio.hpp index 821ad8a1..9401120a 100644 --- a/include/boost/corosio.hpp +++ b/include/boost/corosio.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/detail/scheduler.hpp b/include/boost/corosio/detail/scheduler.hpp index 8b108f72..b3b5aea8 100644 --- a/include/boost/corosio/detail/scheduler.hpp +++ b/include/boost/corosio/detail/scheduler.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/endpoint.hpp b/include/boost/corosio/endpoint.hpp index c25eca4e..7f1839b8 100644 --- a/include/boost/corosio/endpoint.hpp +++ b/include/boost/corosio/endpoint.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/io_buffer_param.hpp b/include/boost/corosio/io_buffer_param.hpp index 08cea13a..caceebd7 100644 --- a/include/boost/corosio/io_buffer_param.hpp +++ b/include/boost/corosio/io_buffer_param.hpp @@ -1,383 +1,383 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_IO_BUFFER_PARAM_HPP -#define BOOST_COROSIO_IO_BUFFER_PARAM_HPP - -#include -#include - -#include - -namespace boost::corosio { - -/** A type-erased buffer sequence for I/O system call boundaries. - - This class enables I/O objects to accept any buffer sequence type - across a virtual function boundary, while preserving the caller's - typed buffer sequence at the call site. The implementation can - then unroll the type-erased sequence into platform-native - structures (e.g., `iovec` on POSIX, `WSABUF` on Windows) for the - actual system call. - - @par Purpose - - When building coroutine-based I/O abstractions, a common pattern - emerges: a templated awaitable captures the caller's buffer - sequence, and at `await_suspend` time, must pass it across a - virtual interface to the I/O implementation. This class solves - the type-erasure problem at that boundary without heap allocation. - - @par Restricted Use Case - - This is NOT a general-purpose composable abstraction. It exists - solely for the final step in a coroutine I/O call chain where: - - @li A templated awaitable captures the caller's buffer sequence - @li The awaitable's `await_suspend` passes buffers across a - virtual interface to an I/O object implementation - @li The implementation immediately unrolls the buffers into - platform-native structures for the system call - - @par Lifetime Model - - The safety of this class depends entirely on coroutine parameter - lifetime extension. When a coroutine is suspended, parameters - passed to the awaitable remain valid until the coroutine resumes - or is destroyed. This class exploits that guarantee by holding - only a pointer to the caller's buffer sequence. - - The referenced buffer sequence is valid ONLY while the calling - coroutine remains suspended at the exact suspension point where - `io_buffer_param` was created. Once the coroutine resumes, - returns, or is destroyed, all referenced data becomes invalid. - - @par Const Buffer Handling - - This class accepts both `ConstBufferSequence` and - `MutableBufferSequence` types. However, `copy_to` always produces - `mutable_buffer` descriptors, casting away constness for const - buffer sequences. This design matches platform I/O structures - (`iovec`, `WSABUF`) which use non-const pointers regardless of - the operation direction. - - @warning The caller is responsible for ensuring the type system - is not violated. When the original buffer sequence was const - (e.g., for a write operation), the implementation MUST NOT write - to the buffers obtained from `copy_to`. The const-cast exists - solely to provide a uniform interface for platform I/O calls. - - @code - // For write operations (const buffers): - void submit_write(io_buffer_param p) - { - capy::mutable_buffer bufs[8]; - auto n = p.copy_to(bufs, 8); - // bufs[] may reference const data - DO NOT WRITE - writev(fd, reinterpret_cast(bufs), n); // OK: read-only - } - - // For read operations (mutable buffers): - void submit_read(io_buffer_param p) - { - capy::mutable_buffer bufs[8]; - auto n = p.copy_to(bufs, 8); - // bufs[] references mutable data - safe to write - readv(fd, reinterpret_cast(bufs), n); // OK: writing - } - @endcode - - @par Correct Usage - - The implementation receiving `io_buffer_param` MUST: - - @li Call `copy_to` immediately upon receiving the parameter - @li Use the unrolled buffer descriptors for the I/O operation - @li Never store the `io_buffer_param` object itself - @li Never store pointers obtained from `copy_to` beyond the - immediate I/O operation - - @par Example: Correct Usage - - @code - // Templated awaitable at the call site - template - struct write_awaitable - { - Buffers bufs; - io_stream* stream; - - bool await_ready() { return false; } - - void await_suspend(std::coroutine_handle<> h) - { - // CORRECT: Pass to virtual interface while suspended. - // The buffer sequence 'bufs' remains valid because - // coroutine parameters live until resumption. - stream->async_write_some_impl(bufs, h); - } - - io_result await_resume() { return stream->get_result(); } - }; - - // Virtual implementation - unrolls immediately - void stream_impl::async_write_some_impl( - io_buffer_param p, - std::coroutine_handle<> h) - { - // CORRECT: Unroll immediately into platform structure - iovec vecs[16]; - std::size_t n = p.copy_to( - reinterpret_cast(vecs), 16); - - // CORRECT: Use unrolled buffers for system call now - submit_to_io_uring(vecs, n, h); - - // After this function returns, 'p' must not be used again. - // The iovec array is safe because it contains copies of - // the pointer/size pairs, not references to 'p'. - } - @endcode - - @par UNSAFE USAGE: Storing io_buffer_param - - @warning Never store `io_buffer_param` for later use. - - @code - class broken_stream - { - io_buffer_param saved_param_; // UNSAFE: member storage - - void async_write_impl(io_buffer_param p, ...) - { - saved_param_ = p; // UNSAFE: storing for later - schedule_write_later(); - } - - void do_write_later() - { - // UNSAFE: The calling coroutine may have resumed - // or been destroyed. saved_param_ now references - // invalid memory! - capy::mutable_buffer bufs[8]; - saved_param_.copy_to(bufs, 8); // UNDEFINED BEHAVIOR - } - }; - @endcode - - @par UNSAFE USAGE: Storing Unrolled Pointers - - @warning The pointers obtained from `copy_to` point into the - caller's buffer sequence. They become invalid when the caller - resumes. - - @code - class broken_stream - { - capy::mutable_buffer saved_bufs_[8]; // UNSAFE - std::size_t saved_count_; - - void async_write_impl(io_buffer_param p, ...) - { - // This copies pointer/size pairs into saved_bufs_ - saved_count_ = p.copy_to(saved_bufs_, 8); - - // UNSAFE: scheduling for later while storing the - // buffer descriptors. The pointers in saved_bufs_ - // will dangle when the caller resumes! - schedule_for_later(); - } - - void later() - { - // UNSAFE: saved_bufs_ contains dangling pointers - for(std::size_t i = 0; i < saved_count_; ++i) - write(fd_, saved_bufs_[i].data(), ...); // UB - } - }; - @endcode - - @par UNSAFE USAGE: Using Outside a Coroutine - - @warning This class relies on coroutine lifetime semantics. - Using it with callbacks or non-coroutine async patterns is - undefined behavior. - - @code - // UNSAFE: No coroutine lifetime guarantee - void bad_callback_pattern(std::vector& data) - { - capy::mutable_buffer buf(data.data(), data.size()); - - // UNSAFE: In a callback model, 'buf' may go out of scope - // before the callback fires. There is no coroutine - // suspension to extend the lifetime. - stream.async_write(buf, [](error_code ec) { - // 'buf' is already destroyed! - }); - } - @endcode - - @par UNSAFE USAGE: Passing to Another Coroutine - - @warning Do not pass `io_buffer_param` to a different coroutine - or spawn a new coroutine that captures it. - - @code - void broken_impl(io_buffer_param p, std::coroutine_handle<> h) - { - // UNSAFE: Spawning a new coroutine that captures 'p'. - // The original coroutine may resume before this new - // coroutine uses 'p'. - co_spawn([p]() -> task { - capy::mutable_buffer bufs[8]; - p.copy_to(bufs, 8); // UNSAFE: original caller may - // have resumed already! - co_return; - }); - } - @endcode - - @par UNSAFE USAGE: Multiple Virtual Hops - - @warning Minimize indirection. Each virtual call that passes - `io_buffer_param` without immediately unrolling it increases - the risk of misuse. - - @code - // Risky: multiple hops before unrolling - void layer1(io_buffer_param p) { - layer2(p); // Still haven't unrolled... - } - void layer2(io_buffer_param p) { - layer3(p); // Still haven't unrolled... - } - void layer3(io_buffer_param p) { - // Finally unrolling, but the chain is fragile. - // Any intermediate layer storing 'p' breaks everything. - } - @endcode - - @par UNSAFE USAGE: Fire-and-Forget Operations - - @warning Do not use with detached or fire-and-forget async - operations where there is no guarantee the caller remains - suspended. - - @code - task caller() - { - char buf[1024]; - // UNSAFE: If async_write is fire-and-forget (doesn't - // actually suspend the caller), 'buf' may be destroyed - // before the I/O completes. - stream.async_write_detached(capy::mutable_buffer(buf, 1024)); - // Returns immediately - 'buf' goes out of scope! - } - @endcode - - @par Passing Convention - - Pass by value. The class contains only two pointers (16 bytes - on 64-bit systems), making copies trivial and clearly - communicating the lightweight, transient nature of this type. - - @code - // Preferred: pass by value - void process(io_buffer_param buffers); - - // Also acceptable: pass by const reference - void process(io_buffer_param const& buffers); - @endcode - - @see capy::ConstBufferSequence, capy::MutableBufferSequence -*/ -class io_buffer_param -{ -public: - /** Construct from a const buffer sequence. - - @param bs The buffer sequence to adapt. - */ - template - io_buffer_param(BS const& bs) noexcept - : bs_(&bs) - , fn_(©_impl) - { - } - - /** Fill an array with buffers from the sequence. - - Copies buffer descriptors from the sequence into the - destination array, skipping any zero-size buffers. - This ensures the output contains only buffers with - actual data, suitable for direct use with system calls. - - @param dest Pointer to array of mutable buffer descriptors. - @param n Maximum number of buffers to copy. - - @return The number of non-zero buffers copied. - */ - std::size_t - copy_to( - capy::mutable_buffer* dest, - std::size_t n) const noexcept - { - return fn_(bs_, dest, n); - } - -private: - template - static std::size_t - copy_impl( - void const* p, - capy::mutable_buffer* dest, - std::size_t n) - { - auto const& bs = *static_cast(p); - auto it = capy::begin(bs); - auto const end_it = capy::end(bs); - - std::size_t i = 0; - if constexpr (capy::MutableBufferSequence) - { - for(; it != end_it && i < n; ++it) - { - capy::mutable_buffer buf(*it); - if(buf.size() == 0) - continue; - dest[i++] = buf; - } - } - else - { - for(; it != end_it && i < n; ++it) - { - capy::const_buffer buf(*it); - if(buf.size() == 0) - continue; - dest[i++] = capy::mutable_buffer( - const_cast( - static_cast(buf.data())), - buf.size()); - } - } - return i; - } - - using fn_t = std::size_t(*)(void const*, - capy::mutable_buffer*, std::size_t); - - void const* bs_; - fn_t fn_; -}; - -} // namespace boost::corosio - -#endif +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_IO_BUFFER_PARAM_HPP +#define BOOST_COROSIO_IO_BUFFER_PARAM_HPP + +#include +#include + +#include + +namespace boost::corosio { + +/** A type-erased buffer sequence for I/O system call boundaries. + + This class enables I/O objects to accept any buffer sequence type + across a virtual function boundary, while preserving the caller's + typed buffer sequence at the call site. The implementation can + then unroll the type-erased sequence into platform-native + structures (e.g., `iovec` on POSIX, `WSABUF` on Windows) for the + actual system call. + + @par Purpose + + When building coroutine-based I/O abstractions, a common pattern + emerges: a templated awaitable captures the caller's buffer + sequence, and at `await_suspend` time, must pass it across a + virtual interface to the I/O implementation. This class solves + the type-erasure problem at that boundary without heap allocation. + + @par Restricted Use Case + + This is NOT a general-purpose composable abstraction. It exists + solely for the final step in a coroutine I/O call chain where: + + @li A templated awaitable captures the caller's buffer sequence + @li The awaitable's `await_suspend` passes buffers across a + virtual interface to an I/O object implementation + @li The implementation immediately unrolls the buffers into + platform-native structures for the system call + + @par Lifetime Model + + The safety of this class depends entirely on coroutine parameter + lifetime extension. When a coroutine is suspended, parameters + passed to the awaitable remain valid until the coroutine resumes + or is destroyed. This class exploits that guarantee by holding + only a pointer to the caller's buffer sequence. + + The referenced buffer sequence is valid ONLY while the calling + coroutine remains suspended at the exact suspension point where + `io_buffer_param` was created. Once the coroutine resumes, + returns, or is destroyed, all referenced data becomes invalid. + + @par Const Buffer Handling + + This class accepts both `ConstBufferSequence` and + `MutableBufferSequence` types. However, `copy_to` always produces + `mutable_buffer` descriptors, casting away constness for const + buffer sequences. This design matches platform I/O structures + (`iovec`, `WSABUF`) which use non-const pointers regardless of + the operation direction. + + @warning The caller is responsible for ensuring the type system + is not violated. When the original buffer sequence was const + (e.g., for a write operation), the implementation MUST NOT write + to the buffers obtained from `copy_to`. The const-cast exists + solely to provide a uniform interface for platform I/O calls. + + @code + // For write operations (const buffers): + void submit_write(io_buffer_param p) + { + capy::mutable_buffer bufs[8]; + auto n = p.copy_to(bufs, 8); + // bufs[] may reference const data - DO NOT WRITE + writev(fd, reinterpret_cast(bufs), n); // OK: read-only + } + + // For read operations (mutable buffers): + void submit_read(io_buffer_param p) + { + capy::mutable_buffer bufs[8]; + auto n = p.copy_to(bufs, 8); + // bufs[] references mutable data - safe to write + readv(fd, reinterpret_cast(bufs), n); // OK: writing + } + @endcode + + @par Correct Usage + + The implementation receiving `io_buffer_param` MUST: + + @li Call `copy_to` immediately upon receiving the parameter + @li Use the unrolled buffer descriptors for the I/O operation + @li Never store the `io_buffer_param` object itself + @li Never store pointers obtained from `copy_to` beyond the + immediate I/O operation + + @par Example: Correct Usage + + @code + // Templated awaitable at the call site + template + struct write_awaitable + { + Buffers bufs; + io_stream* stream; + + bool await_ready() { return false; } + + void await_suspend(std::coroutine_handle<> h) + { + // CORRECT: Pass to virtual interface while suspended. + // The buffer sequence 'bufs' remains valid because + // coroutine parameters live until resumption. + stream->async_write_some_impl(bufs, h); + } + + io_result await_resume() { return stream->get_result(); } + }; + + // Virtual implementation - unrolls immediately + void stream_impl::async_write_some_impl( + io_buffer_param p, + std::coroutine_handle<> h) + { + // CORRECT: Unroll immediately into platform structure + iovec vecs[16]; + std::size_t n = p.copy_to( + reinterpret_cast(vecs), 16); + + // CORRECT: Use unrolled buffers for system call now + submit_to_io_uring(vecs, n, h); + + // After this function returns, 'p' must not be used again. + // The iovec array is safe because it contains copies of + // the pointer/size pairs, not references to 'p'. + } + @endcode + + @par UNSAFE USAGE: Storing io_buffer_param + + @warning Never store `io_buffer_param` for later use. + + @code + class broken_stream + { + io_buffer_param saved_param_; // UNSAFE: member storage + + void async_write_impl(io_buffer_param p, ...) + { + saved_param_ = p; // UNSAFE: storing for later + schedule_write_later(); + } + + void do_write_later() + { + // UNSAFE: The calling coroutine may have resumed + // or been destroyed. saved_param_ now references + // invalid memory! + capy::mutable_buffer bufs[8]; + saved_param_.copy_to(bufs, 8); // UNDEFINED BEHAVIOR + } + }; + @endcode + + @par UNSAFE USAGE: Storing Unrolled Pointers + + @warning The pointers obtained from `copy_to` point into the + caller's buffer sequence. They become invalid when the caller + resumes. + + @code + class broken_stream + { + capy::mutable_buffer saved_bufs_[8]; // UNSAFE + std::size_t saved_count_; + + void async_write_impl(io_buffer_param p, ...) + { + // This copies pointer/size pairs into saved_bufs_ + saved_count_ = p.copy_to(saved_bufs_, 8); + + // UNSAFE: scheduling for later while storing the + // buffer descriptors. The pointers in saved_bufs_ + // will dangle when the caller resumes! + schedule_for_later(); + } + + void later() + { + // UNSAFE: saved_bufs_ contains dangling pointers + for(std::size_t i = 0; i < saved_count_; ++i) + write(fd_, saved_bufs_[i].data(), ...); // UB + } + }; + @endcode + + @par UNSAFE USAGE: Using Outside a Coroutine + + @warning This class relies on coroutine lifetime semantics. + Using it with callbacks or non-coroutine async patterns is + undefined behavior. + + @code + // UNSAFE: No coroutine lifetime guarantee + void bad_callback_pattern(std::vector& data) + { + capy::mutable_buffer buf(data.data(), data.size()); + + // UNSAFE: In a callback model, 'buf' may go out of scope + // before the callback fires. There is no coroutine + // suspension to extend the lifetime. + stream.async_write(buf, [](error_code ec) { + // 'buf' is already destroyed! + }); + } + @endcode + + @par UNSAFE USAGE: Passing to Another Coroutine + + @warning Do not pass `io_buffer_param` to a different coroutine + or spawn a new coroutine that captures it. + + @code + void broken_impl(io_buffer_param p, std::coroutine_handle<> h) + { + // UNSAFE: Spawning a new coroutine that captures 'p'. + // The original coroutine may resume before this new + // coroutine uses 'p'. + co_spawn([p]() -> task { + capy::mutable_buffer bufs[8]; + p.copy_to(bufs, 8); // UNSAFE: original caller may + // have resumed already! + co_return; + }); + } + @endcode + + @par UNSAFE USAGE: Multiple Virtual Hops + + @warning Minimize indirection. Each virtual call that passes + `io_buffer_param` without immediately unrolling it increases + the risk of misuse. + + @code + // Risky: multiple hops before unrolling + void layer1(io_buffer_param p) { + layer2(p); // Still haven't unrolled... + } + void layer2(io_buffer_param p) { + layer3(p); // Still haven't unrolled... + } + void layer3(io_buffer_param p) { + // Finally unrolling, but the chain is fragile. + // Any intermediate layer storing 'p' breaks everything. + } + @endcode + + @par UNSAFE USAGE: Fire-and-Forget Operations + + @warning Do not use with detached or fire-and-forget async + operations where there is no guarantee the caller remains + suspended. + + @code + task caller() + { + char buf[1024]; + // UNSAFE: If async_write is fire-and-forget (doesn't + // actually suspend the caller), 'buf' may be destroyed + // before the I/O completes. + stream.async_write_detached(capy::mutable_buffer(buf, 1024)); + // Returns immediately - 'buf' goes out of scope! + } + @endcode + + @par Passing Convention + + Pass by value. The class contains only two pointers (16 bytes + on 64-bit systems), making copies trivial and clearly + communicating the lightweight, transient nature of this type. + + @code + // Preferred: pass by value + void process(io_buffer_param buffers); + + // Also acceptable: pass by const reference + void process(io_buffer_param const& buffers); + @endcode + + @see capy::ConstBufferSequence, capy::MutableBufferSequence +*/ +class io_buffer_param +{ +public: + /** Construct from a const buffer sequence. + + @param bs The buffer sequence to adapt. + */ + template + io_buffer_param(BS const& bs) noexcept + : bs_(&bs) + , fn_(©_impl) + { + } + + /** Fill an array with buffers from the sequence. + + Copies buffer descriptors from the sequence into the + destination array, skipping any zero-size buffers. + This ensures the output contains only buffers with + actual data, suitable for direct use with system calls. + + @param dest Pointer to array of mutable buffer descriptors. + @param n Maximum number of buffers to copy. + + @return The number of non-zero buffers copied. + */ + std::size_t + copy_to( + capy::mutable_buffer* dest, + std::size_t n) const noexcept + { + return fn_(bs_, dest, n); + } + +private: + template + static std::size_t + copy_impl( + void const* p, + capy::mutable_buffer* dest, + std::size_t n) + { + auto const& bs = *static_cast(p); + auto it = capy::begin(bs); + auto const end_it = capy::end(bs); + + std::size_t i = 0; + if constexpr (capy::MutableBufferSequence) + { + for(; it != end_it && i < n; ++it) + { + capy::mutable_buffer buf(*it); + if(buf.size() == 0) + continue; + dest[i++] = buf; + } + } + else + { + for(; it != end_it && i < n; ++it) + { + capy::const_buffer buf(*it); + if(buf.size() == 0) + continue; + dest[i++] = capy::mutable_buffer( + const_cast( + static_cast(buf.data())), + buf.size()); + } + } + return i; + } + + using fn_t = std::size_t(*)(void const*, + capy::mutable_buffer*, std::size_t); + + void const* bs_; + fn_t fn_; +}; + +} // namespace boost::corosio + +#endif diff --git a/include/boost/corosio/io_context.hpp b/include/boost/corosio/io_context.hpp index 808946d8..6451e107 100644 --- a/include/boost/corosio/io_context.hpp +++ b/include/boost/corosio/io_context.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) // Copyright (c) 2026 Steve Gerbino // // Distributed under the Boost Software License, Version 1.0. (See accompanying diff --git a/include/boost/corosio/io_object.hpp b/include/boost/corosio/io_object.hpp index 95f9c7b3..0f75034d 100644 --- a/include/boost/corosio/io_object.hpp +++ b/include/boost/corosio/io_object.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/io_stream.hpp b/include/boost/corosio/io_stream.hpp index 3d9c0bbe..1326997d 100644 --- a/include/boost/corosio/io_stream.hpp +++ b/include/boost/corosio/io_stream.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/ipv4_address.hpp b/include/boost/corosio/ipv4_address.hpp index e7521da8..40f5815b 100644 --- a/include/boost/corosio/ipv4_address.hpp +++ b/include/boost/corosio/ipv4_address.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/ipv6_address.hpp b/include/boost/corosio/ipv6_address.hpp index 6464c390..fd6d0ea1 100644 --- a/include/boost/corosio/ipv6_address.hpp +++ b/include/boost/corosio/ipv6_address.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/openssl_stream.hpp b/include/boost/corosio/openssl_stream.hpp index dfc4c0dc..1754763e 100644 --- a/include/boost/corosio/openssl_stream.hpp +++ b/include/boost/corosio/openssl_stream.hpp @@ -1,156 +1,159 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_OPENSSL_STREAM_HPP -#define BOOST_COROSIO_OPENSSL_STREAM_HPP - -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace boost::corosio { - -/** A TLS stream using OpenSSL. - - This class wraps an underlying stream satisfying `capy::Stream` - and provides TLS encryption using the OpenSSL library. - - Derives from @ref tls_stream to provide a runtime-polymorphic - interface. The TLS operations are implemented as coroutines - that orchestrate reads and writes on the underlying stream. - - @par Construction Modes - - Two construction modes are supported: - - - **Owning**: Pass stream by value. The openssl_stream takes - ownership and the stream is moved into internal storage. - - - **Reference**: Pass stream by pointer. The openssl_stream - does not own the stream; the caller must ensure the stream - outlives this object. - - @par Thread Safety - Distinct objects: Safe.@n - Shared objects: Unsafe. - - @par Example - @code - tls_context ctx; - ctx.set_hostname("example.com"); - ctx.set_verify_mode(tls_verify_mode::peer); - - corosio::tcp_socket sock(ioc); - co_await sock.connect(endpoint); - - // Reference mode - sock must outlive tls - corosio::openssl_stream tls(&sock, ctx); - auto [ec] = co_await tls.handshake(openssl_stream::client); - - // Or owning mode - tls owns the socket - corosio::openssl_stream tls2(std::move(sock), ctx); - @endcode - - @see tls_stream, wolfssl_stream -*/ -class BOOST_COROSIO_DECL openssl_stream final - : public tls_stream -{ - struct impl; - capy::any_stream stream_; // must be first - impl_ holds reference - impl* impl_; - -public: - /** Construct an OpenSSL stream (owning mode). - - Takes ownership of the underlying stream by moving it into - internal storage. The stream will be destroyed when this - openssl_stream is destroyed. - - @param stream The stream to take ownership of. Must satisfy - `capy::Stream`. - @param ctx The TLS context containing configuration. - */ - template - requires (!std::same_as, openssl_stream>) - openssl_stream(S stream, tls_context ctx) - : stream_(std::move(stream)) - , impl_(make_impl(stream_, ctx)) - { - } - - /** Construct an OpenSSL stream (reference mode). - - Wraps the underlying stream without taking ownership. The - caller must ensure the stream remains valid for the lifetime - of this openssl_stream. - - @param stream Pointer to the stream to wrap. Must satisfy - `capy::Stream`. - @param ctx The TLS context containing configuration. - */ - template - openssl_stream(S* stream, tls_context ctx) - : stream_(stream) - , impl_(make_impl(stream_, ctx)) - { - } - - /** Destructor. - - Releases the underlying OpenSSL resources. If constructed - in owning mode, also destroys the underlying stream. - */ - ~openssl_stream(); - - openssl_stream(openssl_stream&&) noexcept; - openssl_stream& operator=(openssl_stream&&) noexcept; - - capy::io_task<> - handshake(handshake_type type) override; - - capy::io_task<> - shutdown() override; - - capy::any_stream& - next_layer() noexcept override - { - return stream_; - } - - capy::any_stream const& - next_layer() const noexcept override - { - return stream_; - } - - std::string_view - name() const noexcept override; - -protected: - capy::io_task - do_read_some(capy::mutable_buffer_array buffers) override; - - capy::io_task - do_write_some(capy::const_buffer_array buffers) override; - -private: - static impl* - make_impl(capy::any_stream& stream, tls_context const& ctx); -}; - -} // namespace boost::corosio - -#endif +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_OPENSSL_STREAM_HPP +#define BOOST_COROSIO_OPENSSL_STREAM_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace boost::corosio { + +/** A TLS stream using OpenSSL. + + This class wraps an underlying stream satisfying `capy::Stream` + and provides TLS encryption using the OpenSSL library. + + Derives from @ref tls_stream to provide a runtime-polymorphic + interface. The TLS operations are implemented as coroutines + that orchestrate reads and writes on the underlying stream. + + @par Construction Modes + + Two construction modes are supported: + + - **Owning**: Pass stream by value. The openssl_stream takes + ownership and the stream is moved into internal storage. + + - **Reference**: Pass stream by pointer. The openssl_stream + does not own the stream; the caller must ensure the stream + outlives this object. + + @par Thread Safety + Distinct objects: Safe.@n + Shared objects: Unsafe. + + @par Example + @code + tls_context ctx; + ctx.set_hostname("example.com"); + ctx.set_verify_mode(tls_verify_mode::peer); + + corosio::tcp_socket sock(ioc); + co_await sock.connect(endpoint); + + // Reference mode - sock must outlive tls + corosio::openssl_stream tls(&sock, ctx); + auto [ec] = co_await tls.handshake(openssl_stream::client); + + // Or owning mode - tls owns the socket + corosio::openssl_stream tls2(std::move(sock), ctx); + @endcode + + @see tls_stream, wolfssl_stream +*/ +class BOOST_COROSIO_DECL openssl_stream final + : public tls_stream +{ + struct impl; + capy::any_stream stream_; // must be first - impl_ holds reference + impl* impl_; + +public: + /** Construct an OpenSSL stream (owning mode). + + Takes ownership of the underlying stream by moving it into + internal storage. The stream will be destroyed when this + openssl_stream is destroyed. + + @param stream The stream to take ownership of. Must satisfy + `capy::Stream`. + @param ctx The TLS context containing configuration. + */ + template + requires (!std::same_as, openssl_stream>) + openssl_stream(S stream, tls_context ctx) + : stream_(std::move(stream)) + , impl_(make_impl(stream_, ctx)) + { + } + + /** Construct an OpenSSL stream (reference mode). + + Wraps the underlying stream without taking ownership. The + caller must ensure the stream remains valid for the lifetime + of this openssl_stream. + + @param stream Pointer to the stream to wrap. Must satisfy + `capy::Stream`. + @param ctx The TLS context containing configuration. + */ + template + openssl_stream(S* stream, tls_context ctx) + : stream_(stream) + , impl_(make_impl(stream_, ctx)) + { + } + + /** Destructor. + + Releases the underlying OpenSSL resources. If constructed + in owning mode, also destroys the underlying stream. + */ + ~openssl_stream(); + + openssl_stream(openssl_stream&&) noexcept; + openssl_stream& operator=(openssl_stream&&) noexcept; + + capy::io_task<> + handshake(handshake_type type) override; + + capy::io_task<> + shutdown() override; + + void + reset() override; + + capy::any_stream& + next_layer() noexcept override + { + return stream_; + } + + capy::any_stream const& + next_layer() const noexcept override + { + return stream_; + } + + std::string_view + name() const noexcept override; + +protected: + capy::io_task + do_read_some(capy::mutable_buffer_array buffers) override; + + capy::io_task + do_write_some(capy::const_buffer_array buffers) override; + +private: + static impl* + make_impl(capy::any_stream& stream, tls_context const& ctx); +}; + +} // namespace boost::corosio + +#endif diff --git a/include/boost/corosio/resolver.hpp b/include/boost/corosio/resolver.hpp index 37065eab..90b9975d 100644 --- a/include/boost/corosio/resolver.hpp +++ b/include/boost/corosio/resolver.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/resolver_results.hpp b/include/boost/corosio/resolver_results.hpp index d4c42c62..8666462b 100644 --- a/include/boost/corosio/resolver_results.hpp +++ b/include/boost/corosio/resolver_results.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/signal_set.hpp b/include/boost/corosio/signal_set.hpp index 4569e485..33839af6 100644 --- a/include/boost/corosio/signal_set.hpp +++ b/include/boost/corosio/signal_set.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/tcp_acceptor.hpp b/include/boost/corosio/tcp_acceptor.hpp index 1758f6ef..b9acc155 100644 --- a/include/boost/corosio/tcp_acceptor.hpp +++ b/include/boost/corosio/tcp_acceptor.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/tcp_server.hpp b/include/boost/corosio/tcp_server.hpp index 5a4f868b..e5541233 100644 --- a/include/boost/corosio/tcp_server.hpp +++ b/include/boost/corosio/tcp_server.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/tcp_socket.hpp b/include/boost/corosio/tcp_socket.hpp index 272d5908..abd47cb5 100644 --- a/include/boost/corosio/tcp_socket.hpp +++ b/include/boost/corosio/tcp_socket.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/test/mocket.hpp b/include/boost/corosio/test/mocket.hpp index 5f1b8f3d..7479f0ea 100644 --- a/include/boost/corosio/test/mocket.hpp +++ b/include/boost/corosio/test/mocket.hpp @@ -1,451 +1,451 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_TEST_MOCKET_HPP -#define BOOST_COROSIO_TEST_MOCKET_HPP - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace boost::capy { -class execution_context; -} // namespace boost::capy - -namespace boost::corosio::test { - -/** A mock socket for testing I/O operations. - - This class provides a testable socket-like interface where data - can be staged for reading and expected data can be validated on - writes. A mocket is paired with a regular tcp_socket using - @ref make_mocket_pair, allowing bidirectional communication testing. - - When reading, data comes from the `provide()` buffer first. - When writing, data is validated against the `expect()` buffer. - Once buffers are exhausted, I/O passes through to the underlying - socket connection. - - Satisfies the `capy::Stream` concept. - - @par Thread Safety - Not thread-safe. All operations must occur on a single thread. - All coroutines using the mocket must be suspended when calling - `expect()` or `provide()`. - - @see make_mocket_pair -*/ -class BOOST_COROSIO_DECL mocket -{ - tcp_socket sock_; - std::string provide_; - std::string expect_; - capy::test::fuse* fuse_; - std::size_t max_read_size_; - std::size_t max_write_size_; - - template - std::size_t - consume_provide(MutableBufferSequence const& buffers) noexcept; - - template - bool - validate_expect( - ConstBufferSequence const& buffers, - std::size_t& bytes_written); - -public: - template - class read_some_awaitable; - - template - class write_some_awaitable; - - /** Destructor. - */ - ~mocket(); - - /** Construct a mocket. - - @param ctx The execution context for the socket. - @param f The fuse for error injection testing. - @param max_read_size Maximum bytes per read operation. - @param max_write_size Maximum bytes per write operation. - */ - mocket( - capy::execution_context& ctx, - capy::test::fuse& f, - std::size_t max_read_size = std::size_t(-1), - std::size_t max_write_size = std::size_t(-1)); - - /** Move constructor. - */ - mocket(mocket&& other) noexcept; - - /** Move assignment. - */ - mocket& operator=(mocket&& other) noexcept; - - mocket(mocket const&) = delete; - mocket& operator=(mocket const&) = delete; - - /** Return the execution context. - - @return Reference to the execution context that owns this mocket. - */ - capy::execution_context& - context() const noexcept - { - return sock_.context(); - } - - /** Return the underlying socket. - - @return Reference to the underlying tcp_socket. - */ - tcp_socket& - socket() noexcept - { - return sock_; - } - - /** Stage data for reads. - - Appends the given string to this mocket's provide buffer. - When `read_some` is called, it will receive this data first - before reading from the underlying socket. - - @param s The data to provide. - - @pre All coroutines using this mocket must be suspended. - */ - void provide(std::string s); - - /** Set expected data for writes. - - Appends the given string to this mocket's expect buffer. - When the caller writes to this mocket, the written data - must match the expected data. On mismatch, `fuse::fail()` - is called. - - @param s The expected data. - - @pre All coroutines using this mocket must be suspended. - */ - void expect(std::string s); - - /** Close the mocket and verify test expectations. - - Closes the underlying socket and verifies that both the - `expect()` and `provide()` buffers are empty. If either - buffer contains unconsumed data, returns `test_failure` - and calls `fuse::fail()`. - - @return An error code indicating success or failure. - Returns `error::test_failure` if buffers are not empty. - */ - std::error_code close(); - - /** Cancel pending I/O operations. - - Cancels any pending asynchronous operations on the underlying - socket. Outstanding operations complete with `cond::canceled`. - */ - void cancel(); - - /** Check if the mocket is open. - - @return `true` if the mocket is open. - */ - bool is_open() const noexcept; - - /** Initiate an asynchronous read operation. - - Reads available data into the provided buffer sequence. If the - provide buffer has data, it is consumed first. Otherwise, the - operation delegates to the underlying socket. - - @param buffers The buffer sequence to read data into. - - @return An awaitable yielding `(error_code, std::size_t)`. - */ - template - auto read_some(MutableBufferSequence const& buffers) - { - return read_some_awaitable(*this, buffers); - } - - /** Initiate an asynchronous write operation. - - Writes data from the provided buffer sequence. If the expect - buffer has data, it is validated. Otherwise, the operation - delegates to the underlying socket. - - @param buffers The buffer sequence containing data to write. - - @return An awaitable yielding `(error_code, std::size_t)`. - */ - template - auto write_some(ConstBufferSequence const& buffers) - { - return write_some_awaitable(*this, buffers); - } -}; - -//------------------------------------------------------------------------------ - -template -std::size_t -mocket:: -consume_provide(MutableBufferSequence const& buffers) noexcept -{ - auto n = capy::buffer_copy(buffers, capy::make_buffer(provide_), max_read_size_); - provide_.erase(0, n); - return n; -} - -template -bool -mocket:: -validate_expect( - ConstBufferSequence const& buffers, - std::size_t& bytes_written) -{ - if (expect_.empty()) - return true; - - // Build the write data up to max_write_size_ - std::string written; - auto total = capy::buffer_size(buffers); - if (total > max_write_size_) - total = max_write_size_; - written.resize(total); - capy::buffer_copy(capy::make_buffer(written), buffers, max_write_size_); - - // Check if written data matches expect prefix - auto const match_size = (std::min)(written.size(), expect_.size()); - if (std::memcmp(written.data(), expect_.data(), match_size) != 0) - { - fuse_->fail(); - bytes_written = 0; - return false; - } - - // Consume matched portion - expect_.erase(0, match_size); - bytes_written = written.size(); - return true; -} - -//------------------------------------------------------------------------------ - -template -class mocket::read_some_awaitable -{ - using sock_awaitable = - decltype(std::declval().read_some( - std::declval())); - - mocket* m_; - MutableBufferSequence buffers_; - std::size_t n_ = 0; - union { - char dummy_; - sock_awaitable underlying_; - }; - bool sync_ = true; - -public: - read_some_awaitable( - mocket& m, - MutableBufferSequence buffers) noexcept - : m_(&m) - , buffers_(std::move(buffers)) - { - } - - ~read_some_awaitable() - { - if (!sync_) - underlying_.~sock_awaitable(); - } - - read_some_awaitable(read_some_awaitable&& other) noexcept - : m_(other.m_) - , buffers_(std::move(other.buffers_)) - , n_(other.n_) - , sync_(other.sync_) - { - if (!sync_) - { - new (&underlying_) sock_awaitable(std::move(other.underlying_)); - other.underlying_.~sock_awaitable(); - other.sync_ = true; - } - } - - read_some_awaitable(read_some_awaitable const&) = delete; - read_some_awaitable& operator=(read_some_awaitable const&) = delete; - read_some_awaitable& operator=(read_some_awaitable&&) = delete; - - bool await_ready() - { - if (!m_->provide_.empty()) - { - n_ = m_->consume_provide(buffers_); - return true; - } - new (&underlying_) sock_awaitable(m_->sock_.read_some(buffers_)); - sync_ = false; - return underlying_.await_ready(); - } - - template - auto await_suspend(Args&&... args) - { - return underlying_.await_suspend(std::forward(args)...); - } - - capy::io_result await_resume() - { - if (sync_) - return {{}, n_}; - return underlying_.await_resume(); - } -}; - -//------------------------------------------------------------------------------ - -template -class mocket::write_some_awaitable -{ - using sock_awaitable = - decltype(std::declval().write_some( - std::declval())); - - mocket* m_; - ConstBufferSequence buffers_; - std::size_t n_ = 0; - std::error_code ec_; - union { - char dummy_; - sock_awaitable underlying_; - }; - bool sync_ = true; - -public: - write_some_awaitable( - mocket& m, - ConstBufferSequence buffers) noexcept - : m_(&m) - , buffers_(std::move(buffers)) - { - } - - ~write_some_awaitable() - { - if (!sync_) - underlying_.~sock_awaitable(); - } - - write_some_awaitable(write_some_awaitable&& other) noexcept - : m_(other.m_) - , buffers_(std::move(other.buffers_)) - , n_(other.n_) - , ec_(other.ec_) - , sync_(other.sync_) - { - if (!sync_) - { - new (&underlying_) sock_awaitable(std::move(other.underlying_)); - other.underlying_.~sock_awaitable(); - other.sync_ = true; - } - } - - write_some_awaitable(write_some_awaitable const&) = delete; - write_some_awaitable& operator=(write_some_awaitable const&) = delete; - write_some_awaitable& operator=(write_some_awaitable&&) = delete; - - bool await_ready() - { - if (!m_->expect_.empty()) - { - if (!m_->validate_expect(buffers_, n_)) - { - ec_ = capy::error::test_failure; - n_ = 0; - } - return true; - } - new (&underlying_) sock_awaitable(m_->sock_.write_some(buffers_)); - sync_ = false; - return underlying_.await_ready(); - } - - template - auto await_suspend(Args&&... args) - { - return underlying_.await_suspend(std::forward(args)...); - } - - capy::io_result await_resume() - { - if (sync_) - return {ec_, n_}; - return underlying_.await_resume(); - } -}; - -//------------------------------------------------------------------------------ - -/** Create a mocket paired with a socket. - - Creates a mocket and a tcp_socket connected via loopback. - Data written to one can be read from the other. - - The mocket has fuse checks enabled via `maybe_fail()` and - supports provide/expect buffers for test instrumentation. - The tcp_socket is the "peer" end with no test instrumentation. - - Optional max_read_size and max_write_size parameters limit the - number of bytes transferred per I/O operation on the mocket, - simulating chunked network delivery for testing purposes. - - @param ctx The execution context for the sockets. - @param f The fuse for error injection testing. - @param max_read_size Maximum bytes per read operation (default unlimited). - @param max_write_size Maximum bytes per write operation (default unlimited). - - @return A pair of (mocket, tcp_socket). - - @note Mockets are not thread-safe and must be used in a - single-threaded, deterministic context. -*/ -BOOST_COROSIO_DECL -std::pair -make_mocket_pair( - capy::execution_context& ctx, - capy::test::fuse& f, - std::size_t max_read_size = std::size_t(-1), - std::size_t max_write_size = std::size_t(-1)); - -} // namespace boost::corosio::test - -#endif +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_TEST_MOCKET_HPP +#define BOOST_COROSIO_TEST_MOCKET_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace boost::capy { +class execution_context; +} // namespace boost::capy + +namespace boost::corosio::test { + +/** A mock socket for testing I/O operations. + + This class provides a testable socket-like interface where data + can be staged for reading and expected data can be validated on + writes. A mocket is paired with a regular tcp_socket using + @ref make_mocket_pair, allowing bidirectional communication testing. + + When reading, data comes from the `provide()` buffer first. + When writing, data is validated against the `expect()` buffer. + Once buffers are exhausted, I/O passes through to the underlying + socket connection. + + Satisfies the `capy::Stream` concept. + + @par Thread Safety + Not thread-safe. All operations must occur on a single thread. + All coroutines using the mocket must be suspended when calling + `expect()` or `provide()`. + + @see make_mocket_pair +*/ +class BOOST_COROSIO_DECL mocket +{ + tcp_socket sock_; + std::string provide_; + std::string expect_; + capy::test::fuse fuse_; + std::size_t max_read_size_; + std::size_t max_write_size_; + + template + std::size_t + consume_provide(MutableBufferSequence const& buffers) noexcept; + + template + bool + validate_expect( + ConstBufferSequence const& buffers, + std::size_t& bytes_written); + +public: + template + class read_some_awaitable; + + template + class write_some_awaitable; + + /** Destructor. + */ + ~mocket(); + + /** Construct a mocket. + + @param ctx The execution context for the socket. + @param f The fuse for error injection testing. + @param max_read_size Maximum bytes per read operation. + @param max_write_size Maximum bytes per write operation. + */ + mocket( + capy::execution_context& ctx, + capy::test::fuse f = {}, + std::size_t max_read_size = std::size_t(-1), + std::size_t max_write_size = std::size_t(-1)); + + /** Move constructor. + */ + mocket(mocket&& other) noexcept; + + /** Move assignment. + */ + mocket& operator=(mocket&& other) noexcept; + + mocket(mocket const&) = delete; + mocket& operator=(mocket const&) = delete; + + /** Return the execution context. + + @return Reference to the execution context that owns this mocket. + */ + capy::execution_context& + context() const noexcept + { + return sock_.context(); + } + + /** Return the underlying socket. + + @return Reference to the underlying tcp_socket. + */ + tcp_socket& + socket() noexcept + { + return sock_; + } + + /** Stage data for reads. + + Appends the given string to this mocket's provide buffer. + When `read_some` is called, it will receive this data first + before reading from the underlying socket. + + @param s The data to provide. + + @pre All coroutines using this mocket must be suspended. + */ + void provide(std::string s); + + /** Set expected data for writes. + + Appends the given string to this mocket's expect buffer. + When the caller writes to this mocket, the written data + must match the expected data. On mismatch, `fuse::fail()` + is called. + + @param s The expected data. + + @pre All coroutines using this mocket must be suspended. + */ + void expect(std::string s); + + /** Close the mocket and verify test expectations. + + Closes the underlying socket and verifies that both the + `expect()` and `provide()` buffers are empty. If either + buffer contains unconsumed data, returns `test_failure` + and calls `fuse::fail()`. + + @return An error code indicating success or failure. + Returns `error::test_failure` if buffers are not empty. + */ + std::error_code close(); + + /** Cancel pending I/O operations. + + Cancels any pending asynchronous operations on the underlying + socket. Outstanding operations complete with `cond::canceled`. + */ + void cancel(); + + /** Check if the mocket is open. + + @return `true` if the mocket is open. + */ + bool is_open() const noexcept; + + /** Initiate an asynchronous read operation. + + Reads available data into the provided buffer sequence. If the + provide buffer has data, it is consumed first. Otherwise, the + operation delegates to the underlying socket. + + @param buffers The buffer sequence to read data into. + + @return An awaitable yielding `(error_code, std::size_t)`. + */ + template + auto read_some(MutableBufferSequence const& buffers) + { + return read_some_awaitable(*this, buffers); + } + + /** Initiate an asynchronous write operation. + + Writes data from the provided buffer sequence. If the expect + buffer has data, it is validated. Otherwise, the operation + delegates to the underlying socket. + + @param buffers The buffer sequence containing data to write. + + @return An awaitable yielding `(error_code, std::size_t)`. + */ + template + auto write_some(ConstBufferSequence const& buffers) + { + return write_some_awaitable(*this, buffers); + } +}; + +//------------------------------------------------------------------------------ + +template +std::size_t +mocket:: +consume_provide(MutableBufferSequence const& buffers) noexcept +{ + auto n = capy::buffer_copy(buffers, capy::make_buffer(provide_), max_read_size_); + provide_.erase(0, n); + return n; +} + +template +bool +mocket:: +validate_expect( + ConstBufferSequence const& buffers, + std::size_t& bytes_written) +{ + if (expect_.empty()) + return true; + + // Build the write data up to max_write_size_ + std::string written; + auto total = capy::buffer_size(buffers); + if (total > max_write_size_) + total = max_write_size_; + written.resize(total); + capy::buffer_copy(capy::make_buffer(written), buffers, max_write_size_); + + // Check if written data matches expect prefix + auto const match_size = (std::min)(written.size(), expect_.size()); + if (std::memcmp(written.data(), expect_.data(), match_size) != 0) + { + fuse_.fail(); + bytes_written = 0; + return false; + } + + // Consume matched portion + expect_.erase(0, match_size); + bytes_written = written.size(); + return true; +} + +//------------------------------------------------------------------------------ + +template +class mocket::read_some_awaitable +{ + using sock_awaitable = + decltype(std::declval().read_some( + std::declval())); + + mocket* m_; + MutableBufferSequence buffers_; + std::size_t n_ = 0; + union { + char dummy_; + sock_awaitable underlying_; + }; + bool sync_ = true; + +public: + read_some_awaitable( + mocket& m, + MutableBufferSequence buffers) noexcept + : m_(&m) + , buffers_(std::move(buffers)) + { + } + + ~read_some_awaitable() + { + if (!sync_) + underlying_.~sock_awaitable(); + } + + read_some_awaitable(read_some_awaitable&& other) noexcept + : m_(other.m_) + , buffers_(std::move(other.buffers_)) + , n_(other.n_) + , sync_(other.sync_) + { + if (!sync_) + { + new (&underlying_) sock_awaitable(std::move(other.underlying_)); + other.underlying_.~sock_awaitable(); + other.sync_ = true; + } + } + + read_some_awaitable(read_some_awaitable const&) = delete; + read_some_awaitable& operator=(read_some_awaitable const&) = delete; + read_some_awaitable& operator=(read_some_awaitable&&) = delete; + + bool await_ready() + { + if (!m_->provide_.empty()) + { + n_ = m_->consume_provide(buffers_); + return true; + } + new (&underlying_) sock_awaitable(m_->sock_.read_some(buffers_)); + sync_ = false; + return underlying_.await_ready(); + } + + template + auto await_suspend(Args&&... args) + { + return underlying_.await_suspend(std::forward(args)...); + } + + capy::io_result await_resume() + { + if (sync_) + return {{}, n_}; + return underlying_.await_resume(); + } +}; + +//------------------------------------------------------------------------------ + +template +class mocket::write_some_awaitable +{ + using sock_awaitable = + decltype(std::declval().write_some( + std::declval())); + + mocket* m_; + ConstBufferSequence buffers_; + std::size_t n_ = 0; + std::error_code ec_; + union { + char dummy_; + sock_awaitable underlying_; + }; + bool sync_ = true; + +public: + write_some_awaitable( + mocket& m, + ConstBufferSequence buffers) noexcept + : m_(&m) + , buffers_(std::move(buffers)) + { + } + + ~write_some_awaitable() + { + if (!sync_) + underlying_.~sock_awaitable(); + } + + write_some_awaitable(write_some_awaitable&& other) noexcept + : m_(other.m_) + , buffers_(std::move(other.buffers_)) + , n_(other.n_) + , ec_(other.ec_) + , sync_(other.sync_) + { + if (!sync_) + { + new (&underlying_) sock_awaitable(std::move(other.underlying_)); + other.underlying_.~sock_awaitable(); + other.sync_ = true; + } + } + + write_some_awaitable(write_some_awaitable const&) = delete; + write_some_awaitable& operator=(write_some_awaitable const&) = delete; + write_some_awaitable& operator=(write_some_awaitable&&) = delete; + + bool await_ready() + { + if (!m_->expect_.empty()) + { + if (!m_->validate_expect(buffers_, n_)) + { + ec_ = capy::error::test_failure; + n_ = 0; + } + return true; + } + new (&underlying_) sock_awaitable(m_->sock_.write_some(buffers_)); + sync_ = false; + return underlying_.await_ready(); + } + + template + auto await_suspend(Args&&... args) + { + return underlying_.await_suspend(std::forward(args)...); + } + + capy::io_result await_resume() + { + if (sync_) + return {ec_, n_}; + return underlying_.await_resume(); + } +}; + +//------------------------------------------------------------------------------ + +/** Create a mocket paired with a socket. + + Creates a mocket and a tcp_socket connected via loopback. + Data written to one can be read from the other. + + The mocket has fuse checks enabled via `maybe_fail()` and + supports provide/expect buffers for test instrumentation. + The tcp_socket is the "peer" end with no test instrumentation. + + Optional max_read_size and max_write_size parameters limit the + number of bytes transferred per I/O operation on the mocket, + simulating chunked network delivery for testing purposes. + + @param ctx The execution context for the sockets. + @param f The fuse for error injection testing. + @param max_read_size Maximum bytes per read operation (default unlimited). + @param max_write_size Maximum bytes per write operation (default unlimited). + + @return A pair of (mocket, tcp_socket). + + @note Mockets are not thread-safe and must be used in a + single-threaded, deterministic context. +*/ +BOOST_COROSIO_DECL +std::pair +make_mocket_pair( + capy::execution_context& ctx, + capy::test::fuse f = {}, + std::size_t max_read_size = std::size_t(-1), + std::size_t max_write_size = std::size_t(-1)); + +} // namespace boost::corosio::test + +#endif diff --git a/include/boost/corosio/test/socket_pair.hpp b/include/boost/corosio/test/socket_pair.hpp index 5dc6e222..19e37445 100644 --- a/include/boost/corosio/test/socket_pair.hpp +++ b/include/boost/corosio/test/socket_pair.hpp @@ -1,36 +1,36 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_TEST_SOCKET_PAIR_HPP -#define BOOST_COROSIO_TEST_SOCKET_PAIR_HPP - -#include -#include -#include - -#include - -namespace boost::corosio::test { - -/** Create a connected pair of sockets. - - Creates two sockets connected via loopback TCP sockets. - Data written to one socket can be read from the other. - - @param ctx The I/O context for the sockets. - - @return A pair of connected sockets. -*/ -BOOST_COROSIO_DECL -std::pair -make_socket_pair(basic_io_context& ctx); - -} // namespace boost::corosio::test - -#endif +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_TEST_SOCKET_PAIR_HPP +#define BOOST_COROSIO_TEST_SOCKET_PAIR_HPP + +#include +#include +#include + +#include + +namespace boost::corosio::test { + +/** Create a connected pair of sockets. + + Creates two sockets connected via loopback TCP sockets. + Data written to one socket can be read from the other. + + @param ctx The I/O context for the sockets. + + @return A pair of connected sockets. +*/ +BOOST_COROSIO_DECL +std::pair +make_socket_pair(basic_io_context& ctx); + +} // namespace boost::corosio::test + +#endif diff --git a/include/boost/corosio/timer.hpp b/include/boost/corosio/timer.hpp index f443dd4b..3250d1ab 100644 --- a/include/boost/corosio/timer.hpp +++ b/include/boost/corosio/timer.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/tls_context.hpp b/include/boost/corosio/tls_context.hpp index bccb96fe..128b4ff3 100644 --- a/include/boost/corosio/tls_context.hpp +++ b/include/boost/corosio/tls_context.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/include/boost/corosio/tls_stream.hpp b/include/boost/corosio/tls_stream.hpp index 7c3f9c4c..d2b69df3 100644 --- a/include/boost/corosio/tls_stream.hpp +++ b/include/boost/corosio/tls_stream.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) @@ -124,6 +124,32 @@ class BOOST_COROSIO_DECL tls_stream virtual capy::io_task<> shutdown() = 0; + /** Reset TLS session state for reuse. + + Releases TLS session state including session keys and peer + certificates, returning the stream to a state where + `handshake()` can be called again. Internal memory + allocations (I/O buffers) are preserved. + + Calling `handshake()` on a previously-used stream + implicitly performs a reset first, so explicit calls + are only needed to eagerly release session state. + + @par Preconditions + No TLS operation (handshake, read, write, shutdown) is + in progress. + + @par Thread Safety + Not thread safe. The caller must ensure no concurrent + operations are in progress on this stream. + + @note If called mid-session before `shutdown()`, pending + TLS data is discarded and the peer will observe a + truncated stream. + */ + virtual void + reset() = 0; + /** Returns a reference to the underlying stream. Provides access to the type-erased underlying stream for diff --git a/include/boost/corosio/wolfssl_stream.hpp b/include/boost/corosio/wolfssl_stream.hpp index 1ff558c8..a728ee1f 100644 --- a/include/boost/corosio/wolfssl_stream.hpp +++ b/include/boost/corosio/wolfssl_stream.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) @@ -124,6 +124,9 @@ class BOOST_COROSIO_DECL wolfssl_stream final capy::io_task<> shutdown() override; + void + reset() override; + capy::any_stream& next_layer() noexcept override { diff --git a/perf/profile/concurrent_io_bench.cpp b/perf/profile/concurrent_io_bench.cpp index fdd88b4c..2c68df16 100644 --- a/perf/profile/concurrent_io_bench.cpp +++ b/perf/profile/concurrent_io_bench.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/perf/profile/coroutine_post_bench.cpp b/perf/profile/coroutine_post_bench.cpp index a46ef663..511b0dbc 100644 --- a/perf/profile/coroutine_post_bench.cpp +++ b/perf/profile/coroutine_post_bench.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/perf/profile/queue_depth_bench.cpp b/perf/profile/queue_depth_bench.cpp index c86d05ac..1d8e2ca7 100644 --- a/perf/profile/queue_depth_bench.cpp +++ b/perf/profile/queue_depth_bench.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/perf/profile/scheduler_contention_bench.cpp b/perf/profile/scheduler_contention_bench.cpp index 833a99ea..1652db52 100644 --- a/perf/profile/scheduler_contention_bench.cpp +++ b/perf/profile/scheduler_contention_bench.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/perf/profile/small_io_bench.cpp b/perf/profile/small_io_bench.cpp index f8930fdc..e3835edb 100644 --- a/perf/profile/small_io_bench.cpp +++ b/perf/profile/small_io_bench.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/endpoint_convert.hpp b/src/corosio/src/detail/endpoint_convert.hpp index e31e3078..e3064a99 100644 --- a/src/corosio/src/detail/endpoint_convert.hpp +++ b/src/corosio/src/detail/endpoint_convert.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/intrusive.hpp b/src/corosio/src/detail/intrusive.hpp index 9a7d5c2e..02c39bb5 100644 --- a/src/corosio/src/detail/intrusive.hpp +++ b/src/corosio/src/detail/intrusive.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/completion_key.hpp b/src/corosio/src/detail/iocp/completion_key.hpp index f5855ff4..b20d5036 100644 --- a/src/corosio/src/detail/iocp/completion_key.hpp +++ b/src/corosio/src/detail/iocp/completion_key.hpp @@ -1,54 +1,54 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_DETAIL_IOCP_COMPLETION_KEY_HPP -#define BOOST_COROSIO_DETAIL_IOCP_COMPLETION_KEY_HPP - -#include - -#if BOOST_COROSIO_HAS_IOCP - -#include "src/detail/iocp/windows.hpp" - -namespace boost::corosio::detail { - -/** IOCP completion key values. - - These integer values are used as the completion key parameter - when calling CreateIoCompletionPort and PostQueuedCompletionStatus. - The run loop dispatches based on these values using a switch. - - All I/O handles are registered with key_io (0), and dispatch - happens via the function pointer in the overlapped_op structure. - The other keys are for internal scheduler signals. -*/ -enum completion_key : ULONG_PTR -{ - /** I/O operation completed. OVERLAPPED* points to overlapped_op. */ - key_io = 0, - - /** Timer or deferred operation wakeup signal. */ - key_wake_dispatch = 1, - - /** Scheduler stop/shutdown signal. */ - key_shutdown = 2, - - /** Operation completed with results pre-stored in OVERLAPPED fields. - Used when posting completions after synchronous completion. */ - key_result_stored = 3, - - /** Posted scheduler_op*. OVERLAPPED* is actually a scheduler_op*. */ - key_posted = 4 -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_IOCP - -#endif // BOOST_COROSIO_DETAIL_IOCP_COMPLETION_KEY_HPP +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_IOCP_COMPLETION_KEY_HPP +#define BOOST_COROSIO_DETAIL_IOCP_COMPLETION_KEY_HPP + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include "src/detail/iocp/windows.hpp" + +namespace boost::corosio::detail { + +/** IOCP completion key values. + + These integer values are used as the completion key parameter + when calling CreateIoCompletionPort and PostQueuedCompletionStatus. + The run loop dispatches based on these values using a switch. + + All I/O handles are registered with key_io (0), and dispatch + happens via the function pointer in the overlapped_op structure. + The other keys are for internal scheduler signals. +*/ +enum completion_key : ULONG_PTR +{ + /** I/O operation completed. OVERLAPPED* points to overlapped_op. */ + key_io = 0, + + /** Timer or deferred operation wakeup signal. */ + key_wake_dispatch = 1, + + /** Scheduler stop/shutdown signal. */ + key_shutdown = 2, + + /** Operation completed with results pre-stored in OVERLAPPED fields. + Used when posting completions after synchronous completion. */ + key_result_stored = 3, + + /** Posted scheduler_op*. OVERLAPPED* is actually a scheduler_op*. */ + key_posted = 4 +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_IOCP + +#endif // BOOST_COROSIO_DETAIL_IOCP_COMPLETION_KEY_HPP diff --git a/src/corosio/src/detail/iocp/mutex.hpp b/src/corosio/src/detail/iocp/mutex.hpp index 95254c18..5740bcbe 100644 --- a/src/corosio/src/detail/iocp/mutex.hpp +++ b/src/corosio/src/detail/iocp/mutex.hpp @@ -1,74 +1,74 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_DETAIL_IOCP_MUTEX_HPP -#define BOOST_COROSIO_DETAIL_IOCP_MUTEX_HPP - -#include - -#if BOOST_COROSIO_HAS_IOCP - -#include - -#include "src/detail/iocp/windows.hpp" - -namespace boost::corosio::detail { - -/** Recursive mutex using Windows CRITICAL_SECTION. - - This mutex can be locked multiple times by the same thread. - Each call to `lock()` or successful `try_lock()` must be - balanced by a corresponding call to `unlock()`. - - Satisfies the Lockable named requirement and is compatible - with `std::lock_guard`, `std::unique_lock`, and `std::scoped_lock`. -*/ -class win_mutex -{ -public: - win_mutex() - { - ::InitializeCriticalSectionAndSpinCount(&cs_, 0x80000000); - } - - ~win_mutex() - { - ::DeleteCriticalSection(&cs_); - } - - win_mutex(win_mutex const&) = delete; - win_mutex& operator=(win_mutex const&) = delete; - - void - lock() noexcept - { - ::EnterCriticalSection(&cs_); - } - - void - unlock() noexcept - { - ::LeaveCriticalSection(&cs_); - } - - bool - try_lock() noexcept - { - return ::TryEnterCriticalSection(&cs_) != 0; - } - -private: - ::CRITICAL_SECTION cs_; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_IOCP - -#endif // BOOST_COROSIO_DETAIL_IOCP_MUTEX_HPP +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_IOCP_MUTEX_HPP +#define BOOST_COROSIO_DETAIL_IOCP_MUTEX_HPP + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include + +#include "src/detail/iocp/windows.hpp" + +namespace boost::corosio::detail { + +/** Recursive mutex using Windows CRITICAL_SECTION. + + This mutex can be locked multiple times by the same thread. + Each call to `lock()` or successful `try_lock()` must be + balanced by a corresponding call to `unlock()`. + + Satisfies the Lockable named requirement and is compatible + with `std::lock_guard`, `std::unique_lock`, and `std::scoped_lock`. +*/ +class win_mutex +{ +public: + win_mutex() + { + ::InitializeCriticalSectionAndSpinCount(&cs_, 0x80000000); + } + + ~win_mutex() + { + ::DeleteCriticalSection(&cs_); + } + + win_mutex(win_mutex const&) = delete; + win_mutex& operator=(win_mutex const&) = delete; + + void + lock() noexcept + { + ::EnterCriticalSection(&cs_); + } + + void + unlock() noexcept + { + ::LeaveCriticalSection(&cs_); + } + + bool + try_lock() noexcept + { + return ::TryEnterCriticalSection(&cs_) != 0; + } + +private: + ::CRITICAL_SECTION cs_; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_IOCP + +#endif // BOOST_COROSIO_DETAIL_IOCP_MUTEX_HPP diff --git a/src/corosio/src/detail/iocp/overlapped_op.hpp b/src/corosio/src/detail/iocp/overlapped_op.hpp index 1478f46d..37c18ffe 100644 --- a/src/corosio/src/detail/iocp/overlapped_op.hpp +++ b/src/corosio/src/detail/iocp/overlapped_op.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/resolver_service.cpp b/src/corosio/src/detail/iocp/resolver_service.cpp index 4b777bcb..ea90ef41 100644 --- a/src/corosio/src/detail/iocp/resolver_service.cpp +++ b/src/corosio/src/detail/iocp/resolver_service.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/resolver_service.hpp b/src/corosio/src/detail/iocp/resolver_service.hpp index 268fcd89..0af26ccf 100644 --- a/src/corosio/src/detail/iocp/resolver_service.hpp +++ b/src/corosio/src/detail/iocp/resolver_service.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/scheduler.cpp b/src/corosio/src/detail/iocp/scheduler.cpp index b6445e0c..11dfb8df 100644 --- a/src/corosio/src/detail/iocp/scheduler.cpp +++ b/src/corosio/src/detail/iocp/scheduler.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/scheduler.hpp b/src/corosio/src/detail/iocp/scheduler.hpp index 2d3f1f0a..5a3b85cc 100644 --- a/src/corosio/src/detail/iocp/scheduler.hpp +++ b/src/corosio/src/detail/iocp/scheduler.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/signals.cpp b/src/corosio/src/detail/iocp/signals.cpp index 6daadbdc..9b510fa4 100644 --- a/src/corosio/src/detail/iocp/signals.cpp +++ b/src/corosio/src/detail/iocp/signals.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/signals.hpp b/src/corosio/src/detail/iocp/signals.hpp index eaa5c974..b87ec21f 100644 --- a/src/corosio/src/detail/iocp/signals.hpp +++ b/src/corosio/src/detail/iocp/signals.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/sockets.cpp b/src/corosio/src/detail/iocp/sockets.cpp index d0cb468a..11fde301 100644 --- a/src/corosio/src/detail/iocp/sockets.cpp +++ b/src/corosio/src/detail/iocp/sockets.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/sockets.hpp b/src/corosio/src/detail/iocp/sockets.hpp index 12866582..28900078 100644 --- a/src/corosio/src/detail/iocp/sockets.hpp +++ b/src/corosio/src/detail/iocp/sockets.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/timers.cpp b/src/corosio/src/detail/iocp/timers.cpp index 71cb7226..094c0ac0 100644 --- a/src/corosio/src/detail/iocp/timers.cpp +++ b/src/corosio/src/detail/iocp/timers.cpp @@ -1,36 +1,36 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include - -#if BOOST_COROSIO_HAS_IOCP - -#include "src/detail/iocp/timers.hpp" -#include "src/detail/iocp/timers_nt.hpp" -#include "src/detail/iocp/timers_thread.hpp" - -namespace boost::corosio::detail { - -std::unique_ptr -make_win_timers(void* iocp, long* dispatch_required) -{ - // Thread-based is faster; NT API requires one-shot re-association per - // wakeup which tanks performance. See timers_nt.cpp for details. - return std::make_unique(iocp, dispatch_required); - -#if 0 - // NT native API (Windows 8+) - if (auto p = win_timers_nt::try_create(iocp, dispatch_required)) - return p; -#endif -} - -} // namespace boost::corosio::detail - -#endif +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include "src/detail/iocp/timers.hpp" +#include "src/detail/iocp/timers_nt.hpp" +#include "src/detail/iocp/timers_thread.hpp" + +namespace boost::corosio::detail { + +std::unique_ptr +make_win_timers(void* iocp, long* dispatch_required) +{ + // Thread-based is faster; NT API requires one-shot re-association per + // wakeup which tanks performance. See timers_nt.cpp for details. + return std::make_unique(iocp, dispatch_required); + +#if 0 + // NT native API (Windows 8+) + if (auto p = win_timers_nt::try_create(iocp, dispatch_required)) + return p; +#endif +} + +} // namespace boost::corosio::detail + +#endif diff --git a/src/corosio/src/detail/iocp/timers.hpp b/src/corosio/src/detail/iocp/timers.hpp index b485716f..01924835 100644 --- a/src/corosio/src/detail/iocp/timers.hpp +++ b/src/corosio/src/detail/iocp/timers.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/timers_none.hpp b/src/corosio/src/detail/iocp/timers_none.hpp index a4880e4f..741146ec 100644 --- a/src/corosio/src/detail/iocp/timers_none.hpp +++ b/src/corosio/src/detail/iocp/timers_none.hpp @@ -1,37 +1,37 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_DETAIL_IOCP_TIMERS_NONE_HPP -#define BOOST_COROSIO_DETAIL_IOCP_TIMERS_NONE_HPP - -#include - -#if BOOST_COROSIO_HAS_IOCP - -#include "src/detail/iocp/timers.hpp" - -namespace boost::corosio::detail { - -// No-op timer wakeup for debugging/disabling timer support. -// Not automatically selected by make_win_timers. -class win_timers_none final : public win_timers -{ -public: - win_timers_none() = default; - - void start() override {} - void stop() override {} - void update_timeout(time_point) override {} -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_IOCP - -#endif // BOOST_COROSIO_DETAIL_IOCP_TIMERS_NONE_HPP +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_IOCP_TIMERS_NONE_HPP +#define BOOST_COROSIO_DETAIL_IOCP_TIMERS_NONE_HPP + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include "src/detail/iocp/timers.hpp" + +namespace boost::corosio::detail { + +// No-op timer wakeup for debugging/disabling timer support. +// Not automatically selected by make_win_timers. +class win_timers_none final : public win_timers +{ +public: + win_timers_none() = default; + + void start() override {} + void stop() override {} + void update_timeout(time_point) override {} +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_IOCP + +#endif // BOOST_COROSIO_DETAIL_IOCP_TIMERS_NONE_HPP diff --git a/src/corosio/src/detail/iocp/timers_nt.cpp b/src/corosio/src/detail/iocp/timers_nt.cpp index ac1bd75b..593a8c53 100644 --- a/src/corosio/src/detail/iocp/timers_nt.cpp +++ b/src/corosio/src/detail/iocp/timers_nt.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/timers_nt.hpp b/src/corosio/src/detail/iocp/timers_nt.hpp index b14f6f5c..489174bd 100644 --- a/src/corosio/src/detail/iocp/timers_nt.hpp +++ b/src/corosio/src/detail/iocp/timers_nt.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/timers_thread.cpp b/src/corosio/src/detail/iocp/timers_thread.cpp index fab2e47f..58fb6ea1 100644 --- a/src/corosio/src/detail/iocp/timers_thread.cpp +++ b/src/corosio/src/detail/iocp/timers_thread.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/timers_thread.hpp b/src/corosio/src/detail/iocp/timers_thread.hpp index 871f3d55..2956969a 100644 --- a/src/corosio/src/detail/iocp/timers_thread.hpp +++ b/src/corosio/src/detail/iocp/timers_thread.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/windows.hpp b/src/corosio/src/detail/iocp/windows.hpp index 34ec5c5c..e85b182b 100644 --- a/src/corosio/src/detail/iocp/windows.hpp +++ b/src/corosio/src/detail/iocp/windows.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/iocp/wsa_init.cpp b/src/corosio/src/detail/iocp/wsa_init.cpp index 73c56f86..9068ac23 100644 --- a/src/corosio/src/detail/iocp/wsa_init.cpp +++ b/src/corosio/src/detail/iocp/wsa_init.cpp @@ -1,45 +1,45 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include - -#if BOOST_COROSIO_HAS_IOCP - -#include "src/detail/iocp/wsa_init.hpp" -#include "src/detail/make_err.hpp" - -#include - -namespace boost::corosio::detail { - -long win_wsa_init::count_ = 0; - -win_wsa_init::win_wsa_init() -{ - if (::InterlockedIncrement(&count_) == 1) - { - WSADATA wsaData; - int result = ::WSAStartup(MAKEWORD(2, 2), &wsaData); - if (result != 0) - { - ::InterlockedDecrement(&count_); - throw_system_error(make_err(result)); - } - } -} - -win_wsa_init::~win_wsa_init() -{ - if (::InterlockedDecrement(&count_) == 0) - ::WSACleanup(); -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_IOCP +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include "src/detail/iocp/wsa_init.hpp" +#include "src/detail/make_err.hpp" + +#include + +namespace boost::corosio::detail { + +long win_wsa_init::count_ = 0; + +win_wsa_init::win_wsa_init() +{ + if (::InterlockedIncrement(&count_) == 1) + { + WSADATA wsaData; + int result = ::WSAStartup(MAKEWORD(2, 2), &wsaData); + if (result != 0) + { + ::InterlockedDecrement(&count_); + throw_system_error(make_err(result)); + } + } +} + +win_wsa_init::~win_wsa_init() +{ + if (::InterlockedDecrement(&count_) == 0) + ::WSACleanup(); +} + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_IOCP diff --git a/src/corosio/src/detail/iocp/wsa_init.hpp b/src/corosio/src/detail/iocp/wsa_init.hpp index 3d05e809..f83955ef 100644 --- a/src/corosio/src/detail/iocp/wsa_init.hpp +++ b/src/corosio/src/detail/iocp/wsa_init.hpp @@ -1,48 +1,48 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_DETAIL_IOCP_WSA_INIT_HPP -#define BOOST_COROSIO_DETAIL_IOCP_WSA_INIT_HPP - -#include - -#if BOOST_COROSIO_HAS_IOCP - -#include - -#include "src/detail/iocp/windows.hpp" - -namespace boost::corosio::detail { - -/** RAII class for Winsock initialization. - - Uses reference counting to ensure WSAStartup is called once on - first construction and WSACleanup on last destruction. - - Derive from this class to ensure Winsock is initialized before - any socket operations. -*/ -class win_wsa_init -{ -protected: - win_wsa_init(); - ~win_wsa_init(); - - win_wsa_init(win_wsa_init const&) = delete; - win_wsa_init& operator=(win_wsa_init const&) = delete; - -private: - static long count_; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_IOCP - -#endif // BOOST_COROSIO_DETAIL_IOCP_WSA_INIT_HPP +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_IOCP_WSA_INIT_HPP +#define BOOST_COROSIO_DETAIL_IOCP_WSA_INIT_HPP + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include + +#include "src/detail/iocp/windows.hpp" + +namespace boost::corosio::detail { + +/** RAII class for Winsock initialization. + + Uses reference counting to ensure WSAStartup is called once on + first construction and WSACleanup on last destruction. + + Derive from this class to ensure Winsock is initialized before + any socket operations. +*/ +class win_wsa_init +{ +protected: + win_wsa_init(); + ~win_wsa_init(); + + win_wsa_init(win_wsa_init const&) = delete; + win_wsa_init& operator=(win_wsa_init const&) = delete; + +private: + static long count_; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_IOCP + +#endif // BOOST_COROSIO_DETAIL_IOCP_WSA_INIT_HPP diff --git a/src/corosio/src/detail/make_err.cpp b/src/corosio/src/detail/make_err.cpp index 54654c80..164cb198 100644 --- a/src/corosio/src/detail/make_err.cpp +++ b/src/corosio/src/detail/make_err.cpp @@ -1,61 +1,61 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include "src/detail/make_err.hpp" - -#include - -#if BOOST_COROSIO_POSIX -#include -#else -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#endif - -namespace boost::corosio::detail { - -#if BOOST_COROSIO_POSIX - -std::error_code -make_err(int errn) noexcept -{ - if (errn == 0) - return {}; - - if (errn == ECANCELED) - return capy::error::canceled; - - return std::error_code(errn, std::system_category()); -} - -#else - -std::error_code -make_err(unsigned long dwError) noexcept -{ - if (dwError == 0) - return {}; - - if (dwError == ERROR_OPERATION_ABORTED || - dwError == ERROR_CANCELLED) - return capy::error::canceled; - - if (dwError == ERROR_HANDLE_EOF) - return capy::error::eof; - - return std::error_code( - static_cast(dwError), - std::system_category()); -} - -#endif - -} // namespace boost::corosio::detail +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include "src/detail/make_err.hpp" + +#include + +#if BOOST_COROSIO_POSIX +#include +#else +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +namespace boost::corosio::detail { + +#if BOOST_COROSIO_POSIX + +std::error_code +make_err(int errn) noexcept +{ + if (errn == 0) + return {}; + + if (errn == ECANCELED) + return capy::error::canceled; + + return std::error_code(errn, std::system_category()); +} + +#else + +std::error_code +make_err(unsigned long dwError) noexcept +{ + if (dwError == 0) + return {}; + + if (dwError == ERROR_OPERATION_ABORTED || + dwError == ERROR_CANCELLED) + return capy::error::canceled; + + if (dwError == ERROR_HANDLE_EOF) + return capy::error::eof; + + return std::error_code( + static_cast(dwError), + std::system_category()); +} + +#endif + +} // namespace boost::corosio::detail diff --git a/src/corosio/src/detail/make_err.hpp b/src/corosio/src/detail/make_err.hpp index e02270ef..da3b59ca 100644 --- a/src/corosio/src/detail/make_err.hpp +++ b/src/corosio/src/detail/make_err.hpp @@ -1,42 +1,42 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef SRC_DETAIL_MAKE_ERR_HPP -#define SRC_DETAIL_MAKE_ERR_HPP - -#include -#include -#include - -namespace boost::corosio::detail { - -#if BOOST_COROSIO_HAS_IOCP -/** Convert a Windows error code to std::error_code. - - Maps ERROR_OPERATION_ABORTED and ERROR_CANCELLED to capy::error::canceled. - Maps ERROR_HANDLE_EOF to capy::error::eof. - - @param dwError The Windows error code (DWORD). - @return The corresponding std::error_code. -*/ -std::error_code make_err(unsigned long dwError) noexcept; -#else -/** Convert a POSIX errno value to std::error_code. - - Maps ECANCELED to capy::error::canceled. - - @param errn The errno value. - @return The corresponding std::error_code. -*/ -std::error_code make_err(int errn) noexcept; -#endif - -} // namespace boost::corosio::detail - -#endif +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef SRC_DETAIL_MAKE_ERR_HPP +#define SRC_DETAIL_MAKE_ERR_HPP + +#include +#include +#include + +namespace boost::corosio::detail { + +#if BOOST_COROSIO_HAS_IOCP +/** Convert a Windows error code to std::error_code. + + Maps ERROR_OPERATION_ABORTED and ERROR_CANCELLED to capy::error::canceled. + Maps ERROR_HANDLE_EOF to capy::error::eof. + + @param dwError The Windows error code (DWORD). + @return The corresponding std::error_code. +*/ +std::error_code make_err(unsigned long dwError) noexcept; +#else +/** Convert a POSIX errno value to std::error_code. + + Maps ECANCELED to capy::error::canceled. + + @param errn The errno value. + @return The corresponding std::error_code. +*/ +std::error_code make_err(int errn) noexcept; +#endif + +} // namespace boost::corosio::detail + +#endif diff --git a/src/corosio/src/detail/resume_coro.hpp b/src/corosio/src/detail/resume_coro.hpp index c39b2386..0b138db8 100644 --- a/src/corosio/src/detail/resume_coro.hpp +++ b/src/corosio/src/detail/resume_coro.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/scheduler_op.hpp b/src/corosio/src/detail/scheduler_op.hpp index 8b174075..c261748e 100644 --- a/src/corosio/src/detail/scheduler_op.hpp +++ b/src/corosio/src/detail/scheduler_op.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/detail/timer_service.hpp b/src/corosio/src/detail/timer_service.hpp index b60b430b..cc3c6d11 100644 --- a/src/corosio/src/detail/timer_service.hpp +++ b/src/corosio/src/detail/timer_service.hpp @@ -1,68 +1,68 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_SRC_DETAIL_TIMER_SERVICE_HPP -#define BOOST_COROSIO_SRC_DETAIL_TIMER_SERVICE_HPP - -#include -#include - -#include -#include - -namespace boost::corosio::detail { - -struct scheduler; - -class timer_service : public capy::execution_context::service -{ -public: - using clock_type = std::chrono::steady_clock; - using time_point = clock_type::time_point; - - // Nested callback type - context + function pointer - class callback - { - void* ctx_ = nullptr; - void(*fn_)(void*) = nullptr; - - public: - callback() = default; - callback(void* ctx, void(*fn)(void*)) noexcept - : ctx_(ctx), fn_(fn) {} - - explicit operator bool() const noexcept { return fn_ != nullptr; } - void operator()() const { if (fn_) fn_(ctx_); } - }; - - // Create timer implementation - virtual timer::timer_impl* create_impl() = 0; - - // Query methods for scheduler - virtual bool empty() const noexcept = 0; - virtual time_point nearest_expiry() const noexcept = 0; - - // Process expired timers - scheduler calls this after wait - virtual std::size_t process_expired() = 0; - - // Callback for when earliest timer changes - virtual void set_on_earliest_changed(callback cb) = 0; - -protected: - timer_service() = default; -}; - -// Get or create the timer service for the given context -timer_service& -get_timer_service( - capy::execution_context& ctx, scheduler& sched); - -} // namespace boost::corosio::detail - -#endif +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_SRC_DETAIL_TIMER_SERVICE_HPP +#define BOOST_COROSIO_SRC_DETAIL_TIMER_SERVICE_HPP + +#include +#include + +#include +#include + +namespace boost::corosio::detail { + +struct scheduler; + +class timer_service : public capy::execution_context::service +{ +public: + using clock_type = std::chrono::steady_clock; + using time_point = clock_type::time_point; + + // Nested callback type - context + function pointer + class callback + { + void* ctx_ = nullptr; + void(*fn_)(void*) = nullptr; + + public: + callback() = default; + callback(void* ctx, void(*fn)(void*)) noexcept + : ctx_(ctx), fn_(fn) {} + + explicit operator bool() const noexcept { return fn_ != nullptr; } + void operator()() const { if (fn_) fn_(ctx_); } + }; + + // Create timer implementation + virtual timer::timer_impl* create_impl() = 0; + + // Query methods for scheduler + virtual bool empty() const noexcept = 0; + virtual time_point nearest_expiry() const noexcept = 0; + + // Process expired timers - scheduler calls this after wait + virtual std::size_t process_expired() = 0; + + // Callback for when earliest timer changes + virtual void set_on_earliest_changed(callback cb) = 0; + +protected: + timer_service() = default; +}; + +// Get or create the timer service for the given context +timer_service& +get_timer_service( + capy::execution_context& ctx, scheduler& sched); + +} // namespace boost::corosio::detail + +#endif diff --git a/src/corosio/src/endpoint.cpp b/src/corosio/src/endpoint.cpp index 769154b2..2ca7eb57 100644 --- a/src/corosio/src/endpoint.cpp +++ b/src/corosio/src/endpoint.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/ipv4_address.cpp b/src/corosio/src/ipv4_address.cpp index f4ff14c0..729d7551 100644 --- a/src/corosio/src/ipv4_address.cpp +++ b/src/corosio/src/ipv4_address.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/ipv6_address.cpp b/src/corosio/src/ipv6_address.cpp index 476667c3..93d0cdec 100644 --- a/src/corosio/src/ipv6_address.cpp +++ b/src/corosio/src/ipv6_address.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/resolver.cpp b/src/corosio/src/resolver.cpp index 6cc59d13..0e415f7a 100644 --- a/src/corosio/src/resolver.cpp +++ b/src/corosio/src/resolver.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/tcp_acceptor.cpp b/src/corosio/src/tcp_acceptor.cpp index 727370ee..f01a2adc 100644 --- a/src/corosio/src/tcp_acceptor.cpp +++ b/src/corosio/src/tcp_acceptor.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/tcp_server.cpp b/src/corosio/src/tcp_server.cpp index 467d2f47..6b5b1158 100644 --- a/src/corosio/src/tcp_server.cpp +++ b/src/corosio/src/tcp_server.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/tcp_socket.cpp b/src/corosio/src/tcp_socket.cpp index 74413104..8c1ce1c4 100644 --- a/src/corosio/src/tcp_socket.cpp +++ b/src/corosio/src/tcp_socket.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/test/mocket.cpp b/src/corosio/src/test/mocket.cpp index e5f838de..42902644 100644 --- a/src/corosio/src/test/mocket.cpp +++ b/src/corosio/src/test/mocket.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) @@ -31,11 +31,11 @@ mocket:: mocket:: mocket( capy::execution_context& ctx, - capy::test::fuse& f, + capy::test::fuse f, std::size_t max_read_size, std::size_t max_write_size) : sock_(ctx) - , fuse_(&f) + , fuse_(std::move(f)) , max_read_size_(max_read_size) , max_write_size_(max_write_size) { @@ -54,7 +54,6 @@ mocket(mocket&& other) noexcept , max_read_size_(other.max_read_size_) , max_write_size_(other.max_write_size_) { - other.fuse_ = nullptr; } mocket& @@ -69,7 +68,6 @@ operator=(mocket&& other) noexcept fuse_ = other.fuse_; max_read_size_ = other.max_read_size_; max_write_size_ = other.max_write_size_; - other.fuse_ = nullptr; } return *this; } @@ -98,13 +96,13 @@ close() // Verify test expectations if (!expect_.empty()) { - fuse_->fail(); + fuse_.fail(); sock_.close(); return capy::error::test_failure; } if (!provide_.empty()) { - fuse_->fail(); + fuse_.fail(); sock_.close(); return capy::error::test_failure; } @@ -132,7 +130,7 @@ is_open() const noexcept std::pair make_mocket_pair( capy::execution_context& ctx, - capy::test::fuse& f, + capy::test::fuse f, std::size_t max_read_size, std::size_t max_write_size) { diff --git a/src/corosio/src/test/socket_pair.cpp b/src/corosio/src/test/socket_pair.cpp index fff5f7d7..2feff298 100644 --- a/src/corosio/src/test/socket_pair.cpp +++ b/src/corosio/src/test/socket_pair.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/timer.cpp b/src/corosio/src/timer.cpp index 6fae2098..600ef758 100644 --- a/src/corosio/src/timer.cpp +++ b/src/corosio/src/timer.cpp @@ -1,95 +1,95 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include - -#include - -namespace boost::corosio { - -namespace detail { - -// Defined in timer_service.cpp -extern timer::timer_impl* timer_service_create(capy::execution_context&); -extern void timer_service_destroy(timer::timer_impl&) noexcept; -extern timer::time_point timer_service_expiry(timer::timer_impl&) noexcept; -extern void timer_service_expires_at(timer::timer_impl&, timer::time_point); -extern void timer_service_expires_after(timer::timer_impl&, timer::duration); -extern void timer_service_cancel(timer::timer_impl&) noexcept; - -} // namespace detail - -timer:: -~timer() -{ - if (impl_) - detail::timer_service_destroy(get()); -} - -timer:: -timer(capy::execution_context& ctx) - : io_object(ctx) -{ - impl_ = detail::timer_service_create(ctx); -} - -timer:: -timer(timer&& other) noexcept - : io_object(other.context()) -{ - impl_ = other.impl_; - other.impl_ = nullptr; -} - -timer& -timer:: -operator=(timer&& other) -{ - if (this != &other) - { - if (ctx_ != other.ctx_) - detail::throw_logic_error( - "cannot move timer across execution contexts"); - if (impl_) - detail::timer_service_destroy(get()); - impl_ = other.impl_; - other.impl_ = nullptr; - } - return *this; -} - -void -timer:: -cancel() -{ - detail::timer_service_cancel(get()); -} - -timer::time_point -timer:: -expiry() const -{ - return detail::timer_service_expiry(get()); -} - -void -timer:: -expires_at(time_point t) -{ - detail::timer_service_expires_at(get(), t); -} - -void -timer:: -expires_after(duration d) -{ - detail::timer_service_expires_after(get(), d); -} - -} // namespace boost::corosio +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include + +#include + +namespace boost::corosio { + +namespace detail { + +// Defined in timer_service.cpp +extern timer::timer_impl* timer_service_create(capy::execution_context&); +extern void timer_service_destroy(timer::timer_impl&) noexcept; +extern timer::time_point timer_service_expiry(timer::timer_impl&) noexcept; +extern void timer_service_expires_at(timer::timer_impl&, timer::time_point); +extern void timer_service_expires_after(timer::timer_impl&, timer::duration); +extern void timer_service_cancel(timer::timer_impl&) noexcept; + +} // namespace detail + +timer:: +~timer() +{ + if (impl_) + detail::timer_service_destroy(get()); +} + +timer:: +timer(capy::execution_context& ctx) + : io_object(ctx) +{ + impl_ = detail::timer_service_create(ctx); +} + +timer:: +timer(timer&& other) noexcept + : io_object(other.context()) +{ + impl_ = other.impl_; + other.impl_ = nullptr; +} + +timer& +timer:: +operator=(timer&& other) +{ + if (this != &other) + { + if (ctx_ != other.ctx_) + detail::throw_logic_error( + "cannot move timer across execution contexts"); + if (impl_) + detail::timer_service_destroy(get()); + impl_ = other.impl_; + other.impl_ = nullptr; + } + return *this; +} + +void +timer:: +cancel() +{ + detail::timer_service_cancel(get()); +} + +timer::time_point +timer:: +expiry() const +{ + return detail::timer_service_expiry(get()); +} + +void +timer:: +expires_at(time_point t) +{ + detail::timer_service_expires_at(get(), t); +} + +void +timer:: +expires_after(duration d) +{ + detail::timer_service_expires_after(get(), d); +} + +} // namespace boost::corosio diff --git a/src/corosio/src/tls/context.cpp b/src/corosio/src/tls/context.cpp index 17ef13da..b8cb18bc 100644 --- a/src/corosio/src/tls/context.cpp +++ b/src/corosio/src/tls/context.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/corosio/src/tls/detail/context_impl.hpp b/src/corosio/src/tls/detail/context_impl.hpp index cac917e9..d0e74061 100644 --- a/src/corosio/src/tls/detail/context_impl.hpp +++ b/src/corosio/src/tls/detail/context_impl.hpp @@ -1,170 +1,170 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef SRC_TLS_DETAIL_CONTEXT_IMPL_HPP -#define SRC_TLS_DETAIL_CONTEXT_IMPL_HPP - -#include - -#include -#include -#include -#include - -namespace boost::corosio { - -namespace detail { - -/** Abstract base for cached native SSL contexts. - - Stored in context::impl as an intrusive linked list. - Each TLS backend derives from this to cache its native - context handle ( WOLFSSL_CTX*, SSL_CTX*, etc. ). -*/ -class native_context_base -{ -public: - native_context_base* next_ = nullptr; - void const* service_ = nullptr; - - virtual ~native_context_base() = default; -}; - -struct tls_context_data -{ - //-------------------------------------------- - // Credentials - - std::string entity_certificate; - tls_file_format entity_cert_format = tls_file_format::pem; - std::string certificate_chain; - std::string private_key; - tls_file_format private_key_format = tls_file_format::pem; - - //-------------------------------------------- - // Trust anchors - - std::vector ca_certificates; - std::vector verify_paths; - bool use_default_verify_paths = false; - - //-------------------------------------------- - // Protocol settings - - tls_version min_version = tls_version::tls_1_2; - tls_version max_version = tls_version::tls_1_3; - std::string ciphersuites; - std::vector alpn_protocols; - - //-------------------------------------------- - // Verification - - tls_verify_mode verification_mode = tls_verify_mode::none; - int verify_depth = 100; - std::string hostname; - std::function verify_callback; - - //-------------------------------------------- - // SNI (Server Name Indication) - - std::function servername_callback; - - //-------------------------------------------- - // Revocation - - std::vector crls; - std::string ocsp_staple; - bool require_ocsp_staple = false; - tls_revocation_policy revocation = tls_revocation_policy::disabled; - - //-------------------------------------------- - // Password - - std::function password_callback; - - //-------------------------------------------- - // Cached native contexts (intrusive list) - - mutable std::mutex native_contexts_mutex_; - mutable native_context_base* native_contexts_ = nullptr; - - /** Find or insert a cached native context. - - @param service The unique key for the backend. - @param create Factory function called if not found. - - @return Pointer to the cached native context. - */ - template - native_context_base* - find( void const* service, Factory&& create ) const - { - std::lock_guard lock( native_contexts_mutex_ ); - - for( auto* p = native_contexts_; p; p = p->next_ ) - if( p->service_ == service ) - return p; - - // Not found - create and prepend - auto* ctx = create(); - ctx->service_ = service; - ctx->next_ = native_contexts_; - native_contexts_ = ctx; - return ctx; - } - - ~tls_context_data() - { - // Clean up cached native contexts (no lock needed - destructor) - while( native_contexts_ ) - { - auto* next = native_contexts_->next_; - delete native_contexts_; - native_contexts_ = next; - } - } -}; - -} // namespace detail - -//------------------------------------------------------------------------------ - -/** Implementation of tls_context. - - Contains all portable TLS configuration data plus - cached native SSL contexts as an intrusive list. -*/ -struct tls_context::impl : detail::tls_context_data -{ -}; - -//------------------------------------------------------------------------------ - -namespace detail { - -/** Return the TLS context data. - - Provides read-only access to the portable configuration - stored in the context. - - @param ctx The TLS context. - - @return Reference to the context implementation. -*/ -inline tls_context_data const& -get_tls_context_data( tls_context const& ctx ) noexcept -{ - return *ctx.impl_; -} - -} // namespace detail - -} // namespace boost::corosio - -#endif +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef SRC_TLS_DETAIL_CONTEXT_IMPL_HPP +#define SRC_TLS_DETAIL_CONTEXT_IMPL_HPP + +#include + +#include +#include +#include +#include + +namespace boost::corosio { + +namespace detail { + +/** Abstract base for cached native SSL contexts. + + Stored in context::impl as an intrusive linked list. + Each TLS backend derives from this to cache its native + context handle ( WOLFSSL_CTX*, SSL_CTX*, etc. ). +*/ +class native_context_base +{ +public: + native_context_base* next_ = nullptr; + void const* service_ = nullptr; + + virtual ~native_context_base() = default; +}; + +struct tls_context_data +{ + //-------------------------------------------- + // Credentials + + std::string entity_certificate; + tls_file_format entity_cert_format = tls_file_format::pem; + std::string certificate_chain; + std::string private_key; + tls_file_format private_key_format = tls_file_format::pem; + + //-------------------------------------------- + // Trust anchors + + std::vector ca_certificates; + std::vector verify_paths; + bool use_default_verify_paths = false; + + //-------------------------------------------- + // Protocol settings + + tls_version min_version = tls_version::tls_1_2; + tls_version max_version = tls_version::tls_1_3; + std::string ciphersuites; + std::vector alpn_protocols; + + //-------------------------------------------- + // Verification + + tls_verify_mode verification_mode = tls_verify_mode::none; + int verify_depth = 100; + std::string hostname; + std::function verify_callback; + + //-------------------------------------------- + // SNI (Server Name Indication) + + std::function servername_callback; + + //-------------------------------------------- + // Revocation + + std::vector crls; + std::string ocsp_staple; + bool require_ocsp_staple = false; + tls_revocation_policy revocation = tls_revocation_policy::disabled; + + //-------------------------------------------- + // Password + + std::function password_callback; + + //-------------------------------------------- + // Cached native contexts (intrusive list) + + mutable std::mutex native_contexts_mutex_; + mutable native_context_base* native_contexts_ = nullptr; + + /** Find or insert a cached native context. + + @param service The unique key for the backend. + @param create Factory function called if not found. + + @return Pointer to the cached native context. + */ + template + native_context_base* + find( void const* service, Factory&& create ) const + { + std::lock_guard lock( native_contexts_mutex_ ); + + for( auto* p = native_contexts_; p; p = p->next_ ) + if( p->service_ == service ) + return p; + + // Not found - create and prepend + auto* ctx = create(); + ctx->service_ = service; + ctx->next_ = native_contexts_; + native_contexts_ = ctx; + return ctx; + } + + ~tls_context_data() + { + // Clean up cached native contexts (no lock needed - destructor) + while( native_contexts_ ) + { + auto* next = native_contexts_->next_; + delete native_contexts_; + native_contexts_ = next; + } + } +}; + +} // namespace detail + +//------------------------------------------------------------------------------ + +/** Implementation of tls_context. + + Contains all portable TLS configuration data plus + cached native SSL contexts as an intrusive list. +*/ +struct tls_context::impl : detail::tls_context_data +{ +}; + +//------------------------------------------------------------------------------ + +namespace detail { + +/** Return the TLS context data. + + Provides read-only access to the portable configuration + stored in the context. + + @param ctx The TLS context. + + @return Reference to the context implementation. +*/ +inline tls_context_data const& +get_tls_context_data( tls_context const& ctx ) noexcept +{ + return *ctx.impl_; +} + +} // namespace detail + +} // namespace boost::corosio + +#endif diff --git a/src/openssl/src/openssl.cpp b/src/openssl/src/openssl.cpp index d64580c0..6403b5dd 100644 --- a/src/openssl/src/openssl.cpp +++ b/src/openssl/src/openssl.cpp @@ -1,12 +1,12 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include - -// OpenSSL integration sources for boost::corosio +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include + +// OpenSSL integration sources for boost::corosio diff --git a/src/openssl/src/openssl_stream.cpp b/src/openssl/src/openssl_stream.cpp index 418bbaaf..844c8bc3 100644 --- a/src/openssl/src/openssl_stream.cpp +++ b/src/openssl/src/openssl_stream.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) @@ -277,6 +277,7 @@ struct openssl_stream::impl tls_context ctx_; SSL* ssl_ = nullptr; BIO* ext_bio_ = nullptr; + bool used_ = false; std::vector in_buf_; std::vector out_buf_; @@ -301,6 +302,31 @@ struct openssl_stream::impl SSL_free(ssl_); } + void + reset() + { + if(!ssl_) + return; + + // Preserves SSL* and BIO pair, releases session state + SSL_clear(ssl_); + + // Drain stale data from the external BIO + char drain[1024]; + while(BIO_ctrl_pending(ext_bio_) > 0) + BIO_read(ext_bio_, drain, sizeof(drain)); + + // SSL_clear clears per-session settings; reapply hostname + auto& cd = detail::get_tls_context_data(ctx_); + if(!cd.hostname.empty()) + { + SSL_set_tlsext_host_name(ssl_, cd.hostname.c_str()); + SSL_set1_host(ssl_, cd.hostname.c_str()); + } + + used_ = false; + } + //-------------------------------------------------------------------------- capy::task @@ -499,6 +525,9 @@ struct openssl_stream::impl capy::io_task<> do_handshake(int type) { + if(used_) + reset(); + std::error_code ec; while(true) @@ -512,6 +541,7 @@ struct openssl_stream::impl if(ret == 1) { + used_ = true; ec = co_await flush_output(); co_return {ec}; } @@ -735,6 +765,13 @@ shutdown() co_return co_await impl_->do_shutdown(); } +void +openssl_stream:: +reset() +{ + impl_->reset(); +} + std::string_view openssl_stream:: name() const noexcept diff --git a/src/wolfssl/src/wolfssl.cpp b/src/wolfssl/src/wolfssl.cpp index 05c32fe7..e89cd71c 100644 --- a/src/wolfssl/src/wolfssl.cpp +++ b/src/wolfssl/src/wolfssl.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/src/wolfssl/src/wolfssl_stream.cpp b/src/wolfssl/src/wolfssl_stream.cpp index c53822d9..ff05861e 100644 --- a/src/wolfssl/src/wolfssl_stream.cpp +++ b/src/wolfssl/src/wolfssl_stream.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) @@ -290,6 +290,7 @@ struct wolfssl_stream::impl capy::any_stream& s_; tls_context ctx_; WOLFSSL* ssl_ = nullptr; + bool used_ = false; // Buffers for read operations std::vector read_in_buf_; @@ -341,6 +342,26 @@ struct wolfssl_stream::impl // WOLFSSL_CTX* is owned by cached native context, not freed here } + // Releases WOLFSSL object and resets buffer positions. + // I/O buffer vectors keep their allocations. + void + reset() + { + if(ssl_) + { + wolfSSL_free(ssl_); + ssl_ = nullptr; + } + read_in_pos_ = 0; + read_in_len_ = 0; + read_out_len_ = 0; + write_in_pos_ = 0; + write_in_len_ = 0; + write_out_len_ = 0; + current_op_ = nullptr; + used_ = false; + } + //-------------------------------------------------------------------------- // WolfSSL I/O Callbacks //-------------------------------------------------------------------------- @@ -637,6 +658,9 @@ struct wolfssl_stream::impl capy::io_task<> do_handshake(int type) { + if(used_) + reset(); + std::error_code ec; // Initialize SSL object for the specified role (deferred from construction) @@ -667,6 +691,7 @@ struct wolfssl_stream::impl if(ret == WOLFSSL_SUCCESS) { // Handshake completed successfully + used_ = true; // Flush any remaining output if(read_out_len_ > 0) { @@ -968,6 +993,13 @@ shutdown() co_return co_await impl_->do_shutdown(); } +void +wolfssl_stream:: +reset() +{ + impl_->reset(); +} + std::string_view wolfssl_stream:: name() const noexcept diff --git a/test/unit/acceptor.cpp b/test/unit/acceptor.cpp index 7c6c5aec..9946d5b2 100644 --- a/test/unit/acceptor.cpp +++ b/test/unit/acceptor.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/cross_ssl_stream.cpp b/test/unit/cross_ssl_stream.cpp index 9a6abdc5..fb0e54d8 100644 --- a/test/unit/cross_ssl_stream.cpp +++ b/test/unit/cross_ssl_stream.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/endpoint.cpp b/test/unit/endpoint.cpp index 84c114a8..9839caa5 100644 --- a/test/unit/endpoint.cpp +++ b/test/unit/endpoint.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/io_buffer_param.cpp b/test/unit/io_buffer_param.cpp index e3f87427..a85f21be 100644 --- a/test/unit/io_buffer_param.cpp +++ b/test/unit/io_buffer_param.cpp @@ -1,345 +1,345 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -// Test that header file is self-contained. -#include - -#include -#include -#include - -#include "test_suite.hpp" - -namespace boost::corosio { - -struct io_buffer_param_test -{ - // Helper to reduce repeated copy_to assertion pattern - static void - check_copy( - io_buffer_param p, - std::initializer_list> expected) - { - capy::mutable_buffer dest[8]; - auto n = p.copy_to(dest, 8); - BOOST_TEST_EQ(n, expected.size()); - std::size_t i = 0; - for(auto const& e : expected) - { - BOOST_TEST_EQ(dest[i].data(), e.first); - BOOST_TEST_EQ(dest[i].size(), e.second); - ++i; - } - } - - // Helper for checking empty/zero-byte sequences - static void - check_empty(io_buffer_param p) - { - capy::mutable_buffer dest[8]; - BOOST_TEST_EQ(p.copy_to(dest, 8), 0); - } - - void - testConstBuffer() - { - char const data[] = "Hello"; - capy::const_buffer cb(data, 5); - check_copy(cb, {{data, 5}}); - } - - void - testMutableBuffer() - { - char data[] = "Hello"; - capy::mutable_buffer mb(data, 5); - check_copy(mb, {{data, 5}}); - } - - void - testConstBufferPair() - { - char const data1[] = "Hello"; - char const data2[] = "World"; - capy::const_buffer_pair cbp{{ - capy::const_buffer(data1, 5), - capy::const_buffer(data2, 5) }}; - check_copy(cbp, {{data1, 5}, {data2, 5}}); - } - - void - testMutableBufferPair() - { - char data1[] = "Hello"; - char data2[] = "World"; - capy::mutable_buffer_pair mbp{{ - capy::mutable_buffer(data1, 5), - capy::mutable_buffer(data2, 5) }}; - check_copy(mbp, {{data1, 5}, {data2, 5}}); - } - - void - testSpan() - { - char const data1[] = "One"; - char const data2[] = "Two"; - char const data3[] = "Three"; - capy::const_buffer arr[3] = { - capy::const_buffer(data1, 3), - capy::const_buffer(data2, 3), - capy::const_buffer(data3, 5) }; - std::span s(arr, 3); - check_copy(s, {{data1, 3}, {data2, 3}, {data3, 5}}); - } - - void - testArray() - { - char const data1[] = "One"; - char const data2[] = "Two"; - char const data3[] = "Three"; - std::array arr{{ - capy::const_buffer(data1, 3), - capy::const_buffer(data2, 3), - capy::const_buffer(data3, 5) }}; - check_copy(arr, {{data1, 3}, {data2, 3}, {data3, 5}}); - } - - void - testCArray() - { - char const data1[] = "One"; - char const data2[] = "Two"; - char const data3[] = "Three"; - capy::const_buffer arr[3] = { - capy::const_buffer(data1, 3), - capy::const_buffer(data2, 3), - capy::const_buffer(data3, 5) }; - check_copy(arr, {{data1, 3}, {data2, 3}, {data3, 5}}); - } - - void - testLimitedCopy() - { - char const data1[] = "One"; - char const data2[] = "Two"; - char const data3[] = "Three"; - capy::const_buffer arr[3] = { - capy::const_buffer(data1, 3), - capy::const_buffer(data2, 3), - capy::const_buffer(data3, 5) }; - - io_buffer_param ref(arr); - - // copy only 2 buffers - capy::mutable_buffer dest[2]; - auto n = ref.copy_to(dest, 2); - BOOST_TEST_EQ(n, 2); - BOOST_TEST_EQ(dest[0].data(), data1); - BOOST_TEST_EQ(dest[0].size(), 3); - BOOST_TEST_EQ(dest[1].data(), data2); - BOOST_TEST_EQ(dest[1].size(), 3); - } - - void - testEmptySequence() - { - // Zero total bytes returns 0, regardless of buffer count - capy::const_buffer cb; - check_empty(cb); - } - - void - testZeroByteConstBuffer() - { - // Explicit zero-byte const buffer - char const* data = "Hello"; - capy::const_buffer cb(data, 0); - check_empty(cb); - } - - void - testZeroByteMultiple() - { - // Multiple zero-byte buffers should still return 0 - char const data1[] = "Hello"; - char const data2[] = "World"; - capy::const_buffer arr[3] = { - capy::const_buffer(data1, 0), - capy::const_buffer(data2, 0), - capy::const_buffer(nullptr, 0) }; - check_empty(arr); - } - - void - testZeroByteBufferPair() - { - // Buffer pair with both zero-byte buffers - char const data1[] = "Hello"; - char const data2[] = "World"; - capy::const_buffer_pair cbp{{ - capy::const_buffer(data1, 0), - capy::const_buffer(data2, 0) }}; - check_empty(cbp); - } - - void - testMixedZeroAndNonZero() - { - // Mix of zero-byte and non-zero buffers - // Zero-size buffers are skipped, only non-zero returned - char const data1[] = "Hello"; - char const data2[] = "World"; - capy::const_buffer arr[3] = { - capy::const_buffer(data1, 0), - capy::const_buffer(data2, 5), - capy::const_buffer(nullptr, 0) }; - check_copy(arr, {{data2, 5}}); - } - - void - testOneZeroOneNonZero() - { - // Buffer pair with one zero-byte, one non-zero - // Zero-size buffer is skipped - char const data1[] = "Hello"; - char const data2[] = "World"; - capy::const_buffer_pair cbp{{ - capy::const_buffer(data1, 0), - capy::const_buffer(data2, 5) }}; - check_copy(cbp, {{data2, 5}}); - } - - void - testZeroByteMutableBuffer() - { - // Zero-byte mutable buffer - char data[] = "Hello"; - capy::mutable_buffer mb(data, 0); - check_empty(mb); - } - - void - testZeroByteMutableBufferPair() - { - // Mutable buffer pair with zero-byte buffers - char data1[] = "Hello"; - char data2[] = "World"; - capy::mutable_buffer_pair mbp{{ - capy::mutable_buffer(data1, 0), - capy::mutable_buffer(data2, 0) }}; - check_empty(mbp); - } - - void - testEmptySpan() - { - // Empty span (no buffers at all) - std::span s; - check_empty(s); - } - - void - testEmptyArray() - { - // Empty std::array (zero-size) - std::array arr{}; - check_empty(arr); - } - - // Helper function that accepts io_buffer_param by value - static std::size_t - acceptByValue(io_buffer_param p) - { - capy::mutable_buffer dest[8]; - return p.copy_to(dest, 8); - } - - // Helper function that accepts io_buffer_param by const reference - static std::size_t - acceptByConstRef(io_buffer_param const& p) - { - capy::mutable_buffer dest[8]; - return p.copy_to(dest, 8); - } - - void - testPassByValue() - { - // Test that io_buffer_param works when passed by value - char const data[] = "Hello"; - capy::const_buffer cb(data, 5); - - // Pass buffer directly (implicit conversion) - auto n = acceptByValue(cb); - BOOST_TEST_EQ(n, 1); - - // Pass io_buffer_param object - io_buffer_param p(cb); - n = acceptByValue(p); - BOOST_TEST_EQ(n, 1); - - // Pass buffer sequence directly - std::array arr{{ - capy::const_buffer(data, 2), - capy::const_buffer(data + 2, 3) }}; - n = acceptByValue(arr); - BOOST_TEST_EQ(n, 2); - } - - void - testPassByConstRef() - { - // Test that io_buffer_param works when passed by const reference - char const data[] = "Hello"; - capy::const_buffer cb(data, 5); - - // Pass io_buffer_param object by const ref - io_buffer_param p(cb); - auto n = acceptByConstRef(p); - BOOST_TEST_EQ(n, 1); - - // Pass buffer sequence directly (creates temporary io_buffer_param) - n = acceptByConstRef(std::array{{ - capy::const_buffer(data, 2), - capy::const_buffer(data + 2, 3) }}); - BOOST_TEST_EQ(n, 2); - } - - void - run() - { - testConstBuffer(); - testMutableBuffer(); - testConstBufferPair(); - testMutableBufferPair(); - testSpan(); - testArray(); - testCArray(); - testLimitedCopy(); - testEmptySequence(); - testZeroByteConstBuffer(); - testZeroByteMultiple(); - testZeroByteBufferPair(); - testMixedZeroAndNonZero(); - testOneZeroOneNonZero(); - testZeroByteMutableBuffer(); - testZeroByteMutableBufferPair(); - testEmptySpan(); - testEmptyArray(); - testPassByValue(); - testPassByConstRef(); - } -}; - -TEST_SUITE( - io_buffer_param_test, - "boost.corosio.io_buffer_param"); - -} // namespace boost::corosio +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace boost::corosio { + +struct io_buffer_param_test +{ + // Helper to reduce repeated copy_to assertion pattern + static void + check_copy( + io_buffer_param p, + std::initializer_list> expected) + { + capy::mutable_buffer dest[8]; + auto n = p.copy_to(dest, 8); + BOOST_TEST_EQ(n, expected.size()); + std::size_t i = 0; + for(auto const& e : expected) + { + BOOST_TEST_EQ(dest[i].data(), e.first); + BOOST_TEST_EQ(dest[i].size(), e.second); + ++i; + } + } + + // Helper for checking empty/zero-byte sequences + static void + check_empty(io_buffer_param p) + { + capy::mutable_buffer dest[8]; + BOOST_TEST_EQ(p.copy_to(dest, 8), 0); + } + + void + testConstBuffer() + { + char const data[] = "Hello"; + capy::const_buffer cb(data, 5); + check_copy(cb, {{data, 5}}); + } + + void + testMutableBuffer() + { + char data[] = "Hello"; + capy::mutable_buffer mb(data, 5); + check_copy(mb, {{data, 5}}); + } + + void + testConstBufferPair() + { + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer_pair cbp{{ + capy::const_buffer(data1, 5), + capy::const_buffer(data2, 5) }}; + check_copy(cbp, {{data1, 5}, {data2, 5}}); + } + + void + testMutableBufferPair() + { + char data1[] = "Hello"; + char data2[] = "World"; + capy::mutable_buffer_pair mbp{{ + capy::mutable_buffer(data1, 5), + capy::mutable_buffer(data2, 5) }}; + check_copy(mbp, {{data1, 5}, {data2, 5}}); + } + + void + testSpan() + { + char const data1[] = "One"; + char const data2[] = "Two"; + char const data3[] = "Three"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 3), + capy::const_buffer(data2, 3), + capy::const_buffer(data3, 5) }; + std::span s(arr, 3); + check_copy(s, {{data1, 3}, {data2, 3}, {data3, 5}}); + } + + void + testArray() + { + char const data1[] = "One"; + char const data2[] = "Two"; + char const data3[] = "Three"; + std::array arr{{ + capy::const_buffer(data1, 3), + capy::const_buffer(data2, 3), + capy::const_buffer(data3, 5) }}; + check_copy(arr, {{data1, 3}, {data2, 3}, {data3, 5}}); + } + + void + testCArray() + { + char const data1[] = "One"; + char const data2[] = "Two"; + char const data3[] = "Three"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 3), + capy::const_buffer(data2, 3), + capy::const_buffer(data3, 5) }; + check_copy(arr, {{data1, 3}, {data2, 3}, {data3, 5}}); + } + + void + testLimitedCopy() + { + char const data1[] = "One"; + char const data2[] = "Two"; + char const data3[] = "Three"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 3), + capy::const_buffer(data2, 3), + capy::const_buffer(data3, 5) }; + + io_buffer_param ref(arr); + + // copy only 2 buffers + capy::mutable_buffer dest[2]; + auto n = ref.copy_to(dest, 2); + BOOST_TEST_EQ(n, 2); + BOOST_TEST_EQ(dest[0].data(), data1); + BOOST_TEST_EQ(dest[0].size(), 3); + BOOST_TEST_EQ(dest[1].data(), data2); + BOOST_TEST_EQ(dest[1].size(), 3); + } + + void + testEmptySequence() + { + // Zero total bytes returns 0, regardless of buffer count + capy::const_buffer cb; + check_empty(cb); + } + + void + testZeroByteConstBuffer() + { + // Explicit zero-byte const buffer + char const* data = "Hello"; + capy::const_buffer cb(data, 0); + check_empty(cb); + } + + void + testZeroByteMultiple() + { + // Multiple zero-byte buffers should still return 0 + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 0), + capy::const_buffer(data2, 0), + capy::const_buffer(nullptr, 0) }; + check_empty(arr); + } + + void + testZeroByteBufferPair() + { + // Buffer pair with both zero-byte buffers + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer_pair cbp{{ + capy::const_buffer(data1, 0), + capy::const_buffer(data2, 0) }}; + check_empty(cbp); + } + + void + testMixedZeroAndNonZero() + { + // Mix of zero-byte and non-zero buffers + // Zero-size buffers are skipped, only non-zero returned + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 0), + capy::const_buffer(data2, 5), + capy::const_buffer(nullptr, 0) }; + check_copy(arr, {{data2, 5}}); + } + + void + testOneZeroOneNonZero() + { + // Buffer pair with one zero-byte, one non-zero + // Zero-size buffer is skipped + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer_pair cbp{{ + capy::const_buffer(data1, 0), + capy::const_buffer(data2, 5) }}; + check_copy(cbp, {{data2, 5}}); + } + + void + testZeroByteMutableBuffer() + { + // Zero-byte mutable buffer + char data[] = "Hello"; + capy::mutable_buffer mb(data, 0); + check_empty(mb); + } + + void + testZeroByteMutableBufferPair() + { + // Mutable buffer pair with zero-byte buffers + char data1[] = "Hello"; + char data2[] = "World"; + capy::mutable_buffer_pair mbp{{ + capy::mutable_buffer(data1, 0), + capy::mutable_buffer(data2, 0) }}; + check_empty(mbp); + } + + void + testEmptySpan() + { + // Empty span (no buffers at all) + std::span s; + check_empty(s); + } + + void + testEmptyArray() + { + // Empty std::array (zero-size) + std::array arr{}; + check_empty(arr); + } + + // Helper function that accepts io_buffer_param by value + static std::size_t + acceptByValue(io_buffer_param p) + { + capy::mutable_buffer dest[8]; + return p.copy_to(dest, 8); + } + + // Helper function that accepts io_buffer_param by const reference + static std::size_t + acceptByConstRef(io_buffer_param const& p) + { + capy::mutable_buffer dest[8]; + return p.copy_to(dest, 8); + } + + void + testPassByValue() + { + // Test that io_buffer_param works when passed by value + char const data[] = "Hello"; + capy::const_buffer cb(data, 5); + + // Pass buffer directly (implicit conversion) + auto n = acceptByValue(cb); + BOOST_TEST_EQ(n, 1); + + // Pass io_buffer_param object + io_buffer_param p(cb); + n = acceptByValue(p); + BOOST_TEST_EQ(n, 1); + + // Pass buffer sequence directly + std::array arr{{ + capy::const_buffer(data, 2), + capy::const_buffer(data + 2, 3) }}; + n = acceptByValue(arr); + BOOST_TEST_EQ(n, 2); + } + + void + testPassByConstRef() + { + // Test that io_buffer_param works when passed by const reference + char const data[] = "Hello"; + capy::const_buffer cb(data, 5); + + // Pass io_buffer_param object by const ref + io_buffer_param p(cb); + auto n = acceptByConstRef(p); + BOOST_TEST_EQ(n, 1); + + // Pass buffer sequence directly (creates temporary io_buffer_param) + n = acceptByConstRef(std::array{{ + capy::const_buffer(data, 2), + capy::const_buffer(data + 2, 3) }}); + BOOST_TEST_EQ(n, 2); + } + + void + run() + { + testConstBuffer(); + testMutableBuffer(); + testConstBufferPair(); + testMutableBufferPair(); + testSpan(); + testArray(); + testCArray(); + testLimitedCopy(); + testEmptySequence(); + testZeroByteConstBuffer(); + testZeroByteMultiple(); + testZeroByteBufferPair(); + testMixedZeroAndNonZero(); + testOneZeroOneNonZero(); + testZeroByteMutableBuffer(); + testZeroByteMutableBufferPair(); + testEmptySpan(); + testEmptyArray(); + testPassByValue(); + testPassByConstRef(); + } +}; + +TEST_SUITE( + io_buffer_param_test, + "boost.corosio.io_buffer_param"); + +} // namespace boost::corosio diff --git a/test/unit/io_context.cpp b/test/unit/io_context.cpp index dc85b63d..2ceeffe7 100644 --- a/test/unit/io_context.cpp +++ b/test/unit/io_context.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/ipv4_address.cpp b/test/unit/ipv4_address.cpp index 9b7b7df6..5fd1fd68 100644 --- a/test/unit/ipv4_address.cpp +++ b/test/unit/ipv4_address.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/ipv6_address.cpp b/test/unit/ipv6_address.cpp index 9fa6e74e..d7b0a1d9 100644 --- a/test/unit/ipv6_address.cpp +++ b/test/unit/ipv6_address.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/openssl_stream.cpp b/test/unit/openssl_stream.cpp index fdb9b114..abf92e0c 100644 --- a/test/unit/openssl_stream.cpp +++ b/test/unit/openssl_stream.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) @@ -102,6 +102,10 @@ struct openssl_stream_test test::testSniCallback( make_stream ); test::testMtls( make_stream ); + test::testReset( make_stream, cert_modes ); + test::testResetViaHandshake( make_stream, cert_modes ); + test::testResetFuse( make_stream ); + testCertificateChain(); testName(); } diff --git a/test/unit/signal_set.cpp b/test/unit/signal_set.cpp index 07f4577c..5fb1345f 100644 --- a/test/unit/signal_set.cpp +++ b/test/unit/signal_set.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/socket.cpp b/test/unit/socket.cpp index ea265d7f..86a0a981 100644 --- a/test/unit/socket.cpp +++ b/test/unit/socket.cpp @@ -490,6 +490,8 @@ struct socket_test_impl recv_data.data() + total_recv, size - total_recv)); BOOST_TEST(!ec); + if(ec) + break; total_recv += n; } @@ -525,11 +527,11 @@ struct socket_test_impl BOOST_TEST(!ec1); BOOST_TEST_EQ(std::string_view(buf, n1), "final"); - // Next read should get EOF (0 bytes or error) + // Next read should get EOF auto [ec2, n2] = co_await b.read_some( capy::mutable_buffer(buf, sizeof(buf))); - // EOF indicated by error or zero bytes - BOOST_TEST(ec2 || n2 == 0); + BOOST_TEST(ec2 == capy::cond::eof); + BOOST_TEST_EQ(n2, 0u); }; capy::run_async(ioc.get_executor())(task(s1, s2)); diff --git a/test/unit/socket_stress.cpp b/test/unit/socket_stress.cpp index 047d6f37..5438144f 100644 --- a/test/unit/socket_stress.cpp +++ b/test/unit/socket_stress.cpp @@ -40,11 +40,6 @@ #include #include -#if BOOST_COROSIO_POSIX -#include -#else -#include -#endif #include "test_suite.hpp" @@ -63,26 +58,9 @@ get_stress_duration() return default_stress_seconds; } -std::atomic stress_port_counter{0}; - -std::uint16_t -get_stress_port() noexcept -{ - constexpr std::uint16_t port_base = 50000; - constexpr std::uint16_t port_range = 15000; - -#if BOOST_COROSIO_POSIX - auto pid = static_cast(getpid()); -#else - auto pid = static_cast(_getpid()); -#endif - auto pid_offset = static_cast((pid * 7919) % port_range); - auto offset = stress_port_counter.fetch_add(1, std::memory_order_relaxed); - return static_cast(port_base + ((pid_offset + offset) % port_range)); -} - // Create a connected tcp_socket pair for stress testing. -// Must be called BEFORE context::run(). +// Uses ephemeral port (0) so the OS assigns an available port, +// avoiding TIME_WAIT collisions on back-to-back runs. template std::pair make_stress_pair(Context& ctx) @@ -94,22 +72,10 @@ make_stress_pair(Context& ctx) bool accept_done = false; bool connect_done = false; - std::uint16_t port = 0; tcp_acceptor acc(ctx); - bool listening = false; - for (int attempt = 0; attempt < 50; ++attempt) - { - port = get_stress_port(); - if (!acc.listen(endpoint(ipv4_address::loopback(), port))) - { - listening = true; - break; - } - acc.close(); - acc = tcp_acceptor(ctx); - } - if (!listening) - throw std::runtime_error("stress_pair: failed to find available port"); + if (auto ec = acc.listen(endpoint(ipv4_address::loopback(), 0))) + throw std::runtime_error("stress_pair listen failed: " + ec.message()); + auto port = acc.local_endpoint().port(); tcp_socket s1(ctx); tcp_socket s2(ctx); @@ -652,26 +618,13 @@ struct accept_stress_test_impl std::atomic connections{0}; std::atomic stop_flag{false}; - // Find available port - std::uint16_t port = 0; tcp_acceptor acc(ioc); - bool listening = false; - for (int attempt = 0; attempt < 50; ++attempt) - { - port = get_stress_port(); - if (!acc.listen(endpoint(ipv4_address::loopback(), port))) - { - listening = true; - break; - } - acc.close(); - acc = tcp_acceptor(ioc); - } - if (!listening) + if (auto ec = acc.listen(endpoint(ipv4_address::loopback(), 0))) { - BOOST_ERROR("accept_stress: failed to find available port"); + BOOST_ERROR("accept_stress: listen failed"); return; } + auto port = acc.local_endpoint().port(); // Acceptor task auto acceptor_task = [&]() -> capy::task<> diff --git a/test/unit/stream_tests.hpp b/test/unit/stream_tests.hpp index abf0fca1..2db6f380 100644 --- a/test/unit/stream_tests.hpp +++ b/test/unit/stream_tests.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/tcp_server.cpp b/test/unit/tcp_server.cpp index e72be9ae..e61639a5 100644 --- a/test/unit/tcp_server.cpp +++ b/test/unit/tcp_server.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/test/mocket.cpp b/test/unit/test/mocket.cpp index 35e63d5f..74c9076a 100644 --- a/test/unit/test/mocket.cpp +++ b/test/unit/test/mocket.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/test/socket_pair.cpp b/test/unit/test/socket_pair.cpp index 0e25b3b5..c823efec 100644 --- a/test/unit/test/socket_pair.cpp +++ b/test/unit/test/socket_pair.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/test_utils.hpp b/test/unit/test_utils.hpp index 6e0d17a7..4997db91 100644 --- a/test/unit/test_utils.hpp +++ b/test/unit/test_utils.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/timer.cpp b/test/unit/timer.cpp index 813ef3e2..ad809c1e 100644 --- a/test/unit/timer.cpp +++ b/test/unit/timer.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/tls_stream.cpp b/test/unit/tls_stream.cpp index b9808628..bb5801ac 100644 --- a/test/unit/tls_stream.cpp +++ b/test/unit/tls_stream.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) diff --git a/test/unit/tls_stream_stress.cpp b/test/unit/tls_stream_stress.cpp new file mode 100644 index 00000000..1b5a086c --- /dev/null +++ b/test/unit/tls_stream_stress.cpp @@ -0,0 +1,496 @@ +// +// Copyright (c) 2026 Vinnie Falco +// +// 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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Stress tests for OpenSSL and WolfSSL TLS stream adapters. +// These tests hammer TLS-specific code paths to expose race +// conditions, lifetime bugs, and state corruption. +// +// Target areas: +// 1. Session cycling - rapid handshake/data/close lifecycle +// 2. Concurrent TLS I/O - multiple TLS pairs active simultaneously +// 3. Stop token cancellation races during TLS handshake +// +// Tests run for a configurable duration (default 1 second). + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef BOOST_COROSIO_HAS_OPENSSL +#include +#endif + +#ifdef BOOST_COROSIO_HAS_WOLFSSL +#include +#endif + +#include +#include +#include +#include +#include + +#include "test_utils.hpp" +#include "test_suite.hpp" + +namespace boost::corosio { +namespace { + +constexpr int default_tls_stress_seconds = 1; + +int +get_tls_stress_duration() +{ + auto* opt = test_suite::get_command_line_option( "stress-duration" ); + if( opt ) + return std::atoi( opt ); + return default_tls_stress_seconds; +} + +} // namespace + +//------------------------------------------------------------------------------ +// Stress Test 1: Rapid TLS Session Cycling +// +// Repeatedly creates socket pairs, performs TLS handshake, transfers +// data, and closes. Each iteration exercises the full session +// lifecycle to find state corruption and resource leaks. +//------------------------------------------------------------------------------ + +template +struct tls_session_cycle_stress_impl +{ + static constexpr StreamFactory make_stream{}; + + void + run() + { + int duration = get_tls_stress_duration(); + std::fprintf( stderr, + " tls_session_cycle: running for %d seconds...\n", duration ); + + auto stop_time = std::chrono::steady_clock::now() + + std::chrono::seconds( duration ); + + io_context ioc; + auto ex = ioc.get_executor(); + std::size_t iterations = 0; + + while( std::chrono::steady_clock::now() < stop_time ) + { + auto [s1, s2] = corosio::test::make_socket_pair( ioc ); + + auto client_ctx = test::make_client_context(); + auto server_ctx = test::make_server_context(); + + auto client = make_stream( s1, client_ctx ); + auto server = make_stream( s2, server_ctx ); + + // Handshake + std::error_code cec, sec; + + auto hs_client = [&client, &cec]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + cec = ec; + }; + + auto hs_server = [&server, &sec]() -> capy::task<> + { + auto [ec] = co_await server.handshake( tls_stream::server ); + sec = ec; + }; + + capy::run_async( ex )( hs_client() ); + capy::run_async( ex )( hs_server() ); + ioc.run(); + ioc.restart(); + + BOOST_TEST( !cec ); + BOOST_TEST( !sec ); + if( cec || sec ) + { + s1.close(); + s2.close(); + continue; + } + + // Bidirectional data transfer + auto xfer = [&client, &server]() -> capy::task<> + { + char wbuf[] = "stress-test-data"; + auto [ec1, n1] = co_await client.write_some( + capy::const_buffer( wbuf, sizeof( wbuf ) - 1 ) ); + if( ec1 ) + co_return; + + char rbuf[64]; + auto [ec2, n2] = co_await server.read_some( + capy::mutable_buffer( rbuf, sizeof( rbuf ) ) ); + BOOST_TEST( !ec2 ); + if( !ec2 ) + BOOST_TEST_EQ( n2, sizeof( wbuf ) - 1 ); + }; + + capy::run_async( ex )( xfer() ); + ioc.run(); + ioc.restart(); + + s1.close(); + s2.close(); + ++iterations; + } + + std::fprintf( stderr, + " tls_session_cycle: %zu sessions completed\n", iterations ); + + BOOST_TEST( iterations > 0 ); + } +}; + +//------------------------------------------------------------------------------ +// Stress Test 2: Concurrent TLS Data Transfer +// +// Two TLS pairs transfer data simultaneously to stress thread +// safety and completion dispatch in the TLS adapter layer. +//------------------------------------------------------------------------------ + +template +struct tls_concurrent_io_stress_impl +{ + static constexpr StreamFactory make_stream{}; + + void + run() + { + int duration = get_tls_stress_duration(); + std::fprintf( stderr, + " tls_concurrent_io: running for %d seconds...\n", duration ); + + io_context ioc; + auto ex = ioc.get_executor(); + + // Create two socket pairs + auto [sa1, sa2] = corosio::test::make_socket_pair( ioc ); + auto [sb1, sb2] = corosio::test::make_socket_pair( ioc ); + + auto ca_ctx = test::make_client_context(); + auto sa_ctx = test::make_server_context(); + auto cb_ctx = test::make_client_context(); + auto sb_ctx = test::make_server_context(); + + auto client_a = make_stream( sa1, ca_ctx ); + auto server_a = make_stream( sa2, sa_ctx ); + auto client_b = make_stream( sb1, cb_ctx ); + auto server_b = make_stream( sb2, sb_ctx ); + + // Handshake pair A + { + std::error_code cec, sec; + auto hsc = [&client_a, &cec]() -> capy::task<> + { + auto [ec] = co_await client_a.handshake( tls_stream::client ); + cec = ec; + }; + auto hss = [&server_a, &sec]() -> capy::task<> + { + auto [ec] = co_await server_a.handshake( tls_stream::server ); + sec = ec; + }; + capy::run_async( ex )( hsc() ); + capy::run_async( ex )( hss() ); + ioc.run(); + ioc.restart(); + BOOST_TEST( !cec ); + BOOST_TEST( !sec ); + if( cec || sec ) + return; + } + + // Handshake pair B + { + std::error_code cec, sec; + auto hsc = [&client_b, &cec]() -> capy::task<> + { + auto [ec] = co_await client_b.handshake( tls_stream::client ); + cec = ec; + }; + auto hss = [&server_b, &sec]() -> capy::task<> + { + auto [ec] = co_await server_b.handshake( tls_stream::server ); + sec = ec; + }; + capy::run_async( ex )( hsc() ); + capy::run_async( ex )( hss() ); + ioc.run(); + ioc.restart(); + BOOST_TEST( !cec ); + BOOST_TEST( !sec ); + if( cec || sec ) + return; + } + + // Concurrent data transfer on both pairs + std::atomic total_bytes{0}; + std::atomic stop_flag{false}; + + // Writer: pumps data through a TLS stream until stopped + auto writer = []( auto& stream, + std::atomic& stop, + std::atomic& bytes ) -> capy::task<> + { + char buf[256]; + std::memset( buf, 'W', sizeof( buf ) ); + std::size_t sent = 0; + + while( !stop.load( std::memory_order_relaxed ) ) + { + auto [ec, n] = co_await stream.write_some( + capy::const_buffer( buf, sizeof( buf ) ) ); + if( ec ) + break; + sent += n; + } + + bytes.fetch_add( sent, std::memory_order_relaxed ); + }; + + // Reader: drains data from a TLS stream until stopped + auto reader = []( auto& stream, + std::atomic& stop, + std::atomic& bytes ) -> capy::task<> + { + char buf[256]; + std::size_t received = 0; + + while( !stop.load( std::memory_order_relaxed ) ) + { + auto [ec, n] = co_await stream.read_some( + capy::mutable_buffer( buf, sizeof( buf ) ) ); + if( ec ) + break; + received += n; + } + + bytes.fetch_add( received, std::memory_order_relaxed ); + }; + + capy::run_async( ex )( writer( client_a, stop_flag, total_bytes ) ); + capy::run_async( ex )( reader( server_a, stop_flag, total_bytes ) ); + capy::run_async( ex )( writer( client_b, stop_flag, total_bytes ) ); + capy::run_async( ex )( reader( server_b, stop_flag, total_bytes ) ); + + // Stopper: wait for duration then close all sockets + auto stopper = [&]() -> capy::task<> + { + timer t( ioc ); + t.expires_after( std::chrono::seconds( duration ) ); + (void)co_await t.wait(); + stop_flag.store( true, std::memory_order_relaxed ); + + sa1.close(); + sa2.close(); + sb1.close(); + sb2.close(); + }; + + capy::run_async( ex )( stopper() ); + + ioc.run(); + + std::fprintf( stderr, + " tls_concurrent_io: %zu total bytes transferred\n", + total_bytes.load() ); + + BOOST_TEST( total_bytes.load() > 0 ); + } +}; + +//------------------------------------------------------------------------------ +// Stress Test 3: TLS Handshake Cancellation Race +// +// Rapidly starts TLS handshakes and cancels them via stop_token +// after the client has sent the ClientHello. Stresses the +// cancellation path in the TLS async state machine. +//------------------------------------------------------------------------------ + +template +struct tls_cancel_handshake_stress_impl +{ + static constexpr StreamFactory make_stream{}; + + void + run() + { + int duration = get_tls_stress_duration(); + std::fprintf( stderr, + " tls_cancel_handshake: running for %d seconds...\n", duration ); + + auto stop_time = std::chrono::steady_clock::now() + + std::chrono::seconds( duration ); + + io_context ioc; + auto ex = ioc.get_executor(); + std::size_t iterations = 0; + std::size_t cancellations = 0; + + while( std::chrono::steady_clock::now() < stop_time ) + { + auto [s1, s2] = corosio::test::make_socket_pair( ioc ); + + auto client_ctx = test::make_client_context(); + auto server_ctx = test::make_server_context(); + + auto client = make_stream( s1, client_ctx ); + auto server = make_stream( s2, server_ctx ); + + std::stop_source stop_src; + bool client_got_error = false; + bool done = false; + + // Failsafe to prevent hangs + timer failsafe( ioc ); + failsafe.expires_after( std::chrono::milliseconds( 2000 ) ); + + // Client handshake - will be cancelled mid-flight + auto client_task = [&client, &client_got_error, + &done, &failsafe]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + if( ec ) + client_got_error = true; + done = true; + failsafe.cancel(); + }; + + // Server: wait for ClientHello then trigger cancellation + auto server_task = [&s2, &stop_src]() -> capy::task<> + { + char buf[1]; + (void)co_await s2.read_some( + capy::mutable_buffer( buf, 1 ) ); + stop_src.request_stop(); + }; + + bool failsafe_hit = false; + auto failsafe_task = [&failsafe, &failsafe_hit, + &s1, &s2]() -> capy::task<> + { + auto [ec] = co_await failsafe.wait(); + if( !ec ) + { + failsafe_hit = true; + if( s1.is_open() ) { s1.cancel(); s1.close(); } + if( s2.is_open() ) { s2.cancel(); s2.close(); } + } + }; + + capy::run_async( ex, stop_src.get_token() )( client_task() ); + capy::run_async( ex )( server_task() ); + capy::run_async( ex )( failsafe_task() ); + + ioc.run(); + ioc.restart(); + + BOOST_TEST( !failsafe_hit ); + if( client_got_error ) + ++cancellations; + + if( s1.is_open() ) s1.close(); + if( s2.is_open() ) s2.close(); + ++iterations; + } + + std::fprintf( stderr, + " tls_cancel_handshake: %zu iterations, %zu cancellations\n", + iterations, cancellations ); + + BOOST_TEST( iterations > 0 ); + BOOST_TEST( cancellations > 0 ); + } +}; + +//------------------------------------------------------------------------------ +// OpenSSL stress tests +//------------------------------------------------------------------------------ + +#ifdef BOOST_COROSIO_HAS_OPENSSL + +namespace { + +struct openssl_stress_factory +{ + auto operator()( tcp_socket& s, tls_context ctx ) const + { + return openssl_stream( &s, ctx ); + } +}; + +} // namespace + +struct openssl_session_cycle_stress + : tls_session_cycle_stress_impl {}; +TEST_SUITE( openssl_session_cycle_stress, + "boost.corosio.tls_stream_stress.openssl.session_cycle" ); + +struct openssl_concurrent_io_stress + : tls_concurrent_io_stress_impl {}; +TEST_SUITE( openssl_concurrent_io_stress, + "boost.corosio.tls_stream_stress.openssl.concurrent_io" ); + +struct openssl_cancel_handshake_stress + : tls_cancel_handshake_stress_impl {}; +TEST_SUITE( openssl_cancel_handshake_stress, + "boost.corosio.tls_stream_stress.openssl.cancel_handshake" ); + +#endif + +//------------------------------------------------------------------------------ +// WolfSSL stress tests +//------------------------------------------------------------------------------ + +#ifdef BOOST_COROSIO_HAS_WOLFSSL + +namespace { + +struct wolfssl_stress_factory +{ + auto operator()( tcp_socket& s, tls_context ctx ) const + { + return wolfssl_stream( &s, ctx ); + } +}; + +} // namespace + +struct wolfssl_session_cycle_stress + : tls_session_cycle_stress_impl {}; +TEST_SUITE( wolfssl_session_cycle_stress, + "boost.corosio.tls_stream_stress.wolfssl.session_cycle" ); + +struct wolfssl_concurrent_io_stress + : tls_concurrent_io_stress_impl {}; +TEST_SUITE( wolfssl_concurrent_io_stress, + "boost.corosio.tls_stream_stress.wolfssl.concurrent_io" ); + +struct wolfssl_cancel_handshake_stress + : tls_cancel_handshake_stress_impl {}; +TEST_SUITE( wolfssl_cancel_handshake_stress, + "boost.corosio.tls_stream_stress.wolfssl.cancel_handshake" ); + +#endif + +} // namespace boost::corosio diff --git a/test/unit/tls_stream_tests.hpp b/test/unit/tls_stream_tests.hpp index c3df91f9..7a080378 100644 --- a/test/unit/tls_stream_tests.hpp +++ b/test/unit/tls_stream_tests.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) @@ -502,6 +502,337 @@ testMtls( StreamFactory make_stream ) } } +//------------------------------------------------------------------------------ +// +// Reset Tests +// +//------------------------------------------------------------------------------ + +/** Test explicit reset() between TLS sessions. + + Verifies that calling reset() after shutdown allows the + same stream objects to perform a new handshake and data + transfer. Two full rounds on the same stream pair. +*/ +template +void +testReset( + StreamFactory make_stream, + std::array const& modes ) +{ + for( auto mode : modes ) + { + io_context ioc; + auto [m1, m2] = corosio::test::make_mocket_pair( ioc ); + + auto [client_ctx, server_ctx] = make_contexts( mode ); + auto client = make_stream( m1, client_ctx ); + auto server = make_stream( m2, server_ctx ); + + auto do_round = [&]( std::string const& msg ) -> capy::task<> + { + std::error_code client_ec; + std::error_code server_ec; + + // Handshake + auto hs_client = [&]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + client_ec = ec; + }; + auto hs_server = [&]() -> capy::task<> + { + auto [ec] = co_await server.handshake( tls_stream::server ); + server_ec = ec; + }; + + capy::run_async( ioc.get_executor() )( hs_client() ); + capy::run_async( ioc.get_executor() )( hs_server() ); + ioc.run(); + ioc.restart(); + + BOOST_TEST( !client_ec ); + BOOST_TEST( !server_ec ); + if( client_ec || server_ec ) + co_return; + + // Data transfer + auto xfer = [&]() -> capy::task<> + { + // Client writes + auto [wec, wn] = co_await client.write_some( + capy::const_buffer( msg.data(), msg.size() ) ); + BOOST_TEST( !wec ); + if( wec ) + co_return; + + // Server reads + std::string buf( msg.size(), '\0' ); + auto [rec, rn] = co_await server.read_some( + capy::mutable_buffer( buf.data(), buf.size() ) ); + BOOST_TEST( !rec ); + if( !rec ) + BOOST_TEST( buf.substr( 0, rn ) == msg.substr( 0, rn ) ); + }; + capy::run_async( ioc.get_executor() )( xfer() ); + ioc.run(); + ioc.restart(); + + // Shutdown both sides concurrently + auto sd_client = [&]() -> capy::task<> + { + (void) co_await client.shutdown(); + }; + auto sd_server = [&]() -> capy::task<> + { + // Read until close_notify, then send ours + char drain[32]; + (void) co_await server.read_some( + capy::mutable_buffer( drain, sizeof( drain ) ) ); + (void) co_await server.shutdown(); + }; + + capy::run_async( ioc.get_executor() )( sd_client() ); + capy::run_async( ioc.get_executor() )( sd_server() ); + ioc.run(); + ioc.restart(); + + co_return; + }; + + // Round 1 + auto r1 = [&]() -> capy::task<> + { + co_await do_round( "hello1" ); + }; + capy::run_async( ioc.get_executor() )( r1() ); + ioc.run(); + ioc.restart(); + + // Explicit reset + client.reset(); + server.reset(); + + // Round 2 + auto r2 = [&]() -> capy::task<> + { + co_await do_round( "hello2" ); + }; + capy::run_async( ioc.get_executor() )( r2() ); + ioc.run(); + + m1.close(); + m2.close(); + } +} + +/** Test implicit reset via handshake(). + + Verifies that calling handshake() on a previously-used stream + automatically resets, without an explicit reset() call. +*/ +template +void +testResetViaHandshake( + StreamFactory make_stream, + std::array const& modes ) +{ + for( auto mode : modes ) + { + io_context ioc; + auto [m1, m2] = corosio::test::make_mocket_pair( ioc ); + + auto [client_ctx, server_ctx] = make_contexts( mode ); + auto client = make_stream( m1, client_ctx ); + auto server = make_stream( m2, server_ctx ); + + auto do_round = [&]( std::string const& msg ) -> capy::task<> + { + std::error_code client_ec; + std::error_code server_ec; + + auto hs_client = [&]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + client_ec = ec; + }; + auto hs_server = [&]() -> capy::task<> + { + auto [ec] = co_await server.handshake( tls_stream::server ); + server_ec = ec; + }; + + capy::run_async( ioc.get_executor() )( hs_client() ); + capy::run_async( ioc.get_executor() )( hs_server() ); + ioc.run(); + ioc.restart(); + + BOOST_TEST( !client_ec ); + BOOST_TEST( !server_ec ); + if( client_ec || server_ec ) + co_return; + + auto xfer = [&]() -> capy::task<> + { + auto [wec, wn] = co_await client.write_some( + capy::const_buffer( msg.data(), msg.size() ) ); + BOOST_TEST( !wec ); + if( wec ) + co_return; + + std::string buf( msg.size(), '\0' ); + auto [rec, rn] = co_await server.read_some( + capy::mutable_buffer( buf.data(), buf.size() ) ); + BOOST_TEST( !rec ); + if( !rec ) + BOOST_TEST( buf.substr( 0, rn ) == msg.substr( 0, rn ) ); + }; + capy::run_async( ioc.get_executor() )( xfer() ); + ioc.run(); + ioc.restart(); + + auto sd_client = [&]() -> capy::task<> + { + (void) co_await client.shutdown(); + }; + auto sd_server = [&]() -> capy::task<> + { + char drain[32]; + (void) co_await server.read_some( + capy::mutable_buffer( drain, sizeof( drain ) ) ); + (void) co_await server.shutdown(); + }; + + capy::run_async( ioc.get_executor() )( sd_client() ); + capy::run_async( ioc.get_executor() )( sd_server() ); + ioc.run(); + ioc.restart(); + + co_return; + }; + + // Round 1 + auto r1 = [&]() -> capy::task<> + { + co_await do_round( "round1" ); + }; + capy::run_async( ioc.get_executor() )( r1() ); + ioc.run(); + ioc.restart(); + + // No explicit reset -- handshake() should auto-reset + + // Round 2 + auto r2 = [&]() -> capy::task<> + { + co_await do_round( "round2" ); + }; + capy::run_async( ioc.get_executor() )( r2() ); + ioc.run(); + + m1.close(); + m2.close(); + } +} + +/** Test reset with fuse/max_size variations. + + Stresses chunked I/O across reset boundaries. +*/ +template +void +testResetFuse( StreamFactory make_stream ) +{ + for( auto max_size : tls_max_sizes ) + { + if( max_size < 64 ) + continue; + + capy::test::fuse f; + f.armed( [&]( capy::test::fuse& ) -> capy::task<> + { + io_context ioc; + auto [m1, m2] = corosio::test::make_mocket_pair( + ioc, f, max_size, max_size ); + + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + + auto client = make_stream( m1, client_ctx ); + auto server = make_stream( m2, server_ctx ); + + // Round 1 + { + std::error_code cec, sec; + auto hsc = [&]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + cec = ec; + }; + auto hss = [&]() -> capy::task<> + { + auto [ec] = co_await server.handshake( tls_stream::server ); + sec = ec; + }; + capy::run_async( ioc.get_executor() )( hsc() ); + capy::run_async( ioc.get_executor() )( hss() ); + ioc.run(); + ioc.restart(); + BOOST_TEST( !cec ); + BOOST_TEST( !sec ); + if( cec || sec ) + co_return; + + // Shutdown + auto sdc = [&]() -> capy::task<> + { + (void) co_await client.shutdown(); + }; + auto sds = [&]() -> capy::task<> + { + char drain[32]; + (void) co_await server.read_some( + capy::mutable_buffer( drain, sizeof( drain ) ) ); + (void) co_await server.shutdown(); + }; + capy::run_async( ioc.get_executor() )( sdc() ); + capy::run_async( ioc.get_executor() )( sds() ); + ioc.run(); + ioc.restart(); + } + + // Reset both + client.reset(); + server.reset(); + + // Round 2 + { + std::error_code cec, sec; + auto hsc = [&]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + cec = ec; + }; + auto hss = [&]() -> capy::task<> + { + auto [ec] = co_await server.handshake( tls_stream::server ); + sec = ec; + }; + capy::run_async( ioc.get_executor() )( hsc() ); + capy::run_async( ioc.get_executor() )( hss() ); + ioc.run(); + ioc.restart(); + BOOST_TEST( !cec ); + BOOST_TEST( !sec ); + } + + m1.close(); + m2.close(); + co_return; + } ); + } +} + } // namespace boost::corosio::test #endif diff --git a/test/unit/wolfssl_stream.cpp b/test/unit/wolfssl_stream.cpp index af31a964..e016cde2 100644 --- a/test/unit/wolfssl_stream.cpp +++ b/test/unit/wolfssl_stream.cpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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) @@ -109,6 +109,10 @@ struct wolfssl_stream_test test::testSniCallback( make_stream ); test::testMtls( make_stream ); + test::testReset( make_stream, cert_modes ); + test::testResetViaHandshake( make_stream, cert_modes ); + test::testResetFuse( make_stream ); + testCertificateChain(); testName(); }