feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived
- --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with
them msquic: a consumer that only needs TCP builds no msquic. The flag
isn't passed on to dependencies.
- --local takes Crafter.Thread from the sibling checkout, as the other
Crafter projects do.
- ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and
SocketTimeoutException.
- TCP receives return the bytes that arrived:
- RecieveSync(n) returned the whole buffer when fewer had come;
- RecieveUntilCloseSync truncated what it had read;
- a signal (EINTR) during a receive threw where it should wait on;
- RecieveSync(size, buffer) returns the count and throws like the others;
- a receive of no bytes returns at once: recv's 0 for it isn't a close.
- Every TCP *Async receive takes an onError(std::exception_ptr): an
exception escaping a ThreadPool task ended the program. An empty onError
drops the error.
- QUIC streams:
- async receives deliver an empty chunk on close, as the browser build does;
- RecieveUntilFullAsync gives what arrived on a stream that closes early;
- RecieveUntilFullSync takes all it asked for or nothing, under one hold
of the lock, so what did arrive stays for the next receive and no
other receive can take bytes from among it;
- SendAsync's callback hears whether the send went out (bool sent): native,
browser, the JS bridge and the example.
- msquic is pinned to v2.6.3 and built against the system OpenSSL
(QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's
openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with
config_diagnostics on) made it fail to initialise. The README says to use
--no-quic where the distro's OpenSSL is older.
- Tests now register on any Linux triple: they never ran under the Flatpak's
x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP,
ShouldRecieveTCP, ShouldReportClosedQUICStream.
Breaking: SendAsync's callback takes a bool, the TCP *Async receives take
an onError, and RecieveSync(size, buffer) returns the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
|
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
|
|
|
|
module;
|
|
|
|
|
#ifndef CRAFTER_NETWORK_BROWSER
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <sys/socket.h>
|
|
|
|
|
#include <netinet/in.h>
|
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <sys/uio.h>
|
|
|
|
|
#include <sys/time.h>
|
|
|
|
|
#include <sys/wait.h>
|
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
#include <netdb.h>
|
|
|
|
|
#include <strings.h>
|
|
|
|
|
#include <cerrno>
|
|
|
|
|
#endif
|
|
|
|
|
export module Crafter.Network:ClientTCP;
|
|
|
|
|
import std;
|
|
|
|
|
|
|
|
|
|
#ifndef CRAFTER_NETWORK_BROWSER
|
|
|
|
|
namespace Crafter {
|
|
|
|
|
export class SocketClosedException : public std::exception {
|
|
|
|
|
public:
|
|
|
|
|
const char* what() const noexcept override {
|
|
|
|
|
return "Socket closed";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A connect or a receive waited longer than its timeout.
|
|
|
|
|
export class SocketTimeoutException : public std::exception {
|
|
|
|
|
public:
|
|
|
|
|
const char* what() const noexcept override {
|
|
|
|
|
return "Socket timed out";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
// The step of a connection a SocketErrorException failed at.
|
|
|
|
|
export enum class SocketPhase {
|
|
|
|
|
// getaddrinfo on the host name.
|
|
|
|
|
Resolve,
|
|
|
|
|
// socket() for an address the host resolved to.
|
|
|
|
|
Open,
|
|
|
|
|
// connect() to that address.
|
|
|
|
|
Connect,
|
|
|
|
|
Send,
|
|
|
|
|
Receive,
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
};
|
|
|
|
|
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
// The error category of getaddrinfo's results (EAI_NONAME, EAI_AGAIN,
|
|
|
|
|
// ...): a number space of its own that overlaps the errno values, so a
|
|
|
|
|
// resolver code is never confused with an errno, and whose message() is
|
|
|
|
|
// gai_strerror's text. One instance for the process, like
|
|
|
|
|
// std::generic_category().
|
|
|
|
|
export const std::error_category& ResolverCategory() noexcept;
|
|
|
|
|
|
|
|
|
|
// A resolve, a socket(), a connect, a send or a receive that failed. A
|
|
|
|
|
// std::system_error, so still a std::runtime_error for the handlers that
|
|
|
|
|
// catch one: code() is the errno under std::generic_category(), or the
|
|
|
|
|
// getaddrinfo result under ResolverCategory(), and what() is the text
|
|
|
|
|
// ClientTCP has always thrown, the code's own text after it. `phase`
|
|
|
|
|
// says which step failed. A caller that reports the cause in its own
|
|
|
|
|
// words — "[Errno 111] Connection refused", say — takes code().value()
|
|
|
|
|
// and code().message() instead of parsing what(), and compares code()
|
|
|
|
|
// with std::errc::connection_refused instead of the number.
|
|
|
|
|
export class SocketErrorException : public std::system_error {
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
public:
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
SocketErrorException(SocketPhase phase, std::error_code code, const std::string& what)
|
|
|
|
|
: std::system_error(code, what), phase(phase) {}
|
|
|
|
|
// Without a message of its own: what() is the code's text alone.
|
|
|
|
|
SocketErrorException(SocketPhase phase, std::error_code code)
|
|
|
|
|
: std::system_error(code), phase(phase) {}
|
|
|
|
|
SocketPhase phase;
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
};
|
|
|
|
|
|
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived
- --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with
them msquic: a consumer that only needs TCP builds no msquic. The flag
isn't passed on to dependencies.
- --local takes Crafter.Thread from the sibling checkout, as the other
Crafter projects do.
- ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and
SocketTimeoutException.
- TCP receives return the bytes that arrived:
- RecieveSync(n) returned the whole buffer when fewer had come;
- RecieveUntilCloseSync truncated what it had read;
- a signal (EINTR) during a receive threw where it should wait on;
- RecieveSync(size, buffer) returns the count and throws like the others;
- a receive of no bytes returns at once: recv's 0 for it isn't a close.
- Every TCP *Async receive takes an onError(std::exception_ptr): an
exception escaping a ThreadPool task ended the program. An empty onError
drops the error.
- QUIC streams:
- async receives deliver an empty chunk on close, as the browser build does;
- RecieveUntilFullAsync gives what arrived on a stream that closes early;
- RecieveUntilFullSync takes all it asked for or nothing, under one hold
of the lock, so what did arrive stays for the next receive and no
other receive can take bytes from among it;
- SendAsync's callback hears whether the send went out (bool sent): native,
browser, the JS bridge and the example.
- msquic is pinned to v2.6.3 and built against the system OpenSSL
(QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's
openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with
config_diagnostics on) made it fail to initialise. The README says to use
--no-quic where the distro's OpenSSL is older.
- Tests now register on any Linux triple: they never ran under the Flatpak's
x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP,
ShouldRecieveTCP, ShouldReportClosedQUICStream.
Breaking: SendAsync's callback takes a bool, the TCP *Async receives take
an onError, and RecieveSync(size, buffer) returns the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
|
|
|
export class ClientTCP {
|
|
|
|
|
public:
|
|
|
|
|
int socketid;
|
|
|
|
|
ClientTCP(int socket);
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
// The host is resolved for IPv4 and IPv6 alike and every address it
|
|
|
|
|
// resolves to is tried in turn, the way Python's
|
|
|
|
|
// socket.create_connection does: `localhost` reaches a server that
|
|
|
|
|
// listens on ::1 only, as a dev server on a recent Node does. The
|
|
|
|
|
// first address that connects wins; when none does, what the last
|
|
|
|
|
// one failed with is thrown — SocketErrorException, or
|
|
|
|
|
// SocketTimeoutException for a timeout.
|
|
|
|
|
//
|
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived
- --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with
them msquic: a consumer that only needs TCP builds no msquic. The flag
isn't passed on to dependencies.
- --local takes Crafter.Thread from the sibling checkout, as the other
Crafter projects do.
- ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and
SocketTimeoutException.
- TCP receives return the bytes that arrived:
- RecieveSync(n) returned the whole buffer when fewer had come;
- RecieveUntilCloseSync truncated what it had read;
- a signal (EINTR) during a receive threw where it should wait on;
- RecieveSync(size, buffer) returns the count and throws like the others;
- a receive of no bytes returns at once: recv's 0 for it isn't a close.
- Every TCP *Async receive takes an onError(std::exception_ptr): an
exception escaping a ThreadPool task ended the program. An empty onError
drops the error.
- QUIC streams:
- async receives deliver an empty chunk on close, as the browser build does;
- RecieveUntilFullAsync gives what arrived on a stream that closes early;
- RecieveUntilFullSync takes all it asked for or nothing, under one hold
of the lock, so what did arrive stays for the next receive and no
other receive can take bytes from among it;
- SendAsync's callback hears whether the send went out (bool sent): native,
browser, the JS bridge and the example.
- msquic is pinned to v2.6.3 and built against the system OpenSSL
(QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's
openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with
config_diagnostics on) made it fail to initialise. The README says to use
--no-quic where the distro's OpenSSL is older.
- Tests now register on any Linux triple: they never ran under the Flatpak's
x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP,
ShouldRecieveTCP, ShouldReportClosedQUICStream.
Breaking: SendAsync's callback takes a bool, the TCP *Async receives take
an onError, and RecieveSync(size, buffer) returns the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
|
|
|
// A non-zero connectTimeout makes a connect nobody answers throw
|
|
|
|
|
// SocketTimeoutException after that long, where it would otherwise
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
// wait out the kernel's retries, about two minutes. It bounds each
|
|
|
|
|
// address on its own. A refused connect fails at once either way.
|
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived
- --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with
them msquic: a consumer that only needs TCP builds no msquic. The flag
isn't passed on to dependencies.
- --local takes Crafter.Thread from the sibling checkout, as the other
Crafter projects do.
- ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and
SocketTimeoutException.
- TCP receives return the bytes that arrived:
- RecieveSync(n) returned the whole buffer when fewer had come;
- RecieveUntilCloseSync truncated what it had read;
- a signal (EINTR) during a receive threw where it should wait on;
- RecieveSync(size, buffer) returns the count and throws like the others;
- a receive of no bytes returns at once: recv's 0 for it isn't a close.
- Every TCP *Async receive takes an onError(std::exception_ptr): an
exception escaping a ThreadPool task ended the program. An empty onError
drops the error.
- QUIC streams:
- async receives deliver an empty chunk on close, as the browser build does;
- RecieveUntilFullAsync gives what arrived on a stream that closes early;
- RecieveUntilFullSync takes all it asked for or nothing, under one hold
of the lock, so what did arrive stays for the next receive and no
other receive can take bytes from among it;
- SendAsync's callback hears whether the send went out (bool sent): native,
browser, the JS bridge and the example.
- msquic is pinned to v2.6.3 and built against the system OpenSSL
(QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's
openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with
config_diagnostics on) made it fail to initialise. The README says to use
--no-quic where the distro's OpenSSL is older.
- Tests now register on any Linux triple: they never ran under the Flatpak's
x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP,
ShouldRecieveTCP, ShouldReportClosedQUICStream.
Breaking: SendAsync's callback takes a bool, the TCP *Async receives take
an onError, and RecieveSync(size, buffer) returns the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
|
|
|
ClientTCP(const char* host, std::uint16_t port, std::chrono::milliseconds connectTimeout = std::chrono::milliseconds::zero());
|
|
|
|
|
ClientTCP(std::string host, std::uint16_t port, std::chrono::milliseconds connectTimeout = std::chrono::milliseconds::zero());
|
|
|
|
|
~ClientTCP();
|
|
|
|
|
ClientTCP(const ClientTCP&) = delete;
|
|
|
|
|
ClientTCP(ClientTCP&& other) noexcept;
|
|
|
|
|
void Connect(std::chrono::milliseconds timeout = std::chrono::milliseconds::zero());
|
|
|
|
|
void Stop();
|
|
|
|
|
// Turns Nagle's algorithm off (TCP_NODELAY), so a small write goes
|
|
|
|
|
// out at once instead of waiting for the ACK of the one before it.
|
|
|
|
|
// For small messages that must arrive promptly, such as a stream of
|
|
|
|
|
// frames.
|
|
|
|
|
void SetNoDelay(bool enabled);
|
|
|
|
|
// A receive that waits longer than this throws
|
|
|
|
|
// SocketTimeoutException, losing what a whole-buffer receive had read
|
|
|
|
|
// so far. Zero, the default, waits for good.
|
|
|
|
|
void SetReceiveTimeout(std::chrono::milliseconds timeout);
|
|
|
|
|
void Send(const void* buffer, std::uint32_t size) const;
|
|
|
|
|
// The receives return what arrived, at least one byte; asked for none,
|
|
|
|
|
// none, at once, without looking at the socket. They throw
|
|
|
|
|
// SocketClosedException once the peer has closed, and
|
|
|
|
|
// SocketTimeoutException when a receive timeout runs out.
|
|
|
|
|
std::vector<char> RecieveSync() const;
|
|
|
|
|
std::vector<char> RecieveUntilCloseSync() const;
|
|
|
|
|
std::vector<char> RecieveUntilFullSync(std::uint32_t bufferSize) const;
|
|
|
|
|
std::vector<char> RecieveSync(std::uint32_t bufferSize) const;
|
|
|
|
|
// Into `buffer`; returns how many bytes it put there.
|
|
|
|
|
std::uint32_t RecieveSync(std::uint32_t bufferSize, void* buffer) const;
|
|
|
|
|
// The same receives on Crafter.Thread's ThreadPool. What arrived goes
|
|
|
|
|
// to the callback; what a receive throws goes to onError instead (an
|
|
|
|
|
// empty one drops it), as an exception left to escape a pool task
|
|
|
|
|
// would end the program.
|
|
|
|
|
void RecieveAsync(std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const;
|
|
|
|
|
void RecieveUntilCloseAsync(std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const;
|
|
|
|
|
void RecieveAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const;
|
|
|
|
|
void RecieveUntilFullAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const;
|
|
|
|
|
// Into `buffer`, which must outlive the receive; the callback gets the count.
|
|
|
|
|
void RecieveAsync(std::uint32_t bufferSize, std::function<void(std::uint32_t)> recieveCallback, char* buffer, std::function<void(std::exception_ptr)> onError) const;
|
|
|
|
|
private:
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
struct Address {
|
|
|
|
|
sockaddr_storage storage;
|
|
|
|
|
socklen_t length;
|
|
|
|
|
};
|
|
|
|
|
// What the host resolved to, in getaddrinfo's order; empty for a
|
|
|
|
|
// socket handed over by accept().
|
|
|
|
|
std::vector<Address> addresses;
|
feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived
- --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with
them msquic: a consumer that only needs TCP builds no msquic. The flag
isn't passed on to dependencies.
- --local takes Crafter.Thread from the sibling checkout, as the other
Crafter projects do.
- ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and
SocketTimeoutException.
- TCP receives return the bytes that arrived:
- RecieveSync(n) returned the whole buffer when fewer had come;
- RecieveUntilCloseSync truncated what it had read;
- a signal (EINTR) during a receive threw where it should wait on;
- RecieveSync(size, buffer) returns the count and throws like the others;
- a receive of no bytes returns at once: recv's 0 for it isn't a close.
- Every TCP *Async receive takes an onError(std::exception_ptr): an
exception escaping a ThreadPool task ended the program. An empty onError
drops the error.
- QUIC streams:
- async receives deliver an empty chunk on close, as the browser build does;
- RecieveUntilFullAsync gives what arrived on a stream that closes early;
- RecieveUntilFullSync takes all it asked for or nothing, under one hold
of the lock, so what did arrive stays for the next receive and no
other receive can take bytes from among it;
- SendAsync's callback hears whether the send went out (bool sent): native,
browser, the JS bridge and the example.
- msquic is pinned to v2.6.3 and built against the system OpenSSL
(QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's
openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with
config_diagnostics on) made it fail to initialise. The README says to use
--no-quic where the distro's OpenSSL is older.
- Tests now register on any Linux triple: they never ran under the Flatpak's
x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP,
ShouldRecieveTCP, ShouldReportClosedQUICStream.
Breaking: SendAsync's callback takes a bool, the TCP *Async receives take
an onError, and RecieveSync(size, buffer) returns the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
|
|
|
};
|
|
|
|
|
}
|
2026-05-19 02:53:50 +02:00
|
|
|
#endif
|