Skip to content

ayushkumar912/HTTP_Server_in_C-17

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HTTP Server in C++17

A non-blocking, epoll-based HTTP/1.1 server written in C++17 with no external frameworks.


Table of Contents


Architecture

main.cpp
  └── TcpServer
        └── EventLoop  (epoll, edge-triggered)
              │
              ▼
         Connection  (one per socket fd)
              │
              ├── ReadBuffer  ──►  HttpParser  ──►  HttpRequest
              │                                          │
              │                                          ▼
              │                                       Router  ──►  Handler
              │                                                        │
              └── WriteBuffer  ◄──────────────  HttpResponse  ◄────────┘

All I/O runs on a single event-loop thread using edge-triggered epoll. Handlers must not block; the event loop stalls if they do.


Components

Component File Responsibility
EventLoop event_loop.hpp/cpp Owns the epoll fd. Registers/removes fd interests. Drives run(). Sweeps idle timers.
TcpServer tcp_server.hpp/cpp Creates the listen socket. Accepts connections in a loop (ET). Tracks live connections.
Connection connection.hpp/cpp Owns one socket fd (RAII). Manages read/write buffers and the per-connection state machine. Handles keep-alive, idle timeout, and pipelining.
HttpParser http_parser.hpp/cpp Incremental, stateful HTTP/1.1 parser. Feeds on partial reads. Returns Complete, Incomplete, or Error.
HttpRequest http_request.hpp/cpp Plain value type: method, path, query string, headers, body, route params.
HttpResponse http_response.hpp/cpp Builder pattern. Serialises to a wire-format string.
Router router.hpp/cpp Trie-based. Supports literal and parameterised segments (/:id). Returns 404 / 405 automatically.
Logger logger.hpp/cpp Structured JSON lines to stdout. One line per event. Configurable minimum level.
types.hpp Result<T,E>, Error, HttpMethod, StatusCode. No external dependencies.

Data Flow

1. kernel  →  epoll_wait  →  EventLoop::dispatch
2.                        →  TcpServer::on_accept_ready
3.                             accept4() loop until EAGAIN
4.                             Connection::create() per fd
5.
6. kernel  →  EPOLLIN     →  Connection::on_io_event
7.                        →  Connection::drain_read_buffer
8.                             recv() loop until EAGAIN   (ET requirement)
9.                             if response pending → accumulate in read_backlog_
10.                            else → Connection::process_incoming_bytes
11.                              HttpParser::feed(chunk)
12.                              loop while ParseStatus::Complete:
13.                                parser_.take_unconsumed()  (pipelining)
14.                                Connection::handle_request
15.                                  Router::dispatch(request) → handler → HttpResponse
16.                                  enqueue_response(response.serialize())
17.                                  flush_write_buffer()
18.                                    send() loop until EAGAIN or queue empty
19.                                    if partial → arm EPOLLOUT, set response_pending_
20.
21. kernel  →  EPOLLOUT    →  Connection::on_io_event
22.                        →  flush_write_buffer() drains remaining queue
23.                             on full drain: clear response_pending_
24.                                           disable EPOLLOUT
25.                                           process read_backlog_ if any

Project Structure

Http/
├── Dockerfile           # Multi-stage build (gcc:13 builder → debian:slim runtime)
├── compose.yaml         # Podman / Docker Compose
├── CMakeLists.txt       # CMake build definition (C++17, warnings-as-errors)
├── README.md
└── src/
    ├── types.hpp/cpp        # Result<T,E>, Error, enums
    ├── logger.hpp/cpp       # Structured JSON logger
    ├── http_request.hpp/cpp # Parsed request value type
    ├── http_response.hpp/cpp# Builder + wire serialiser
    ├── http_parser.hpp/cpp  # Incremental HTTP/1.1 parser
    ├── router.hpp/cpp       # Trie router with :param support
    ├── event_loop.hpp/cpp   # epoll abstraction + timer sweep
    ├── connection.hpp/cpp   # Per-connection state machine
    ├── tcp_server.hpp/cpp   # Listen socket + accept loop
    └── main.cpp             # Entry point, route registration

Building

With Podman Compose (recommended)

Requires Podman and podman-compose.

# Start the machine (macOS only)
podman machine start

# Build image and start server
podman compose up --build

# Run in background
podman compose up --build -d

# Stop
podman compose down

