G
YouTube TikTok Games
Skip to content
PastorayPublic

About

A high-performance C++17 HTTP server and reverse proxy with multiple concurrency models, TLS/mTLS, and backend health checks. Designed for extensibility and modern protocol support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

Peb

Peb is a C++17 HTTP/HTTPS server and reverse proxy with multiple concurrency models.

Features

  • Thread-pool server – one blocking thread per connection, HTTP/1.1
  • Epoll + io_uring server – non-blocking I/O for many connections per thread
  • Reverse proxy – backend pool with health checks
  • TLS / HTTPS via OpenSSL
  • Optional mutual TLS (mTLS)
  • HTTP/2 (epoll target only)
  • HTTP/3 over QUIC (UDP, picoquic; dynamic QPACK)
  • WebSocket support
  • Static file serving
  • Chunked transfer encoding
  • Request body parsing (Content-Length and chunked)

Requirements

  • Linux (liburing required for epoll target)
  • CMake ≥ 3.20
  • C++17 compiler
  • OpenSSL development headers
  • liburing development headers

Debian/Ubuntu:

sudo apt install build-essential cmake libssl-dev liburing-dev

Fedora:

sudo dnf install cmake gcc-c++ openssl-devel liburing-devel

Building

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

Executables are placed in build/:

Target Description
peb Thread-pool HTTP/HTTPS server
peb_epoll Epoll + io_uring async server
peb_proxy Reverse proxy with health checks
peb_tests Unit tests
peb_benchmarks Microbenchmarks

Development certificates

Self-signed certs are generated for local testing only. Do not use in production.

./certs/gen_cert.sh
./certs/gen_client_cert.sh

Running

./build/peb [http_port] [https_port] [static_root] [cert_path] [key_path] [client_ca_path] [require_client_cert] [quic_port]

Example with default paths:

./build/peb 8080 8443 ./public

When the certificate is valid, the server also listens for HTTP/3 on UDP port 4433 by default. Set the final quic_port argument to change that port. The QPACK advertises a 4 KiB dynamic table and supports dynamic table instructions, field references, acknowledgments, and up to 16 blocked streams. It also accepts Huffman-encoded strings from clients.

peb_epoll accepts optional worker count and maximum TCP connections per worker after its existing arguments. The worker count defaults to the available CPU count and is clamped to 64; each worker accepts up to 4096 connections by default. At capacity, new TCP connections are closed promptly, leaving the kernel listen backlog to absorb short bursts. TCP/TLS connections have a 60 second idle timeout, TLS handshakes have a 10 second deadline, and HTTP/1 requests have a 120 second absolute deadline. HTTP/2, WebSocket, and HTTP/3 connections use the idle timeout; HTTP/3 is capped at 1024 simultaneous connections and uses a 10 second handshake deadline.

The blocking peb server uses eight workers per listening port, with at most 64 queued connections per worker. When workers and the queue are full, it closes newly accepted sockets. Its blocking reads and writes have a 15 second socket timeout.

Metrics and load testing

All HTTP server modes, including the reverse proxy, expose process-wide Prometheus text metrics at GET /metrics: active and rejected connections, thread-pool queue depth, HTTP/3 application send-queue bytes, resident memory, buffered bytes and budget rejections, request counts and errors, protocol/application errors, and a request-handler latency histogram. Server-owned parser, body, file-read, and output buffers share a 256 MiB process-wide budget by default. Set PEB_MAX_BUFFERED_BYTES to a positive byte count to change it. When a request cannot reserve memory, its connection or stream is closed with a protocol overload error; oversized response buffering is rejected with 503 where the protocol path can still send one. The handler histogram measures route execution time. Handler-local allocations and allocator overhead are outside the reservation counter; RSS reports actual process memory. The load test reports client-observed latency, including connection setup and response transfer.

Run the dependency-free concurrency sweep against a running server:

python3 bench/load_test.py --url http://127.0.0.1:8080 --path /api/health --concurrency 1,8,32,64,128 --duration 10

It prints CSV with throughput, errors, p50/p95/p99 latency, RSS, and server connection/queue/request metrics at each load level. Increase concurrency until throughput flattens or tail latency and errors rise; results depend on hardware, build mode, kernel, and the selected endpoint.

Demo endpoints

  • GET /api/health – { "status":"ok" }
  • GET /api/time – current Unix time
  • GET /api/stream – chunked response (5 chunks)
  • POST /api/echo – echoes request body
  • GET /api/whoami – client cert subject if using mTLS

Static files are served from static_root (default ./public).

Streaming request and response bodies

Use Router::post_streaming when a handler can consume an upload incrementally. HTTP/1.1 request body bytes, HTTP/2 DATA payloads, and HTTP/3 DATA payloads are passed to on_body as they arrive. Streaming routes do not copy request payloads into HttpRequest::body; the existing post API continues to buffer bodies for handlers that need the complete string. The string_view passed to on_body is valid only until that callback returns, so copy data the handler needs to retain.

router.post_streaming("/upload", [](const HttpRequest&) {
  auto received = std::make_shared<std::size_t>(0);
  return StreamingRequestSession{
    [received](std::string_view chunk) { *received += chunk.size(); },
    [received](const HttpRequest&) {
      HttpResponse response;
      response.set_body(std::to_string(*received), "text/plain");
      return response;
    }
  };
});

HttpResponse::enable_chunked_encoding streams generated response chunks over HTTP/1.1, HTTP/2, and HTTP/3 rather than constructing one large body string. Chunks are requested as the transport is ready to send more data. HTTP/3 also caps its per-connection application send queue at 256 KiB. The reverse proxy streams fixed-length and chunked client uploads to the backend, then streams fixed-length, chunked, or close-delimited backend responses to the client. Proxy uploads remain capped at 20 MiB and upstream responses at 50 MiB.

Testing

cd build
ctest --output-on-failure

The end-to-end slow-client, overload, TLS, HTTP/2, HTTP/3, and sustained-load shutdown checks require the built server binaries and curl with HTTP/2 and HTTP/3 support:

python3 tests/protocol_load.py

The C++ proxy integration tests start a local mock backend and check slow downloads, chunked request trailers, truncated upstream bodies, timeout expiry, overload rejection, TLS client connections, and worker release under stalled uploads. Run them with the regular test suite:

ctest --test-dir build --output-on-failure

The primary HTTP/1.1 proxy relay copies at most one 8 KiB reader chunk at a time. Defaults are a 5 second client/backend inactivity timeout, a 5 second backend connect timeout, a 120 second exchange deadline, 16 workers, a 32 connection queue, and eight idle backend connections per backend. Every value can be overridden with command-line options; excess accepted connections are rejected.

Each backend keeps up to eight idle HTTP/1.1 connections for reuse. A socket is returned only after a complete length-delimited response and when the backend connection semantics permit reuse; stale, truncated, and close-delimited connections are discarded. The C++ load test reports achieved requests per second and p50, p95, and p99 request latency in its output and test properties.

The proxy's primary listener speaks HTTP/1.1. Optional protocol listeners can be enabled with TLS credentials: --https-port enables HTTP/1.1 plus HTTP/2 over TLS, and --http3-port enables HTTP/3 over QUIC. HTTP/2 and HTTP/3 requests are translated to HTTP/1.1 upstream requests through the backend pool. Those protocol adapters currently buffer each request and response and perform blocking upstream I/O in their reactor callback; use the primary HTTP/1.1 listener for large streaming transfers.

build/peb_proxy 8080 127.0.0.1:9000 /api/health 5 \
  certs/server.crt certs/server.key \
  --workers 16 --queue-limit 32 --backend-pool-size 8 \
  --client-timeout 5 --backend-timeout 5 --connect-timeout 5 \
  --request-deadline 120 --https-port 8443 --http3-port 4433

Benchmarks

./build/peb_benchmarks

About

A high-performance C++17 HTTP server and reverse proxy with multiple concurrency models, TLS/mTLS, and backend health checks. Designed for extensibility and modern protocol support.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages