docs(listener): ListenerQUIC::Port(), and port 0 for tests
The README gains a QUIC section, which it never had: plain QUIC under an ALPN of your own, starting with a listener on port 0 and Port(). The consumer notes describe Port() and point tests at port 0 rather than at binding a socket by hand; the agent notes count the new test and make port 0 the default for a new one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3457ff08ce
commit
fdb0ac2524
3 changed files with 22 additions and 4 deletions
|
|
@ -82,7 +82,7 @@ A route map is `std::unordered_map<std::string, std::function<HTTPResponse(const
|
|||
- **`QUICStream`** is move-only, with `connection`, `canSend`, `canReceive` (a unidirectional stream has one of them false) and the msquic `handle`. `SendSync(buffer, size, finish = false)` blocks until msquic has taken the buffer, throws `QUICClosedException` on a closed stream or connection and `QUICException` on a failed send; `finish` closes the send side after it (the peer's `RecieveUntilCloseSync` returns). `RecieveSync()` waits for at least one byte and throws `QUICClosedException` once the peer closed its side and the buffer is drained; `RecieveUntilCloseSync()`; `RecieveUntilFullSync(n)` takes exactly `n` or throws and leaves what arrived for the next receive. `SendAsync(buffer, size, finish, onSent(bool))` copies the buffer and tells the callback whether the stream took it; `RecieveAsync`, `RecieveUntilCloseAsync` and `RecieveUntilFullAsync(n, ...)` give a closed stream an empty chunk and an early close what arrived, never an exception. A default-constructed `QUICStream` behaves as closed. `PrependReceived(bytes)` pushes bytes back for the next receive (a demuxer that peeked a prefix); `GetStreamId()` throws `QUICException` before the stream has started; `Stop()` shuts both directions down (the send side with a FIN, the receive side abortively, else a peer holding its side open keeps the handle alive), completes asynchronously, and is what the destructor calls.
|
||||
- Lifetimes: `~ClientQUIC` first destroys peer streams nobody claimed, then shuts the connection down and closes it, waiting up to 5 s for every stream on it to finish closing. Streams may outlive their connection, dead; destroy or `Stop()` them first when you can. msquic has two process-wide registrations here, one for clients and one for listeners and the peers they accept, each closed by a static destructor at exit, and each close blocks until every connection and stream handle opened under it is closed: an accepted peer you never deleted holds the listener's until the idle timeout, two minutes by default. Delete or `Stop()` what you own before `main` returns, or end with `std::_Exit` after flushing, as the library's own tests do.
|
||||
|
||||
**`ListenerQUIC(port, alpn, QUICServerCredentials, connectCallback(ClientQUIC*), concurrentClientLimit, totalClientLimit)`** listens on every address of both families and throws `QUICException` when it cannot. The callback's `ClientQUIC*` is heap-allocated and yours: set `OnStream` / `OnDatagram` on it, keep it as long as the connection should live, `Stop()` or `delete` it to shut the connection down, and `delete` it in any case, since only that frees it (a connection the peer or the idle timeout ended has its msquic handle closed for it, but the object stays). `ListenSyncSync()` runs the accept loop on the calling thread and the callback there; `ListenSyncAsync()` runs the callback as a pool task; `ListenAsyncSync()` and `ListenAsyncAsync()` run the loop on a thread of their own, the second with the callback on the pool. `Stop()` stops accepting; the destructor stops, joins, and deletes peers accepted but not yet handed out. `totalClientLimit` ends the hand-off loop after that many connections (later ones queue until the listener is destroyed); `concurrentClientLimit` is stored and not enforced.
|
||||
**`ListenerQUIC(port, alpn, QUICServerCredentials, connectCallback(ClientQUIC*), concurrentClientLimit, totalClientLimit)`** listens on every address of both families and throws `QUICException` when it cannot. `Port()` is the UDP port it bound: pass port 0 to let msquic pick a free one and read it back (still valid after `Stop()`, 0 on a moved-from listener). The callback's `ClientQUIC*` is heap-allocated and yours: set `OnStream` / `OnDatagram` on it, keep it as long as the connection should live, `Stop()` or `delete` it to shut the connection down, and `delete` it in any case, since only that frees it (a connection the peer or the idle timeout ended has its msquic handle closed for it, but the object stays). `ListenSyncSync()` runs the accept loop on the calling thread and the callback there; `ListenSyncAsync()` runs the callback as a pool task; `ListenAsyncSync()` and `ListenAsyncAsync()` run the loop on a thread of their own, the second with the callback on the pool. `Stop()` stops accepting; the destructor stops, joins, and deletes peers accepted but not yet handed out. `totalClientLimit` ends the hand-off loop after that many connections (later ones queue until the listener is destroyed); `concurrentClientLimit` is stored and not enforced.
|
||||
|
||||
- `selfSigned` shells out to the `openssl` CLI (install it, or pass `certPath`/`keyPath`; without it the constructor throws `QUICException`) and caches the pair at `/tmp/crafter-network-quic-cert/{cert,key}.pem`, reused across restarts while valid (inside crafter-build's Flatpak, `/tmp` is a per-app directory discarded once no instance is running, so there the pair rarely survives between runs; pass `certPath`/`keyPath` for a stable certificate): ECDSA P-256, `CN=localhost`, SANs `localhost`, `127.0.0.1`, `::1`, 10 days, with the extensions Chromium's `serverCertificateHashes` demands. `GetSelfSignedCertificatePath()` is that file, and `ComputeCertificateHashSHA256(certPath)` (also through `openssl`) is the 32-byte digest a browser pins, see "Browser build". A different certificate from the TLS partition's: a client of both needs both.
|
||||
|
||||
|
|
@ -135,6 +135,6 @@ On a `wasm32-*` target the same `ClientHTTP` and `ClientQUIC` compile against th
|
|||
### Testing a consumer
|
||||
|
||||
- Start the pool first, and end a QUIC test with `std::fflush(stdout); std::_Exit(code);`, or delete every accepted peer and `Stop()` every client before `main` returns; otherwise the exit waits for the idle timeout.
|
||||
- crafter-build runs tests in parallel, so each test binds a port of its own. `ListenerTCP` and `ListenerHTTP1` set `SO_REUSEADDR`, so a port in `TIME_WAIT` from the previous run is fine; one held by a stray server from a crashed run is `Could not bind port`. For an ephemeral port, bind a socket yourself and read it back with `getsockname`, as `tests/ShouldConfigureTCP` does, then hand it to a `ClientTCP`.
|
||||
- crafter-build runs tests in parallel, so each test binds a port of its own. `ListenerTCP` and `ListenerHTTP1` set `SO_REUSEADDR`, so a port in `TIME_WAIT` from the previous run is fine; one held by a stray server from a crashed run is `Could not bind port`. Simpler is no fixed port at all: `ListenerTCP(address, 0, ...)` and `ListenerQUIC(0, ...)` take whatever port is free and report it with `Port()`.
|
||||
- `QUICServerCredentials{ .selfSigned = true }` against `QUICClientCredentials{ .insecureNoServerValidation = true }` is the local pair; `TLSServerCredentials{ .selfSigned = true }` against `TLSClientCredentials{ .caPem = GetSelfSignedCertificatePem().certificate }` exercises real verification. Neither needs anything installed except, for QUIC, the `openssl` CLI (the Flatpak has it).
|
||||
- Give a blocked reader (`RecieveSync` in a loop) a `std::thread`, not a pool task, and a test that could hang a watchdog thread that calls `std::_Exit(1)` sooner than the runner's 60 s timeout, as `tests/ShouldNotDropEarlyStreams` does at 8 s (the churn test's 90 s watchdog never fires first).
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ crafter-build # native: TCP, TLS, HTTP/1.1, QUI
|
|||
crafter-build --no-quic # native without QUIC, HTTP/3, WebTransport and msquic
|
||||
crafter-build --local # Crafter.Thread from ../Crafter.Thread
|
||||
crafter-build --target=wasm32-wasip1 # the browser library
|
||||
crafter-build test # the suite, native: 30 tests
|
||||
crafter-build test # the suite, native: 31 tests
|
||||
crafter-build test 'Should*HTTP1' # a name glob
|
||||
crafter-build test --no-quic # the TCP, URL, TLS and HTTP/1.1 tests plus ShouldBuildWithoutQUIC: 19
|
||||
crafter-build test --list
|
||||
|
|
@ -69,7 +69,7 @@ Conventions the tests follow:
|
|||
|
||||
- Names are `Should<Verb><What>`; the `SendRecieve*` and `Interop*` families end in `HTTP1` or `HTTPS1` on the HTTP/1.1 side and `HTTP` or `QUIC` on the QUIC side, and `ShouldSend`, `ShouldNotDropEarlyStreams`, `ShouldFallbackUnknownRoutes` and `ShouldSurviveConnectionChurn` carry no suffix. Open `main.cpp` with a comment on the behaviour under test and the failure it guards against; `tests/ShouldRecieveTCP/main.cpp` and `tests/ShouldSurviveConnectionChurn/main.cpp` show the shape, and a regression test names the bug.
|
||||
- A `Check(ok, what)` helper in an anonymous namespace counts failures and prints them (`PASS`/`FAIL` lines in the newer tests), and `main` returns nonzero at the end; the oldest QUIC tests return at the first failure. Do not stop at the first failure unless later steps are meaningless without it.
|
||||
- Each test owns fixed ports, distinct across the suite because the runner is parallel: 8090 to 8097 and 8110 to 8114 for HTTP/1.1 listeners, 8082 to 8086 and 8098 for HTTP/3, 9181, 9182, 9187 and 9188 for QUIC, 19200 to 19224 for the churn test; the two TCP tests bind ephemeral ports, and `ShouldBuildWithoutQUIC` and `ShouldSend` bind none. Pick an unused one for a new test, or bind an ephemeral port yourself (`LoopbackListener` in `ShouldConfigureTCP`).
|
||||
- Each test owns fixed ports, distinct across the suite because the runner is parallel: 8090 to 8097 and 8110 to 8114 for HTTP/1.1 listeners, 8082 to 8086 and 8098 for HTTP/3, 9181, 9182, 9187 and 9188 for QUIC, 19200 to 19224 for the churn test; the two TCP tests bind ephemeral ports, as do the QUIC tests written since `ListenerQUIC::Port()` (port 0, read back), and `ShouldBuildWithoutQUIC` and `ShouldSend` bind none. A new test binds port 0 and reads `Port()` (`ListenerTCP` and `ListenerQUIC` both have it); a fixed port is for when it cannot.
|
||||
- `ThreadPool::Start()` first, for anything that enqueues: every `*Async` call, `ClientHTTP` (its constructor installs an `OnStream` drain that runs as pool tasks), `ListenerHTTP`, `ListenerQUIC`, and the `*Async`-dispatch `Listen*` variants of `ListenerTCP`; `ListenerHTTP1` and the sync TCP listeners need none, which is why ten tests never call it. `Start()` runs `hardware_concurrency() - 1` workers and `Start(1)` none at all (tasks then wait for a `JoinPool()` nobody calls), so don't use `Start(1)` to make callbacks deterministic.
|
||||
- A QUIC test ends in `std::_Exit(code)`, after `std::fflush(stdout)` since `_Exit` flushes nothing (`ShouldReportClosedQUICStream` does; the older ones skip the flush and can lose their last line in the log): msquic's two registrations are closed by static destructors that block until every connection is torn down, and the peers a listener accepted are heap-allocated for the callback's owner, so returning from `main` waits for the idle timeout. `ShouldSurviveConnectionChurn` is the exception on purpose: that wait is its assertion, so it deletes everything and returns, under a watchdog thread that calls `std::_Exit(1)` after 90 s, which the runner's 60 s timeout beats; a watchdog meant to fire goes under 60 s, as the 8 s one in `ShouldNotDropEarlyStreams` does. Any test that can hang gets one.
|
||||
- Credentials: `QUICServerCredentials{ .selfSigned = true }` against `QUICClientCredentials{ .insecureNoServerValidation = true }`; for TLS, `TLSServerCredentials{ .selfSigned = true }` against a client given `GetSelfSignedCertificatePem().certificate` as `caPem`, so verification is exercised rather than skipped.
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -85,6 +85,23 @@ std::thread([&] { broker.ListenSyncSync(); }).detach();
|
|||
// ... broker.Stop() ends the loop from another thread.
|
||||
```
|
||||
|
||||
### QUIC Components
|
||||
|
||||
Plain QUIC under an ALPN of your own, for a protocol that is not HTTP. A browser cannot reach these: it speaks QUIC only as WebTransport, see [WebTransport Components](#webtransport-components).
|
||||
|
||||
#### ListenerQUIC
|
||||
```cpp
|
||||
Crafter::QUICServerCredentials creds;
|
||||
creds.selfSigned = true; // dev-only, see "Self-signed certs & browser peers"
|
||||
Crafter::ListenerQUIC listener(0, "myproto/1", creds, [](Crafter::ClientQUIC* peer) {
|
||||
// An accepted connection, heap-allocated and yours to delete.
|
||||
});
|
||||
listener.ListenAsyncAsync();
|
||||
const std::uint16_t port = listener.Port(); // the port msquic picked for port 0
|
||||
```
|
||||
|
||||
`Port()` reports the UDP port the listener is bound to, so port 0 is usable: the listener takes whatever port is free and tells you which.
|
||||
|
||||
### HTTP/3 Components
|
||||
|
||||
HTTP/3 runs over QUIC, which always requires TLS. Pass server credentials when constructing the listener (or set `selfSigned = true` for a development-only ephemeral cert) and matching client credentials when constructing the client (`insecureNoServerValidation = true` for self-signed servers).
|
||||
|
|
@ -316,6 +333,7 @@ The library includes tests covering:
|
|||
- HTTP/3 external interop (`ShouldSend`) — live fetch from `cloudflare-quic.com:443`, exercises real TLS chain validation, mandatory control stream, peer-initiated unidi streams, and QPACK Huffman decoding
|
||||
- QUIC reliable streams (`ShouldSendRecieveQUICStream`)
|
||||
- QUIC unreliable datagrams (`ShouldSendRecieveQUICDatagram`)
|
||||
- QUIC listener ports (`ShouldReportListenerPortQUIC`) — a listener on port 0 reports the port it got and is reachable there, a second one gets another, and a fixed port reads back as itself
|
||||
- QUIC connection teardown (`ShouldSurviveConnectionChurn`) — 25 build-up/tear-down cycles with reader threads parked inside `RecieveSync`, the shape a reconnect loop has; the rest of the QUIC tests end in `std::_Exit(0)` and so never run a `ClientQUIC` destructor
|
||||
- WebTransport echo (`ShouldEchoWebTransport`) — extended-CONNECT acceptance, draft-02 SETTINGS, bidi data stream framing (`WT_STREAM 0x41` + session-id varint), and byte-for-byte echo
|
||||
- HTTP/1.1 wire format (`ShouldParseHTTP1`) — serialisation, incremental parsing fed one byte at a time, chunked bodies with trailers, pipelining, interim 1xx, HTTP/1.0 and HEAD framing, and the malformed inputs the parser is required to reject
|
||||
|
|
|
|||
Loading…
Reference in a new issue