Network library based on C++23 modules.
- Clang 21+ on Linux (
clang++-21; CB and CI default to LLVM 21) - macOS: locally built LLVM at
/usr/local/llvm(seedeps/testerREADME) - Open in a dev container (
.devcontainer/) for a reproducible Linux toolchain
This subproject is designed to work both:
- Standalone (as its own repo, with
deps/testeravailable), and - As a dependency inside YarDB (reusing YarDB’s
deps/tester).
Build and run tests with the project’s C++ Builder wrapper:
cd deps/net
tools/CB.sh debug testBy default, network tests run (they may require local privileges, free ports, multicast enabled, etc.).
To disable network/integration tests:
NET_DISABLE_NETWORK_TESTS=1 tools/CB.sh debug testNotes:
- In Cursor sandbox environments,
tools/CB.shauto-setsNET_DISABLE_NETWORK_TESTS=1unless you override it. - Multicast tests depend on the host/network allowing multicast.
import net;
import std;
try
{
using namespace std::string_literals;
using namespace net;
auto ator = acceptor{"::1", "2112"}; // IPv6 localhost
auto [stream,client,port] = ator.accept();
while(stream)
{
auto echo = ""s;
getline(stream, echo);
stream << echo << endl;
clog << echo << endl;
}
}
catch(const exception& e)
{
cerr << "Exception: " << e.what() << endl;
}import net;
import std;
try
{
using namespace net;
auto s = connect("www.google.com", "http");
s << "GET / HTTP/1.1" << crlf
<< "Host: www.google.com" << crlf
<< "Connection: close" << crlf
<< "Accept: text/plain, text/html" << crlf
<< "Accept-Charset: utf-8" << crlf
<< crlf
<< flush;
while(s)
{
auto c = ' ';
s >> noskipws >> c;
clog << c;
}
clog << flush;
}
catch(const exception& e)
{
cerr << "Exception: " << e.what() << endl;
}When building HTML responses by hand, escape untrusted data at the point of output.
http::html_escaped and http::url_encoded are stream adaptors — they write directly
into an ostringstream without per-field std::string temporaries.
import net;
import std;
using namespace std::string_literals;
auto symbol = "NOKIA"s;
auto reference = R"(ACC+"<x>)"s;
auto contents = std::ostringstream{};
contents << "<td>" << http::html_escaped{symbol} << "</td>";
contents << "<a href=\"/cancel?ref=" << http::url_encoded{reference} << "\">Cancel</a>";
// Decode URL-encoded values with net::url_decode (net:uri module).
auto decoded = net::url_decode("ACC%2B%22%3Cx%3E");Use html_escaped for text nodes and attribute values. Use url_encoded for query
parameter values in href and action URLs. The HTTP server does not escape
response bodies automatically.
SSE is a connection takeover on http::server, parallel to WebSocket. Register with
server.sse(path).sse(handler); a matching GET returns 200 with
Content-Type: text/event-stream (no Content-Length), then runs
http::sse::session until the handler returns or the write path fails.
import net;
import std;
auto server = http::server{};
auto allow = [](std::string_view o) { return o == "http://localhost:3000"; };
// Browser preflight (reuse cors_middleware). Reflect the same Origin allowlist
// on the SSE response head via .cors(...).
server.options("/events").response("text/plain",
http::middleware::cors_middleware(allow, [](auto&&, auto&&, auto&&) {
return http::make_response(http::status_no_content, "");
}));
server.sse("/events").cors(allow).sse([](http::sse::session& session, auto, http::headers& hdr) {
if(hdr.contains("last-event-id"))
{
// resume policy is application-defined
}
session.send_comment("ok");
session.send_event("tick", "1", "1");
});
server.listen("127.0.0.1", "8080");Notes:
- Flush after each event/comment (session does this). Proxies that buffer SSE may
need
X-Accel-Buffering: no(optional app header — not set by default). - After the SSE stream ends, the connection is closed (no keep-alive reuse).
- MCP over SSE transport (
http::mcp::sse_transport) is on branchfeature/mcp-sse-transport; seedocs/sse-mcp-implementation-plan.md.
Legacy MCP HTTP+SSE (compatible with mcp.server.sse.SseServerTransport):
GET /sse→event: endpointwith/messages/?session_id=<uuid4 hex>POSTJSON-RPC to that URI →202 Accepted- Replies as SSE
event: message
http::mcp::server runs initialize / ping / tools/list / tools/call and
applies Host/Origin allowlists (localhost defaults; disable with
dns_rebinding_protection(false)). Apps supply tool callbacks only.
import net;
import std;
auto http = http::server{};
auto mcp = http::mcp::server{"/messages/"};
mcp.info({.name = "demo", .version = "1.0.0"})
.list_tools([] {
return std::vector<http::mcp::tool_spec>{
{.name = "health", .description = "liveness"},
};
})
.call_tool([](std::string_view, std::string_view) {
return http::mcp::tool_result{.text = "ok"};
})
.attach(http, "/sse");
http.listen("127.0.0.1", "8080");Low-level sse_transport remains available for custom session loops. Keep the
mcp / transport object alive for the lifetime of listen.
Upgrade is handled inside http::server (not as response middleware). Register a path
with server.ws(...).ws(handler); a matching GET with Upgrade: websocket returns
101 and runs a text-frame session (ping/pong/close + optional text replies).
Clients use websocket::connect for the same text session (masked outbound frames,
automatic ping/pong, send / recv / read_loop / close). Overloads take
(host, port, path) or a net::uri (ws://host:port/path). The connect timeout
bounds TCP connect and the full upgrade-response read (status line and headers).
close() waits briefly for the peer close frame (including truncated frames), then
force-closes (default 2s; RFC 6455 §7.1.1). No wss:// — terminate TLS in front of
the server if needed.
v1 framing policy (fail closed with a close frame, no silent drops):
- Client-to-server frames must be masked (
1002if not). - Complete text frames only —
FIN=0,continuation, andbinary→1003. - Text payloads must be valid UTF-8 (
1007); no xson dependency. - Control frames must be FIN and ≤ 125 bytes; RSV bits must be 0 (
1002). - Payloads larger than 1 MiB →
1009.
import net;
import std;
// Server
auto server = http::server{};
server.ws("/events").ws([](std::string_view msg) {
return net::websocket::text_reply{std::string{msg}}; // echo
});
server.listen("127.0.0.1", "8080");
// Client
auto ws = net::websocket::connect("127.0.0.1", "8080", "/events");
// or: auto ws = net::websocket::connect(net::uri{"ws://127.0.0.1:8080/events"});
ws.send("hello");
if(auto reply = ws.recv())
{
// …
}
// or: ws.read_loop([](std::string_view msg) { … });
ws.close();import net;
import std;
using namespace net;
using namespace std::string_literals;
slog.level(syslog::severity::info);
slog.facility(syslog::facility::local0);
slog.appname("example");
auto clothes = "shirts"s; auto spouse = "wife"; auto wrong = false;
slog << debug << "... " << 3 << ' ' << 2 << ' ' << 1 << " Liftoff" << flush;
slog << info << "The papers want to know whose " << clothes << " you wear..." << flush;
slog << notice << "Tell my " << spouse << " I love her very much!" << flush;
slog << warning << "Ground Control to Major Tom Your circuit's dead, there's something " << boolalpha << wrong << '?' << flush;
slog << error << "Planet Earth is blue and there's nothing I can do." << flush;The structured log stream supports method chaining for convenient configuration:
slog.format(net::log_format::jsonl)
.app_name("my-service")
.log_level(syslog::severity::info)
.sd_id("app")
.redirect("logs/app.log");All configuration methods return structured_log_stream& to enable fluent chaining.
// Using pair syntax
slog << info << std::pair{"user_id", 42} << std::pair{"ip", "192.168.1.1"} << "User logged in" << flush;
// Using field() convenience method
slog << info << slog.field("user_id", 42) << slog.field("ip", "192.168.1.1") << "User logged in" << flush;Automatically capture file, line, and function information:
slog << info << std::source_location::current() << "Operation completed" << flush;
// Or with structured fields
slog << error
<< std::source_location::current()
<< slog.field("duration_ms", 127)
<< "Request failed"
<< flush;if (slog.is_enabled(syslog::severity::debug)) {
// Expensive debug computation
auto details = compute_debug_info();
slog << debug << details << flush;
}