Server listens on http://localhost:8080.

Manually on Linux

Requires GCC 11+, CMake 3.16+.

# Release build
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
./http_server 8080

# Debug build (AddressSanitizer + UBSan enabled)
cmake .. -DCMAKE_BUILD_TYPE=Debug
make -j$(nproc)
./http_server 8080

Testing

# Health check
curl http://localhost:8080/health

# Parameterised route
curl http://localhost:8080/users/42

# POST with body
curl -X POST http://localhost:8080/echo \
     -H "Content-Type: application/json" \
     -d '{"hello":"world"}'

# 404 — no matching route
curl http://localhost:8080/nonexistent

# 405 — wrong method
curl -X POST http://localhost:8080/health

Expected responses:

Request Status Body
GET /health 200 {"status":"ok"}
GET /users/42 200 {"id":"42","name":"example user"}
POST /echo 200 request body echoed
GET /nonexistent 404 404 Not Found
POST /health 405 405 Method Not Allowed

Log output is one JSON line per event:

{"ts":"2026-04-04T14:48:48.662Z","level":"info","event":"server_started","address":"0.0.0.0","port":"8080"}
{"ts":"2026-04-04T14:48:49.665Z","level":"info","event":"request","method":"GET","path":"/health"}

Configuration

Edit ServerConfig in main.cpp:

ServerConfig config;
config.bind_address = "0.0.0.0";   // interface to bind
config.port         = 8080;         // TCP port
config.backlog      = 128;          // listen() backlog
config.idle_timeout = std::chrono::seconds{30}; // per-connection idle timeout

Port can also be overridden at runtime:

./http_server 9090

Log level (default INFO):

Logger::set_level(LogLevel::DEBUG);  // DEBUG | INFO | WARN | ERROR

Adding Routes

All routes are registered in register_routes() in main.cpp.

// Exact path
router.add_route(HttpMethod::GET, "/ping", [](const HttpRequest&) {
    return HttpResponse::builder(StatusCode::OK)
               .body("pong")
               .build();
});

// Parameterised segment — value available in request.params["name"]
router.add_route(HttpMethod::GET, "/users/:id/posts/:post_id",
    [](const HttpRequest& req) {
        auto user_id = req.params.at("id");
        auto post_id = req.params.at("post_id");
        // ...
        return HttpResponse::builder(StatusCode::OK)
                   .body("{}", "application/json")
                   .build();
    });

// POST with body access
router.add_route(HttpMethod::POST, "/items",
    [](const HttpRequest& req) {
        // req.body contains the raw request body
        // req.header("content-type") for MIME type
        return HttpResponse::builder(StatusCode::Created)
                   .body(req.body, "application/json")
                   .build();
    });

Handler type: std::function<HttpResponse(const HttpRequest&)>

Handlers must be non-blocking. Any blocking call (file I/O, database, sleep) stalls every other connection.


Design Decisions

Decision Rationale
Edge-triggered epoll Fewer wake-ups; requires recv()/send() until EAGAIN on every event
Single event-loop thread No locking needed; simple mental model; handlers must be non-blocking
Result<T,E> instead of exceptions Explicit error propagation; no hidden control flow in hot paths
take_unconsumed() on parser Correct HTTP/1.1 pipelining — bytes after request N are fed into parser for request N+1
response_pending_ flag Prevents processing request N+1 before response N is fully written; backpressure via read_backlog_
pair<time_point, id> timer key Eliminates timer collision when two connections share an identical expiry timestamp
TCP_NODELAY Reduces latency for small responses by disabling Nagle's algorithm
Multi-stage Dockerfile Builder image (~1.5 GB) discarded; runtime image is debian:bookworm-slim (~80 MB)

Known Limits

Limit Detail
Single-threaded One slow handler blocks all connections
No TLS Plain TCP only; put nginx/Caddy in front for HTTPS
No chunked request bodies Only Content-Length-delimited bodies
No sendfile Responses are copied through userspace; not ideal for large files
~10k connections Practical ceiling before single-thread CPU becomes the bottleneck; scale with SO_REUSEPORT + multiple processes

HTTP_Server_in_C-17

HTTP_Server_in_C-17

HTTP_Server_in_C-17

About

A ground-up explanation of every concept, syscall, language feature, data structure, and design decision in this project. Written for someone who knows C++ basics but has not written systems or network code before.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors