reactor
July 3, 2026 · View on GitHub
A generic Asio server runtime: the shared-nothing, thread-per-core reactor pool plus accept plumbing plus graceful shutdown that every network protocol needs, with the protocol itself left entirely to you. On standalone Asio (C++20 coroutines), header-only.
Every TCP server re-implements the same three things: a pool of event loops across cores, the accept loop that binds one port and fans connections out to them, and the shutdown dance that tears it all down without a hang. This library owns exactly that, and nothing else. You hand it a Session — a coroutine that serves one accepted connection — and it runs it for you:
#include "reactor.h"
// The protocol lives entirely here: read a line, echo it back, keep-alive until EOF.
asio::awaitable<void> echo(asio::ip::tcp::socket socket, reactor::Reactor& r) {
std::string buf; char tmp[1024];
for (;;) {
std::size_t n = co_await socket.async_read_some(asio::buffer(tmp), asio::use_awaitable);
if (n == 0) co_return; // peer closed
co_await asio::async_write(socket, asio::buffer(tmp, n), asio::use_awaitable);
}
}
int main() {
reactor::TcpServer server(/*reactors*/ 4, /*offload workers*/ 2, /*queue*/ 256, echo);
server.start(8080); // binds the port, runs 4 loops on 4 threads, returns
// ... run until a signal ...
server.stop(); // aborts in-flight work, stops the loops, joins
}
The runtime knows nothing about HTTP, a binary framing, or a wire format. It hands
the Session a socket and its Reactor and gets out of the way.
Kronuz/http rides on it; a Xapian remote-backend
server, a replication server, or a future service ride on the same runtime instead
of each re-writing the accept loop, the offload pool, and the shutdown.
What it gives a Session
- N shared-nothing reactors on N threads. Thread-per-core: no shared state between loops, no global lock, no cross-core bounce.
- One port, two bind modes, chosen automatically.
SO_REUSEPORT(Linux) so each reactor binds its own acceptor and the kernel load-balances; a single portable shared acceptor everywhere else (macOS/BSD reject a second same-port bind). - A per-reactor offload pool (
Reactor::pool()) for the blocking or CPU-heavy work that must not run on the loop, with a bounded admission gate (Reactor::gate()) so a saturated pool sheds load instead of growing an unbounded backlog. - A clean shutdown. Register anything a worker can block on as an
Abortable(Reactor::track());stop()aborts them all before stopping the loops, so a connection parked on a half-read request or a full queue unwedges and the threads join instead of hanging.
UDP too (reactor_udp.h)
The connectionless counterpart, reactor::UdpServer, is one reactor owning one bound
UDP socket and an async receive loop that hands each datagram to a Datagram handler.
It carries the full multicast toolkit a gossip/discovery protocol needs (reuse_port
so N nodes share a port on one host, IP_MULTICAST_LOOP, IP_MULTICAST_TTL, join a
group, large SND/RCV buffers). No per-connection Session — UDP is connectionless — and
the framing is still entirely yours. Its io() is exposed so a single-threaded protocol
runs its own timers on the same loop as the receive (no locking between them).
#include "reactor_udp.h"
reactor::UdpServer disc([](std::string_view data, const asio::ip::udp::endpoint& from) {
handle_gossip(data, from); // runs on the reactor thread, next to your timers
});
reactor::UdpOptions o;
o.multicast_group = "239.192.168.1";
o.multicast_loop = true; // one-host clusters see their own sends
o.reuse_port = true; // N nodes share the port
disc.set_options(o);
disc.start(58880);
disc.send(bytes); // to the group
Dependencies
asio (standalone, ASIO_STANDALONE), pulled in by CMake FetchContent; otherwise header-only and standard-library only (C++20 coroutines). The protocol/session and any logging are supplied above the runtime, not depended on here.
Build
cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
ctest --test-dir build --output-on-failure
Header-only: to use it, add the directory to your include path (or FetchContent
this repo) and link reactor::reactor. Standalone Asio is fetched at configure time,
or point -DASIO_INCLUDE_DIR=/path/to/asio/include at an existing checkout.
test/test.cc is the whole TCP surface end-to-end: a line-echo server across both bind
modes, an offloaded slow request that keeps the reactor free, and a stop() that
unblocks a session parked on an Abortable. test/udp_test.cc covers the UDP transport:
unicast, multicast loopback between two sockets sharing a port, an app timer on the same
loop, and a clean stop(). examples/echo_server.cc (reactor_echo_server) is a runnable
TCP echo server; benchmarks/echo_bench.cc (reactor_echo_bench) measures the pool's
request throughput + latency (~55k req/s at 4 reactors on a laptop).