mynet is an ultra-high-performance, modern C++26 asynchronous network framework and protocol toolkit designed for Linux. Built from the ground up for microsecond-level latency, zero-copy/zero-allocation hot paths, and robust software engineering practices, mynet provides dual I/O engines (Reactor powered by edge-triggered epoll and Proactor powered by io_uring with kernel buffer rings), unified stream and datagram transports (TCP, TLS, UDP), an HTTP/2 wire-compatible stream multiplexer, and strongly-typed error semantics.
- Key Features
- Architecture
- Core Components
- Benchmark Results
- Dependencies & Prerequisites
- Building & Installation
- Quick Start Guide
- Project Layout
- Testing & Quality Assurance
- License
-
Modern C++26 Paradigm:
- Leverages C++26 Preconditions / Contracts (
-fcontracts) for strict caller assertion. - Informative compile-time deletion reasons via
= delete("reason")(P2573R2). - Modern concepts & constraints (
std::derived_from,std::same_as, custom requires clauses). -
std::expectedandstd::optionalreturn pathways eliminating magic numbers.
- Leverages C++26 Preconditions / Contracts (
-
Dual Asynchronous Engines:
-
Reactor: Edge-Triggered
epoll(EPOLLET) witheventfdcross-thread wakeups and a lock-free/lazy-cancelling$O(1)$ priority timer queue. -
Proactor: Pure asynchronous
io_uringutilizing multishot accepts (io_uring_prep_accept), multishot receives (io_uring_prep_recv_multishot), kernel-managed registered buffer rings (io_uring_setup_buf_ring), and poll-driven TLS.
-
Reactor: Edge-Triggered
-
Zero-Allocation Hot Paths:
- Address-stable fixed-capacity
ObjectPool<T>preventing memory reallocation and pointer invalidation during event dispatch. - Sliding-window auto-compacting
Bufferdesigned for long-lived connections.
- Address-stable fixed-capacity
-
Extreme Zero-Copy Data Pipelines:
- Linux
splice(2)kernel-level zero-copy looping: a handler that declareskEnableSplice = truegets an in-kernel non-blocking pipe buffer (socket -> pipe -> socket) on plain TCP, delivering 0 user-space copies and 0 kernel memory copies (SpliceEchoHandlerinexamples/common/handlers.hppshows the opt-in). - Direct writable ingress: connections read directly into pre-allocated
Bufferwrite windows viaprepareRequestWritable, eliminating stack scratch buffers and intermediarymemcpy. - Vectorized scatter-gather I/O: plain connections aggregate up to 16 response segments per single
writev(2)syscall.
- Linux
-
Transparent Security:
- First-class OpenSSL 3.x integration with zero code duplication between plain TCP and TLS pipelines.
- Full support for ALPN negotiation, SNI host matching, and asynchronous TLS handshakes.
-
Protocol Engineering & Framing:
- HTTP/2 wire-compatible 9-byte frame codec (
Length:24,Type:8,Flags:8,StreamID:31). - Generalized
HandlerHttp2<StreamHandler>supporting concurrent stream management, priority, ping, reset, connection/stream-level flow control (WINDOW_UPDATE), and gracefulGOAWAYteardown. - HTTP/1.x codec powered by
llhttpsupporting pipelining and keep-alive.
- HTTP/2 wire-compatible 9-byte frame codec (
-
Observability & Operability:
- Exhaustive enum class error codes (
ErrandIoEnd). - Clean separation: data flows over stdout, structured diagnostic logging routes to
stderrviamynet::log(wrapping submodulemylog), preventing stream pipeline corruption. -
Sandboxed Lua Hot-Configuration & Lockless Snapshots:
mynet::reloadConfig("config/config.lua")executes in a barelua_State(no standard libraries likeos/ioto prevent code injection) with an instruction budget limit of 50M steps; supports transactional validation (All-or-Nothing) and value clamping. Backed byConfigHolderandConfigSnapshotfor lock-free read-paths, and seamlessly synchronizes withmylog's logging configuration in a unified config file.
- Exhaustive enum class error codes (
The following diagram illustrates the layered architecture of mynet:
flowchart TD
subgraph App ["Application & Protocol Handlers"]
direction LR
ConnHandler["HandlerConnection (stream sessions)"]
HandlerHttp["HandlerHttp (HTTP/1.x)"]
Mux["HandlerHttp2<StreamHandler> (HTTP/2)"]
DatagramHandler["HandlerDatagram (datagram sessions)"]
end
subgraph Core ["Connection & Transport Abstractions"]
direction LR
Conn["Connection<Handler>"]
Dgram["Datagram<Handler>"]
Session["Proactor::Session"]
PeerCl["Client<Event> (Client)"]
Datagram["DatagramReply"]
end
subgraph Engine ["Dual I/O Engines"]
direction LR
ReactorEngine["Reactor (epoll + EPOLLET + TimerQueue + eventfd)"]
ProactorEngine["Proactor (io_uring + multishot recv + buf_ring + poll TLS)"]
end
subgraph Foundation ["Memory, Primitives & Diagnostics"]
direction LR
Pool["ObjectPool<T>"]
Buf["Buffer"]
Constraints["constraints"]
Err["Err & IoEnd"]
Log["mynet::log (mylog -> stderr)"]
end
App --> Core
Core --> Engine
Engine --> Foundation
| Feature | Reactor (Reactor<Event>) |
Proactor (Proactor<Event>) |
|---|---|---|
| Kernel Subsystem | Linux epoll (Edge-Triggered, EPOLLET) |
Linux io_uring |
| Notification Model | Readiness Notification | Asynchronous Completion Notification |
| Buffer Management | Userspace Buffer per Connection |
Kernel-managed registered buffer ring (io_uring_buf_ring) |
| Syscall Overhead | One epoll_wait + subsequent non-blocking recv/send |
Batch completion via io_uring_wait_cqe, multishot syscall-free reads |
| TLS Integration | Direct non-blocking OpenSSL SSL_read/SSL_write |
Poll-driven readiness mask (io_uring_prep_poll_add) to avoid ciphertext detachment |
| Cross-Thread Dispatch | eventfd wakeup + loop-bound task queue (runInLoop) |
std::stop_source / submission ring scheduling |
Connection<HandlerType> unifies socket state, TLS encryption, protocol handling, and outgoing write queues:
- Transport Symmetry: Plain TCP and encrypted TLS share the same data path (
doRecv/doSend). TLS is treated as a transport property rather than requiring a separate loop or wrapper. - Pipelining Safety: Responses reside in an internal fixed-capacity
RingQueue<std::string>write queue by value. Multiple responses generated from a single batch read are guaranteed to arrive in-order without dynamic allocations during enqueuing or overwriting shared buffers. - Backpressure: Integrated high-watermark inspection and
readCredit()hooks pause reads when downstream consumer queues become saturated. - Reference-Counted Lifetime in Proactor: In
Proactor, connections are wrapped insideSessionstructs trackinginFlightring operations. Connections are never returned to the pool while the kernel has outstanding writes or reads. - Handler Layering:
HandlerConnection(connection.hpp) andHandlerDatagram(datagram.hpp) are two peer base classes with no shared ancestor: the former owns the request buffer, response queue and read-credit backpressure for stream sessions, the latter the peer-tagged reply queue for connectionless datagrams. Protocol handlers (HandlerHttp,HandlerHttp2,HandlerHttp3) derive from these; a handler that needs both transports (HandlerHttp3) inherits both, and since neither base is virtual there is no virtual-base indirection or shared subobject bookkeeping on member access. The contract that used to sit in a common root —reset,onMessageComplete,onParseError— now lives on each base class and, transport-agnostically, in theHandlerConstraintconcept (constraints.hpp), which is all the event slots require (the object pool needs onlyResettable). The two engine contracts are named for what they are — the mandatory method set each transport actually calls:MandatoryMethodConnection(connection.hpp) andMandatoryMethodDatagram(datagram.hpp). Each lives beside its own base class sostd::derived_fromalways sees a complete type, and each requires derivation plus validates the signatures the engine calls through the handler type itself. A datagram-only handler needsHandlerDatagramalone: the engines map non-streaming handlers to a statelessNoConnectionslot (ConnectionSlot) and stream-only handlers to aNoDatagramslot (DatagramSlot), so neither path instantiates the other transport's machinery.
-
ObjectPool<Object>: Pre-allocates a contiguous array of slots viastd::unique_ptr<Slot[]>with an index-based ring queue (RingQueue<size_t>). Objects are pinned in memory so epoll data pointers and io_uring completion context pointers remain 100% stable, completely bypassing heap allocation during connection accept/release cycles. -
Buffer: A zero-exception, self-managed contiguous byte buffer (operator new[](std::nothrow)) with sliding-window auto-compaction and direct zero-copy write window (writableBytes(),writableBegin(),commitWritten(),ensureWritable(),readableView()). Storage is compacted in-place only when dead prefix space exceeds 4 KiB and occupies at least half of the allocation, amortizing compaction overhead while keeping memory footprints bounded. -
RingQueue<T>: Fixed-capacity contiguous circular ring buffer (src/ringQueue.hpp) providing$O(1)$ zero-allocation enqueue/dequeue operations with bounded memory footprints. -
StreamTable<T>: Open-addressing flat hash table (src/http/v2/streamTable.hpp) with linear probing, tombstone deletion, and tombstone recycling, achieving zero runtime allocations for active stream management.
- Wire Compatibility:
FrameDecoderandappendFrameparse and construct 9-byte HTTP/2 standard frames (DATA,HEADERS,PRIORITY,RST_STREAM,SETTINGS,PUSH_PROMISE,PING,GOAWAY,WINDOW_UPDATE). HandlerHttp2<StreamHandler>: Enables many independent logical request/response streams over a single byte stream.- Implements credit-based flow control at both connection and stream levels.
- Utilizes
mux::StreamTable<StreamSlot>for zero-allocation stream lookups and lifecycles. - Automatically sends
WINDOW_UPDATEframes when window thresholds drop below half. - Handles concurrent stream capacity limits via
SETTINGS_MAX_CONCURRENT_STREAMSand rejects over-capacity requests withRST_STREAM. - Dispatches to individual
StreamHandlerinstances sourced from anObjectPool.
HandlerDatagram: Unlike connected stream sockets, UDP sockets are connectionless and must handle interleaved peers on a single file descriptor.HandlerDatagramassociates peer addresses (sockaddr_in) directly with message buffers (DatagramReply). It is the protocol-side root: it only decides what to reply.Datagram<Handler>: The transport-side counterpart ofConnection<Handler>on the UDP path (datagram.hpp). It binds one handler to one socket and owns the fixed-capacity receive window plus the send/receive counters; truncation againstmaxDatagramBytes()and reply draining live here, so both engines shrink to event dispatch (recvfrom intorecvBuffer(), hand it toreceiveFrom(), drain withflushTo()). It deliberately carries no fd, no peer address and no write queue — UDP is connectionless, so those would be per-datagram state, not per-session state. Its receive/send counters are diagnostic scaffolding for unit tests, not production metrics:runUdpcallsreset()before returning and no accessor exposes the session, so they are not observable from a running server.DatagramSlot<H>picksDatagram<H>or the inertNoDatagramat compile time, mirroringConnectionSloton the stream side.- Overload Handling: Bounded by
maxPendingReplies; once the reply queue is full, further datagrams are dropped with explicit metrics logging, preventing unbounded memory growth under sudden traffic spikes.
- No Magic Numbers:
enum class ERR : uint8_t: Comprehensive, library-wide runtime errors (ADDR_INVALID,SOCKET_FAILED,SSL_CTX_FAILED,URING_BUF_RING_FAILED, etc.) returned viaResult<T = void>(std::expected<T, ERR>).enum class IO_END : uint8_t: Explicit, non-error I/O state explanations (WOULDBLOCK,INTERRUPTED,EOF_REACHED,FATAL) returned viastd::expected<size_t, IO_END>.
- Precondition Contracts: Contract violations (
contract_assert) catch programmer errors (e.g., null pointers, out-of-range port numbers) at translation boundaries.
Client<Event> is an idiomatic Unix-style pipeline client bridging stdin/stdout and network sockets:
cat request.bin | ./build/client_tcp 127.0.0.1:8888 > response.binFeatures watermark-based backpressure for stdin, graceful TCP/TLS half-close forwarding (close_notify / SHUT_WR), signal-safe blocking of SIGPIPE, and detection of regular file vs interactive terminal descriptors.
Benchmarks run on Linux (x86_64, GCC 15, Release build -O2, single process, with logging redirected away from the measurement path):
benchmark latency throughput
--------------------------------------------------------------------------------
Buffer::append+retrieveAll (1KiB) 12.7 ns/op 80,477.0 MB/s
Buffer::append (16B) to 1KiB + drain 2.5 ns/op --
Buffer::append+retrieve (16B) 4.0 ns/op 4,041.9 MB/s
appendFrame (payload 16B) 4.5 ns/op 3,555.4 MB/s
appendFrame (payload 1024B) 12.3 ns/op 83,077.8 MB/s
appendFrame (payload 16384B) 223.4 ns/op 73,328.8 MB/s
FrameDecoder::next (payload 16B) 12.6 ns/op 1,268.1 MB/s
FrameDecoder::next (payload 1024B) 77.5 ns/op 13,216.9 MB/s
FrameDecoder::next (payload 16384B) 1104.7 ns/op 14,831.4 MB/s
read+appendBigEndian32 0.7 ns/op --
ObjectPool acquire+release (128) 3.8 ns/op --
HandlerConnection::pushResponse+takeResponse (1KiB) 22.5 ns/op 45,605.2 MB/s
HandlerConnection::process echo (1KiB) 47.1 ns/op 21,760.2 MB/s
HandlerHttp2 open+echo+close (64 streams) 553.4 ns/op --
- Operating System: Linux (Kernel >= 5.19 recommended for full
io_uringbuffer ring and multishot support). - C++ Compiler: GCC 14+ or GCC 15+ with C++26 and
-fcontractssupport. - Build System: CMake >= 3.25.
- System Libraries:
liburing-dev(>= 2.2)libssl-dev(OpenSSL 3.x)libgtest-dev(Google Test, for unit tests)pkg-configllhttp
On Debian/Ubuntu 25.10+:
sudo apt-get update
sudo apt-get install -y g++ cmake make pkg-config libssl-dev liburing-dev libgtest-dev libpq-devgit clone --recurse-submodules https://github.com/printf033/mynet.git
cd mynetIf already cloned without submodules:
git submodule update --init --recursivemkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)| Option | Default | Description |
|---|---|---|
CMAKE_BUILD_TYPE |
Debug |
Build profile (Debug, Release, RelWithDebInfo) |
BUILD_TESTING |
ON |
Build Google Test unit test executables |
MYNET_BUILD_BENCH |
ON |
Build micro-benchmark executables (bench/bench) |
A multi-stage Dockerfile based on Ubuntu 25.10 is included in the root directory:
docker build -t mynet:latest .
docker run --rm -it -p 8888:8888 mynet:latest#include "connection.hpp"
#include "error.hpp"
#include "log_config.hpp"
#include "reactor.hpp"
using namespace mynet;
int main() {
mynet::log::init(); // Directs logs to stderr
// Instantiate a Reactor with plain socket events and an echo handler
Reactor<EventSocket<HandlerConnection>> server;
if (auto res = server.runTcp("0.0.0.0", 8888); !res) {
LOG_ERROR("Server failed: {}", toString(res.error()));
return toExitCode(res.error());
}
return 0;
}#include "connection.hpp"
#include "error.hpp"
#include "log_config.hpp"
#include "proactor.hpp"
using namespace mynet;
int main() {
mynet::log::init();
// Instantiate a Proactor using io_uring multishot accept & multishot recv
Proactor<EventSocket<HandlerConnection>> server;
// Parameters: IP, Port, backlog, SQ entries, CQ entries, max accept ops, session pool, buf ring count, buf size
if (auto res = server.runTcp("0.0.0.0", 8888, 511, 512, 1024, 256, 512, 1024, 4096); !res) {
LOG_ERROR("Proactor failed: {}", toString(res.error()));
return toExitCode(res.error());
}
return 0;
}#include "connection.hpp"
#include "error.hpp"
#include "http/v2/handler.hpp"
#include "reactor.hpp"
using namespace mynet;
int main() {
mynet::log::init();
// Stack: Reactor -> Socket -> HandlerHttp2 (framing + flow control) -> per-stream handler
Reactor<EventSocket<HandlerHttp2<HandlerConnection>>> server;
if (auto res = server.runTcp("0.0.0.0", 8888); !res) {
LOG_ERROR("HTTP/2 server failed: {}", toString(res.error()));
return toExitCode(res.error());
}
return 0;
}#include "client.hpp"
#include "connection.hpp"
#include "error.hpp"
#include "log_config.hpp"
using namespace mynet;
int main(int argc, char* argv[]) {
mynet::log::init();
Client<EventSocket<HandlerConnection>> client;
// Connects to target server, streams stdin -> server and server -> stdout
if (auto res = client.runTcp("127.0.0.1", 8888); !res) {
LOG_ERROR("Client error: {}", toString(res.error()));
return toExitCode(res.error());
}
return 0;
}Every sample takes a single <host>:<port> argument and falls back to its own default:
# Client: stream stdin to the server, print whatever comes back
printf "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" | ./build/client_tcp 127.0.0.1:8888
# Server: pick any endpoint you like (defaults to 0.0.0.0:8888, HTTP/3 uses 4433)
./build/reactor_http1 127.0.0.1:9000
curl -X POST --data-binary 'hello' http://127.0.0.1:9000/The full matrix is client_{tcp,ssl,udp} for the client and
{reactor,proactor}_{tcp,ssl,udp,http1,http2,http3} for the servers.
mynet/
├── CMakeLists.txt # Top-level CMake configuration (C++26 standard, contracts)
├── Dockerfile # Multi-stage containerized build configuration
├── LICENSE # MIT License
├── README.md # English Documentation
├── README_zh.md # Chinese Documentation
│
├── src/ # Core Header-Only Implementation
│ ├── CMakeLists.txt # Header-only interface library definition (INTERFACE mynet & mynet_lua)
│ ├── constraints.hpp # Generic constraints (Resettable / HandlerConstraint)
│ ├── configHolder.hpp # Global network configuration & generation snapshots (NetConfig / ConfigHolder)
│ ├── lua_config.hpp # Sandboxed Lua config loader & hot reload (reloadConfig)
│ ├── error.hpp # Strongly-typed Err & IoEnd + errno/OpenSSL text helpers
│ ├── log_config.hpp # Logging setup (mylog -> stderr)
│ ├── buffer.hpp # Low-allocation auto-compacting byte buffer
│ ├── ringQueue.hpp # Zero-allocation fixed-capacity circular ring queue
│ ├── objectPool.hpp # Fixed-capacity stable-pointer object pool
│ ├── timer.hpp # Lazy-cancellation binary min-heap timer queue
│ ├── loop.hpp # epoll EventLoop with eventfd wakeups
│ ├── event.hpp # Listener root (EventBase) + socket & SSL channels
│ ├── connection.hpp # TCP/TLS connection engine + HandlerConnection
│ ├── datagram.hpp # UDP transport (Datagram<H>) + HandlerDatagram
│ ├── http/ # HTTP protocol implementations
│ │ ├── v1/
│ │ │ └── handler.hpp # HTTP/1.x protocol handler (HandlerHttp)
│ │ ├── v2/
│ │ │ ├── frame.hpp # HTTP/2 wire-compatible framing codec
│ │ │ ├── streamTable.hpp # Open-addressing zero-allocation stream hash table
│ │ │ └── handler.hpp # HTTP/2 handler: framing, stream table, credit multiplexer
│ │ └── v3/
│ │ ├── varint.hpp # RFC 9000 QUIC/HTTP/3 variable-length integer codec
│ │ ├── frame.hpp # RFC 9114 HTTP/3 framing, settings and error codes
│ │ ├── qpack.hpp # RFC 9204 QPACK static table and header field codec
│ │ ├── quic.hpp # RFC 9000 QUIC short-header packet & STREAM frames
│ │ ├── streamTable.hpp # HTTP/3 logical stream table and state tracker
│ │ └── handler.hpp # Unified HTTP/3 protocol handler (HandlerHttp3)
│ ├── reactor.hpp # epoll-based Edge-Triggered Reactor engine
│ ├── proactor.hpp # io_uring-based completion Proactor engine
│ └── client.hpp # Pipeline client abstraction (stdin/stdout bridge)
│
├── config/ # Runtime configuration
│ └── config.lua # Documented sandboxed Lua configuration (all sections)
│
├── examples/ # Ready-to-run Executable Samples
│ ├── common/
│ │ ├── endpoint.hpp # "<host>:<port>" parser shared by every sample
│ │ └── handlers.hpp # SpliceEchoHandler / UdpEchoHandler shared by both engines
│ ├── client/ # Client (transport-level, protocol-agnostic)
│ │ ├── tcp.cpp # Stdin/stdout TCP client (client_tcp)
│ │ ├── ssl.cpp # Stdin/stdout TLS client (client_ssl)
│ │ └── udp.cpp # Stdin/stdout UDP client (client_udp)
│ ├── reactor/ # epoll Edge-Triggered Reactor servers
│ │ ├── tcp.cpp # reactor_tcp TCP echo (splice zero-copy)
│ │ ├── ssl.cpp # reactor_ssl TLS echo
│ │ ├── udp.cpp # reactor_udp UDP datagram echo
│ │ ├── http1.cpp # reactor_http1 HTTP/1.x via llhttp
│ │ ├── http2.cpp # reactor_http2 HTTP/2 stream multiplexer
│ │ └── http3.cpp # reactor_http3 HTTP/3 over QUIC/UDP
│ └── proactor/ # io_uring Proactor servers
│ ├── tcp.cpp # proactor_tcp TCP echo (multishot accept + buf ring)
│ ├── ssl.cpp # proactor_ssl TLS echo (poll-driven handshake)
│ ├── udp.cpp # proactor_udp UDP datagram echo
│ ├── http1.cpp # proactor_http1 HTTP/1.x via llhttp
│ ├── http2.cpp # proactor_http2 HTTP/2 stream multiplexer
│ └── http3.cpp # proactor_http3 HTTP/3 over QUIC/UDP
│
├── tests/ # Unit Test Suite (Google Test + ASan)
│ ├── test_buffer.cpp
│ ├── test_config_lua.cpp # Sandboxed Lua configuration hot-reloading tests
│ ├── test_constraints.cpp
│ ├── test_datagram.cpp # UDP transport session tests
│ ├── test_error.cpp
│ ├── test_frame.cpp
│ ├── test_handler.cpp
│ ├── test_http3.cpp
│ ├── test_multiplexer.cpp
│ ├── test_object_pool.cpp
│ └── test_timer.cpp
│
├── bench/ # Micro-benchmark Suite
│ └── bench.cpp # Throughput and latency micro-benchmarks
│
├── certs/ # Development self-signed certificates and keys
├── scripts/ # Maintenance & formatting scripts
└── thirdparty/ # External Git Submodules (mylog)
All unit tests are compiled with AddressSanitizer (ASan) and -Wall -Wextra -Wpedantic to guarantee memory safety and contract conformance:
cd build
ctest --output-on-failureOutput:
Test project /home/aaa/code/mynet/build
Start 1: test_buffer
1/11 Test #1: test_buffer ...................... Passed 0.01 sec
Start 2: test_constraints
2/11 Test #2: test_constraints ................. Passed 0.01 sec
Start 3: test_datagram
3/11 Test #3: test_datagram .................... Passed 0.02 sec
Start 4: test_error
4/11 Test #4: test_error ....................... Passed 0.01 sec
Start 5: test_frame
5/11 Test #5: test_frame ....................... Passed 0.01 sec
Start 6: test_handler
6/11 Test #6: test_handler ..................... Passed 0.03 sec
Start 7: test_http3
7/11 Test #7: test_http3 ....................... Passed 0.02 sec
Start 8: test_multiplexer
8/11 Test #8: test_multiplexer ................. Passed 0.09 sec
Start 9: test_object_pool
9/11 Test #9: test_object_pool ................. Passed 0.02 sec
Start 10: test_timer
10/11 Test #10: test_timer ....................... Passed 0.03 sec
Start 11: test_config_lua
11/11 Test #11: test_config_lua .................. Passed 0.02 sec
100% tests passed, 0 tests failed out of 11
mynet is released under the MIT License. See LICENSE for details.