Cross-platform C++20 networking library providing TCP, UDP, and TLS sockets — both blocking and reactor-driven async variants — with a uniform interface enforced by C++20 concepts. Error handling uses stl::result<T> from sap_core — no exceptions.
Part of the Sapfire library ecosystem.
TCPSocket— stream socket.bind,listen,connect,accept,send,recv,close. Configurable send/recv timeouts.UDPSocket— datagram socket.TLSSocket— TLS 1.2/1.3 over TCP via OpenSSL. Supports both client and server roles, ALPN negotiation, post-handshake cipher/protocol/certificate introspection.
TCPSocketAsync— non-blocking TCP. Same lifecycle asTCPSocketbutconnect/accept/read/writeare coroutines returningsap::async::Task<stl::result<...>>. Driven by asap::async::Executorand the underlyingsap::io::Reactor(epoll on Linux, IOCP+AFD on Windows).TLSSocketAsync— non-blocking TLS. OpenSSLBIOdriven by a WANT_READ / WANT_WRITE state machine, so handshake / read / write / close all suspend on the reactor instead of blocking.
Both async sockets:
- Take a
sap::async::Executor&at construction; they don't own the loop. - Accept an optional
sap::async::StopTokenper operation; cancellation throwsCancelledErrorat the await boundary. - Use a single in-flight flag to enforce one outstanding operation per direction (no concurrent reads / no concurrent writes on the same socket).
template<typename T>
concept Socket = requires(T s, stl::span<const stl::byte> data, stl::span<stl::byte> buf) {
{ s.send(data) } -> std::same_as<stl::result<size_t>>;
{ s.recv(buf) } -> std::same_as<stl::result<size_t>>;
{ s.close() } -> std::same_as<void>;
{ s.valid() } -> std::same_as<bool>;
};
template<typename T>
concept SocketAsync = requires(T s, stl::span<const stl::byte> wdata, stl::span<stl::byte> rdata, sap::async::StopToken tok) {
{ s.connect(tok) } -> std::same_as<sap::async::Task<stl::result<>>>;
{ s.read(rdata, tok) } -> std::same_as<sap::async::Task<stl::result<size_t>>>;
{ s.write(wdata, tok) } -> std::same_as<sap::async::Task<stl::result<size_t>>>;
{ s.close() } -> std::same_as<void>;
{ s.valid() } -> std::same_as<bool>;
};TCPSocket and TLSSocket satisfy Socket; TCPSocketAsync and TLSSocketAsync satisfy SocketAsync. Both checks are verified by static_assert in the relevant headers.
platform.h normalises socket handles and errno across Windows (WinSock2) and POSIX. All platform differences are isolated to *_windows.cpp / *_posix.cpp translation units.
TLSSocket has three internal states encoded in a std::variant<TlsClientConfig, TlsServerConfig, TlsAcceptedConfig>:
- Client —
connect()performs TCP connect + TLS handshake + certificate verification. - Server —
bind()/listen()/accept()accept incoming TCP connections and complete the TLS handshake. - Accepted — returned from
accept(), ready forsend()/recv().
The config variant is declared before the TCPSocket member — member initialisation order is load-bearing here since the TCP socket constructor reads from the active variant.
Post-handshake you can query negotiated_protocol() (ALPN result), negotiated_cipher(), negotiated_tls_version(), peer_cert_subject(), and peer_cert_issuer().
Requires CMake 3.20+, C++20, OpenSSL, and sap_core.
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build#include <sap_network/tcp_socket.h>
sap::network::TCPSocket sock{SocketConfig{.host = "127.0.0.1", .port = 8080}};
if (!sock.connect())
return;
std::array<std::byte, 1024> buf;
auto result = sock.recv(buf);
if (!result)
std::println("recv error: {}", result.error());
else
std::println("received {} bytes", result.value());#include <sap_network/tls_socket.h>
// TLS client
sap::network::TLSSocket tls{TlsClientConfig{.host = "example.com", .port = 443}};
if (!tls.connect()) {
std::println("handshake failed: {}", tls.handshake_error());
return;
}
std::println("cipher: {}", tls.negotiated_cipher());#include <sap_network/tcp_socket_async.h>
#include <sap_core/async/executor.h>
#include <sap_core/async/spawn.h>
auto ex_r = sap::async::Executor::create();
auto& ex = ex_r.value();
sap::network::TCPSocketAsync sock(ex, sap::network::SocketConfig{.host = "127.0.0.1", .port = 8080});
auto fetch = [&]() -> sap::async::Task<stl::result<>> {
if (auto r = co_await sock.connect(); !r) co_return r;
const char* msg = "ping";
auto w = co_await sock.write(stl::span<const stl::byte>(reinterpret_cast<const stl::byte*>(msg), 4));
if (!w) co_return stl::make_error<>("{}", w.error());
stl::byte buf[64];
auto r = co_await sock.read(stl::span<stl::byte>(buf, sizeof(buf)));
if (!r) co_return stl::make_error<>("{}", r.error());
co_return stl::result<>{};
};
auto handle = sap::async::spawn(ex, fetch());
sap::async::sync_wait(stl::move(handle)); // drives the executor