Project 11: HTTP Server Framework

Build a high-performance HTTP/1.1 server framework with routing, middleware, and async I/O using epoll/kqueue capable of handling thousands of concurrent connections.


Quick Reference

Attribute Value
Difficulty Expert
Time Estimate 1-2 months
Language C++
Prerequisites Projects 1-8, TCP sockets, HTTP protocol
Key Topics HTTP/1.1, epoll/kqueue, routing, middleware, non-blocking I/O

Table of Contents


1. Learning Objectives

By completing this project, you will:

  1. Master HTTP/1.1 protocol: Understand request/response format, headers, and body handling
  2. Implement event-driven I/O: Use epoll (Linux) or kqueue (macOS/BSD) for scalable networking
  3. Design routing systems: Pattern matching for URLs with path parameters
  4. Build middleware chains: Composable request/response processing
  5. Handle connection lifecycle: Keep-alive, timeouts, graceful shutdown
  6. Parse protocols efficiently: State machine-based HTTP parsing
  7. Optimize for performance: Minimize allocations, maximize throughput
  8. Benchmark and profile: Measure requests/second and latency

2. Theoretical Foundation

2.1 Core Concepts

HTTP/1.1 Request/Response Format

+-----------------------------------------------------------------------+
|                    HTTP/1.1 REQUEST FORMAT                             |
+-----------------------------------------------------------------------+
|                                                                        |
|  Request Line:  METHOD SP URI SP HTTP-Version CRLF                     |
|  Headers:       Header-Name: Header-Value CRLF                         |
|                 ... (one per line)                                     |
|  Empty Line:    CRLF                                                   |
|  Body:          (optional, length from Content-Length or chunked)      |
|                                                                        |
|  Example:                                                              |
|  +-----------------------------------------------------------------+   |
|  | GET /api/users/42 HTTP/1.1                                      |   |
|  | Host: localhost:8080                                            |   |
|  | User-Agent: curl/7.68.0                                         |   |
|  | Accept: application/json                                        |   |
|  | Connection: keep-alive                                          |   |
|  |                                                                  |   |
|  +-----------------------------------------------------------------+   |
|                                                                        |
|  POST Example with Body:                                               |
|  +-----------------------------------------------------------------+   |
|  | POST /api/users HTTP/1.1                                        |   |
|  | Host: localhost:8080                                            |   |
|  | Content-Type: application/json                                  |   |
|  | Content-Length: 27                                              |   |
|  |                                                                  |   |
|  | {"name":"Alice","age":30}                                       |   |
|  +-----------------------------------------------------------------+   |
|                                                                        |
+-----------------------------------------------------------------------+
|                    HTTP/1.1 RESPONSE FORMAT                            |
+-----------------------------------------------------------------------+
|                                                                        |
|  Status Line:   HTTP-Version SP Status-Code SP Reason-Phrase CRLF      |
|  Headers:       Header-Name: Header-Value CRLF                         |
|  Empty Line:    CRLF                                                   |
|  Body:          (optional)                                             |
|                                                                        |
|  Example:                                                              |
|  +-----------------------------------------------------------------+   |
|  | HTTP/1.1 200 OK                                                 |   |
|  | Content-Type: application/json                                  |   |
|  | Content-Length: 45                                              |   |
|  | Connection: keep-alive                                          |   |
|  |                                                                  |   |
|  | {"id":42,"name":"Alice","email":"a@b.com"}                      |   |
|  +-----------------------------------------------------------------+   |
|                                                                        |
|  Common Status Codes:                                                  |
|  200 OK          - Success                                             |
|  201 Created     - Resource created                                    |
|  204 No Content  - Success with no body                                |
|  301 Moved       - Permanent redirect                                  |
|  302 Found       - Temporary redirect                                  |
|  400 Bad Request - Client error                                        |
|  401 Unauthorized- Authentication required                             |
|  403 Forbidden   - Permission denied                                   |
|  404 Not Found   - Resource not found                                  |
|  500 Internal    - Server error                                        |
|                                                                        |
+-----------------------------------------------------------------------+

Event-Driven I/O with epoll

+-----------------------------------------------------------------------+
|                    EPOLL EVENT LOOP                                    |
+-----------------------------------------------------------------------+
|                                                                        |
|  Traditional Thread-per-Connection:                                    |
|  +--------+  +--------+  +--------+  +--------+                        |
|  |Thread 1|  |Thread 2|  |Thread 3|  |Thread N|   Problem: Thousands   |
|  |  Conn  |  |  Conn  |  |  Conn  |  |  Conn  |   of threads = huge    |
|  +--------+  +--------+  +--------+  +--------+   memory overhead      |
|                                                                        |
|  Event-Driven (epoll/kqueue):                                          |
|                                                                        |
|         +------------------------------------------+                   |
|         |              epoll instance               |                  |
|         |  Monitors thousands of file descriptors   |                  |
|         +------------------------------------------+                   |
|                           |                                            |
|                           v                                            |
|         +------------------------------------------+                   |
|         |           Event Loop (single thread)      |                  |
|         |  while (true) {                           |                  |
|         |      events = epoll_wait(epfd, ...)       |                  |
|         |      for (event : events) {               |                  |
|         |          if (event.fd == server_fd)       |                  |
|         |              accept_new_connection()      |                  |
|         |          else if (event.events & EPOLLIN) |                  |
|         |              handle_read(event.fd)        |                  |
|         |          else if (event.events & EPOLLOUT)|                  |
|         |              handle_write(event.fd)       |                  |
|         |      }                                    |                  |
|         |  }                                        |                  |
|         +------------------------------------------+                   |
|                                                                        |
|  Key epoll operations:                                                 |
|  - epoll_create1(0)         Create epoll instance                      |
|  - epoll_ctl(EPOLL_CTL_ADD) Register fd to monitor                     |
|  - epoll_ctl(EPOLL_CTL_MOD) Modify monitored events                    |
|  - epoll_ctl(EPOLL_CTL_DEL) Remove fd from monitoring                  |
|  - epoll_wait()             Block until events occur                   |
|                                                                        |
|  Event types:                                                          |
|  - EPOLLIN   Ready to read                                             |
|  - EPOLLOUT  Ready to write                                            |
|  - EPOLLERR  Error condition                                           |
|  - EPOLLHUP  Peer closed connection                                    |
|  - EPOLLET   Edge-triggered mode (notify once per state change)        |
|                                                                        |
+-----------------------------------------------------------------------+

HTTP State Machine Parser

+-----------------------------------------------------------------------+
|                    HTTP PARSER STATE MACHINE                           |
+-----------------------------------------------------------------------+
|                                                                        |
|  States:                                                               |
|                                                                        |
|  [START] --"G/P/D/..."-> [METHOD] --" "-> [URI] --" "-> [VERSION]      |
|                                                                        |
|  [VERSION] --"\r\n"-> [HEADER_NAME] --":"-> [HEADER_VALUE]             |
|                                                                        |
|  [HEADER_VALUE] --"\r\n"-> [HEADER_NAME] (more headers)                |
|                        |                                               |
|                        +--"\r\n\r\n"-> [BODY] or [COMPLETE]            |
|                                                                        |
|  Visual:                                                               |
|                                                                        |
|        G E T   /                                                       |
|        ^ ^ ^   ^                                                       |
|        | | |   |                                                       |
|  START-+-+-+   +- URI parsing                                          |
|          |                                                             |
|       METHOD                                                           |
|                                                                        |
|  Parsing "GET /users HTTP/1.1\r\nHost: localhost\r\n\r\n":             |
|                                                                        |
|  State       | Input | Action                                          |
|  ------------|-------|----------------------------------------------   |
|  START       | 'G'   | -> METHOD, accumulate "G"                       |
|  METHOD      | 'E'   | accumulate "GE"                                 |
|  METHOD      | 'T'   | accumulate "GET"                                |
|  METHOD      | ' '   | -> URI, method="GET"                            |
|  URI         | '/'   | accumulate "/"                                  |
|  URI         | 'u'   | accumulate "/u"                                 |
|  ...         | ...   | ...                                             |
|  URI         | ' '   | -> VERSION, uri="/users"                        |
|  VERSION     | 'H'   | accumulate "H"                                  |
|  ...         | '\n'  | -> HEADER_NAME                                  |
|  HEADER_NAME | 'H'   | accumulate "H"                                  |
|  ...         | ':'   | -> HEADER_VALUE, name="Host"                    |
|  HEADER_VALUE| ' '   | skip leading whitespace                         |
|  HEADER_VALUE| 'l'   | accumulate "l"                                  |
|  ...         | '\n'  | -> HEADER_NAME, value="localhost"               |
|  HEADER_NAME | '\r'  | check for empty line                            |
|  HEADER_NAME | '\n'  | -> COMPLETE (or BODY if Content-Length > 0)     |
|                                                                        |
+-----------------------------------------------------------------------+

Routing with Path Parameters

+-----------------------------------------------------------------------+
|                    ROUTING SYSTEM                                      |
+-----------------------------------------------------------------------+
|                                                                        |
|  Route patterns:                                                       |
|  - Static: "/api/health"                                               |
|  - Parameters: "/api/users/:id"                                        |
|  - Wildcard: "/static/*filepath"                                       |
|                                                                        |
|  Trie-based router:                                                    |
|                                                                        |
|                       [root]                                           |
|                          |                                             |
|                       [api]                                            |
|                      /      \                                          |
|                 [users]    [health]                                    |
|                /      \        |                                       |
|           [:id]    (POST)   [GET handler]                              |
|              |                                                         |
|         [GET handler]                                                  |
|                                                                        |
|  Matching algorithm:                                                   |
|  1. Split path by '/'                                                  |
|  2. Traverse trie node by node                                         |
|  3. If no static match, try parameter node (:xxx)                      |
|  4. If parameter, extract value and continue                           |
|  5. If wildcard, capture rest of path                                  |
|  6. At leaf, match HTTP method to handler                              |
|                                                                        |
|  Example:                                                              |
|  Route: "/api/users/:id/posts/:postId"                                 |
|  Request: "/api/users/42/posts/100"                                    |
|  Params: { id: "42", postId: "100" }                                   |
|                                                                        |
+-----------------------------------------------------------------------+

2.2 Why This Matters

HTTP servers are fundamental infrastructure:

  • Every web service: REST APIs, GraphQL, WebSocket upgrade
  • Microservices: Inter-service communication
  • Performance-critical: Latency directly impacts user experience
  • Scalability: Handle millions of requests

Real-world impact:

  • Nginx handles 30%+ of web traffic
  • High-frequency trading uses custom HTTP servers
  • CDNs serve billions of requests per day
  • Cloud functions are HTTP-triggered

2.3 Historical Context

1991: HTTP/0.9 (GET only, no headers)
1996: HTTP/1.0 (headers, multiple methods)
1997: HTTP/1.1 (keep-alive, chunked encoding)
1999: Nginx begins development
2015: HTTP/2 (multiplexing, header compression)
2022: HTTP/3 (QUIC, UDP-based)

Key developments:

  • Apache (1995): Fork-based, one process per request
  • Nginx (2004): Event-driven, revolutionary performance
  • Node.js (2009): JavaScript with event loop
  • Go net/http (2009): Goroutines simplify async
  • io_uring (2019): Linux’s next-gen async I/O

2.4 Common Misconceptions

Misconception Reality
“Threads are simpler” Event loops handle more connections with less memory
“HTTP parsing is trivial” Edge cases and security make it complex
“Just use a library” Understanding internals is crucial for debugging
“async/await is magic” It’s compiler transforms over event loops
“Keep-alive is always better” It consumes file descriptors; limits apply

3. Project Specification

3.1 What You Will Build

A complete HTTP/1.1 server framework featuring:

  1. HTTP Parser: State machine for request parsing
  2. Event Loop: epoll (Linux) or kqueue (macOS)
  3. Router: Trie-based URL matching with parameters
  4. Middleware: Composable request/response processing
  5. Static Files: Efficient file serving with sendfile
  6. JSON Support: Request/response body handling
  7. Connection Pool: Keep-alive management

3.2 Functional Requirements

Requirement Description
FR-1 Parse HTTP/1.1 requests (GET, POST, PUT, DELETE, etc.)
FR-2 Handle request headers and body (JSON, form data)
FR-3 Route requests to handlers with path parameters
FR-4 Execute middleware chain (logging, auth, CORS)
FR-5 Send responses with status, headers, and body
FR-6 Serve static files efficiently
FR-7 Support keep-alive connections
FR-8 Handle connection timeouts

3.3 Non-Functional Requirements

Requirement Description
NFR-1 Handle 10,000+ concurrent connections
NFR-2 Process 100,000+ requests/second (simple endpoint)
NFR-3 Sub-millisecond latency for simple routes
NFR-4 Graceful shutdown without dropping connections
NFR-5 Cross-platform (Linux, macOS)

3.4 Example Usage / Output

int main() {
    HttpServer server;

    // Middleware
    server.use([](Request& req, Response& res, Next next) {
        auto start = std::chrono::high_resolution_clock::now();
        next();
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        std::cout << req.method << " " << req.path
                  << " " << res.statusCode << " - "
                  << duration.count() << "us\n";
    });

    // CORS middleware
    server.use([](Request& req, Response& res, Next next) {
        res.header("Access-Control-Allow-Origin", "*");
        if (req.method == "OPTIONS") {
            res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
            res.status(204).send();
            return;
        }
        next();
    });

    // Routes
    server.get("/", [](Request& req, Response& res) {
        res.send("Hello, World!");
    });

    server.get("/api/users/:id", [](Request& req, Response& res) {
        std::string userId = req.params["id"];
        res.json({
            {"id", userId},
            {"name", "Alice"},
            {"email", "alice@example.com"}
        });
    });

    server.post("/api/users", [](Request& req, Response& res) {
        auto body = req.json();
        // Create user...
        res.status(201).json({
            {"id", 1},
            {"name", body["name"]}
        });
    });

    // Static files
    server.serveStatic("/static", "./public");

    // Start server
    server.listen(8080, []() {
        std::cout << "Server listening on port 8080\n";
    });

    return 0;
}

curl testing:

$ curl http://localhost:8080/
Hello, World!

$ curl http://localhost:8080/api/users/42
{"id":"42","name":"Alice","email":"alice@example.com"}

$ curl -X POST http://localhost:8080/api/users \
       -H "Content-Type: application/json" \
       -d '{"name":"Bob"}'
{"id":1,"name":"Bob"}

$ curl http://localhost:8080/static/index.html
<!DOCTYPE html>...

Benchmark output:

$ wrk -t12 -c400 -d30s http://localhost:8080/
Running 30s test @ http://localhost:8080/
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   156.32us  234.56us   8.43ms   95.23%
    Req/Sec    18.43k     1.21k   21.34k    78.45%
  6,623,456 requests in 30.00s, 892.34MB read
Requests/sec: 220,781.87
Transfer/sec:     29.74MB

3.5 Real World Outcome

After completing this project, you will have:

  1. Production-capable server: Can handle real traffic
  2. Deep networking knowledge: Sockets, protocols, async I/O
  3. Systems programming skills: Low-level optimization
  4. Framework design experience: API design, middleware

You should be able to:

  • Debug HTTP protocol issues with Wireshark
  • Understand Nginx, Node.js, Go net/http internals
  • Optimize network applications
  • Design APIs and frameworks

4. Solution Architecture

4.1 High-Level Design

+-----------------------------------------------------------------------+
|                    SERVER ARCHITECTURE                                 |
+-----------------------------------------------------------------------+
|                                                                        |
|  +------------------------------------------------------------------+  |
|  |                      HttpServer                                   |  |
|  |  - Router                                                        |  |
|  |  - Middleware stack                                              |  |
|  |  - EventLoop                                                     |  |
|  |  - ConnectionManager                                             |  |
|  +------------------------------------------------------------------+  |
|                              |                                         |
|       +----------------------+----------------------+                  |
|       v                      v                      v                  |
|  +----------+          +----------+          +----------+              |
|  | EventLoop|          |  Router  |          |Middleware|              |
|  |  epoll/  |          |  (Trie)  |          |  Chain   |              |
|  |  kqueue  |          +----------+          +----------+              |
|  +----------+                                                          |
|       |                                                                |
|       v                                                                |
|  +------------------------------------------------------------------+  |
|  |                    Connection Manager                             |  |
|  |  +------------+  +------------+  +------------+                  |  |
|  |  |Connection 1|  |Connection 2|  |Connection N|                  |  |
|  |  | - socket   |  | - socket   |  | - socket   |                  |  |
|  |  | - parser   |  | - parser   |  | - parser   |                  |  |
|  |  | - buffer   |  | - buffer   |  | - buffer   |                  |  |
|  |  +------------+  +------------+  +------------+                  |  |
|  +------------------------------------------------------------------+  |
|                                                                        |
|  Request Flow:                                                         |
|                                                                        |
|  1. EventLoop: epoll_wait() returns readable socket                    |
|  2. Connection: Read bytes into buffer                                 |
|  3. Parser: Parse HTTP request from buffer                             |
|  4. Router: Match route, extract params                                |
|  5. Middleware: Execute chain (logging, auth, ...)                     |
|  6. Handler: User's route handler                                      |
|  7. Response: Build HTTP response                                      |
|  8. Connection: Write response to socket                               |
|  9. EventLoop: If keep-alive, re-register for read                     |
|                                                                        |
+-----------------------------------------------------------------------+

4.2 Key Components

Request and Response Objects

struct Request {
    // Request line
    std::string method;      // GET, POST, etc.
    std::string path;        // /api/users/42
    std::string httpVersion; // HTTP/1.1

    // Parsed path
    std::unordered_map<std::string, std::string> params;   // :id -> "42"
    std::unordered_map<std::string, std::string> query;    // ?foo=bar

    // Headers
    std::unordered_map<std::string, std::string> headers;

    // Body
    std::string body;
    std::string contentType;

    // Convenience methods
    std::string header(const std::string& name) const;
    Json json() const;
    bool hasHeader(const std::string& name) const;
};

class Response {
    int statusCode = 200;
    std::string statusMessage = "OK";
    std::unordered_map<std::string, std::string> headers;
    std::string body;
    bool sent = false;

public:
    Response& status(int code);
    Response& header(const std::string& name, const std::string& value);
    void send(const std::string& body);
    void json(const Json& data);
    void sendFile(const std::string& path);
    void redirect(const std::string& url, int code = 302);

    std::string serialize() const;
};

Event Loop

class EventLoop {
#ifdef __linux__
    int epollFd;
#else
    int kqueueFd;
#endif
    std::unordered_map<int, std::shared_ptr<Connection>> connections;
    bool running = true;

public:
    EventLoop();
    ~EventLoop();

    void addSocket(int fd, uint32_t events, Connection* conn);
    void modifySocket(int fd, uint32_t events);
    void removeSocket(int fd);

    void run(int serverFd, std::function<void(int)> onAccept,
             std::function<void(Connection*)> onRead,
             std::function<void(Connection*)> onWrite);

    void stop();
};

4.3 Data Structures

Router Trie

struct RouteNode {
    std::unordered_map<std::string, std::unique_ptr<RouteNode>> children;
    RouteNode* paramChild = nullptr;   // For :param
    RouteNode* wildcardChild = nullptr; // For *wildcard
    std::string paramName;
    std::string wildcardName;

    // Handlers per method
    std::unordered_map<std::string, Handler> handlers;
};

class Router {
    RouteNode root;

public:
    void addRoute(const std::string& method,
                  const std::string& pattern,
                  Handler handler);

    struct Match {
        Handler handler;
        std::unordered_map<std::string, std::string> params;
    };

    std::optional<Match> match(const std::string& method,
                                const std::string& path);
};

Connection Buffer

class Buffer {
    std::vector<char> data;
    size_t readPos = 0;
    size_t writePos = 0;

public:
    // Write side
    char* writePtr() { return data.data() + writePos; }
    size_t writableBytes() const { return data.size() - writePos; }
    void advance(size_t n) { writePos += n; }
    void ensureWritable(size_t n);

    // Read side
    const char* readPtr() const { return data.data() + readPos; }
    size_t readableBytes() const { return writePos - readPos; }
    void consume(size_t n) { readPos += n; }

    // Search
    const char* findCRLF() const;
    std::string readLine();
    void compact();
};

4.4 Algorithm Overview

HTTP Request Parsing

+-----------------------------------------------------------------------+
|                    HTTP PARSING ALGORITHM                              |
+-----------------------------------------------------------------------+
|                                                                        |
|  enum class ParseState {                                               |
|      REQUEST_LINE,                                                     |
|      HEADERS,                                                          |
|      BODY,                                                             |
|      COMPLETE,                                                         |
|      ERROR                                                             |
|  };                                                                    |
|                                                                        |
|  ParseResult HttpParser::parse(Buffer& buffer) {                       |
|      while (buffer.readableBytes() > 0) {                              |
|          switch (state) {                                              |
|              case REQUEST_LINE: {                                      |
|                  auto line = buffer.findCRLF();                        |
|                  if (!line) return ParseResult::NeedMore;              |
|                                                                        |
|                  if (!parseRequestLine(line)) {                        |
|                      state = ERROR;                                    |
|                      return ParseResult::Error;                        |
|                  }                                                     |
|                  buffer.consume(line.size() + 2);                      |
|                  state = HEADERS;                                      |
|                  break;                                                |
|              }                                                         |
|                                                                        |
|              case HEADERS: {                                           |
|                  auto line = buffer.findCRLF();                        |
|                  if (!line) return ParseResult::NeedMore;              |
|                                                                        |
|                  if (line.empty()) {                                   |
|                      // Empty line = end of headers                    |
|                      buffer.consume(2);                                |
|                      if (hasBody()) {                                  |
|                          state = BODY;                                 |
|                      } else {                                          |
|                          state = COMPLETE;                             |
|                          return ParseResult::Complete;                 |
|                      }                                                 |
|                  } else {                                              |
|                      parseHeader(line);                                |
|                      buffer.consume(line.size() + 2);                  |
|                  }                                                     |
|                  break;                                                |
|              }                                                         |
|                                                                        |
|              case BODY: {                                              |
|                  size_t needed = contentLength - bodyRead;             |
|                  size_t available = buffer.readableBytes();            |
|                  size_t toRead = std::min(needed, available);          |
|                                                                        |
|                  body.append(buffer.readPtr(), toRead);                |
|                  buffer.consume(toRead);                               |
|                  bodyRead += toRead;                                   |
|                                                                        |
|                  if (bodyRead >= contentLength) {                      |
|                      state = COMPLETE;                                 |
|                      return ParseResult::Complete;                     |
|                  }                                                     |
|                  return ParseResult::NeedMore;                         |
|              }                                                         |
|          }                                                             |
|      }                                                                 |
|      return ParseResult::NeedMore;                                     |
|  }                                                                     |
|                                                                        |
+-----------------------------------------------------------------------+

5. Implementation Guide

5.1 Development Environment Setup

# Install dependencies (Linux)
sudo apt install build-essential cmake

# macOS (uses kqueue, no extra deps)
xcode-select --install

# Create project
mkdir -p http-server/{src,include,tests}
cd http-server

# CMakeLists.txt
cat > CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.16)
project(HttpServer CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_executable(httpserver
    src/main.cpp
    src/server.cpp
    src/event_loop.cpp
    src/connection.cpp
    src/parser.cpp
    src/router.cpp
    src/middleware.cpp
    src/response.cpp
)

target_include_directories(httpserver PRIVATE include)
target_compile_options(httpserver PRIVATE -Wall -Wextra -O2)

# For benchmarking
add_executable(benchmark tools/benchmark.cpp)
EOF

# Build
mkdir build && cd build
cmake .. && make

5.2 Project Structure

http-server/
├── CMakeLists.txt
├── include/
│   ├── server.hpp
│   ├── event_loop.hpp
│   ├── connection.hpp
│   ├── parser.hpp
│   ├── router.hpp
│   ├── middleware.hpp
│   ├── request.hpp
│   ├── response.hpp
│   ├── buffer.hpp
│   └── json.hpp
├── src/
│   ├── main.cpp
│   ├── server.cpp
│   ├── event_loop.cpp
│   ├── connection.cpp
│   ├── parser.cpp
│   ├── router.cpp
│   ├── middleware.cpp
│   └── response.cpp
├── tests/
│   ├── parser_test.cpp
│   ├── router_test.cpp
│   └── integration_test.cpp
└── examples/
    └── api_server.cpp

5.3 The Core Question You’re Answering

How do you efficiently multiplex thousands of network connections in a single thread while maintaining low latency and high throughput?

This encompasses:

  • Event notification mechanisms
  • Non-blocking I/O
  • Efficient buffer management
  • Protocol parsing without blocking

5.4 Concepts You Must Understand First

Before implementing, verify you can answer:

  1. What is the difference between blocking and non-blocking I/O?
    • Reference: “The Linux Programming Interface” Chapter 63
  2. How does epoll differ from select/poll?
    • Reference: “Linux System Programming” by Robert Love
  3. What is the HTTP/1.1 persistent connection model?
    • Reference: RFC 7230
  4. How do you handle partial reads/writes on non-blocking sockets?
    • Reference: Stevens “UNIX Network Programming”
  5. What is edge-triggered vs level-triggered notification?
    • Reference: epoll man pages

5.5 Questions to Guide Your Design

Event Loop:

  • How will you handle the server socket vs client sockets?
  • How will you detect when a connection is ready to write?
  • How will you handle timeouts?

Parser:

  • How will you handle requests that span multiple reads?
  • How will you handle malformed requests?
  • How will you limit header size to prevent attacks?

Router:

  • How will you match path parameters efficiently?
  • How will you handle overlapping routes?
  • How will you support wildcards?

Middleware:

  • How will you pass control to the next middleware?
  • How will you handle early response (e.g., auth failure)?
  • How will you share data between middleware?

5.6 Thinking Exercise

Before coding, trace through this scenario:

1. Client connects (TCP handshake complete)
2. Client sends: "GET /api/use"  (partial request)
3. 10ms later: "rs/42 HTTP/1.1\r\nHost: localhost\r\n\r\n"
4. Server sends response
5. Client sends another request (keep-alive)

Draw the state of:

  • epoll event queue at each step
  • Connection buffer contents
  • Parser state

5.7 Hints in Layers

Hint 1 - Getting Started: Start with a simple blocking server that handles one connection. Then add non-blocking I/O. Then add epoll.

Hint 2 - Buffer Management: Use a ring buffer or growable buffer. The key is handling partial reads efficiently without copying data.

Hint 3 - Router Implementation: Build the trie incrementally. Start with static routes, then add parameters, then wildcards.

Hint 4 - Debugging: Use strace to see system calls. Use curl -v for verbose HTTP output. Add extensive logging.

5.8 The Interview Questions They’ll Ask

  1. “Explain the C10K problem and how epoll solves it.”
    • Expected: select/poll O(n), epoll O(1) for ready fds
  2. “What’s the difference between edge-triggered and level-triggered?”
    • Expected: ET notifies once, LT notifies while condition true
  3. “How would you implement connection timeouts?”
    • Expected: Timer wheel or priority queue with connection deadlines
  4. “What happens if you try to write() when the socket buffer is full?”
    • Expected: EAGAIN/EWOULDBLOCK, register for EPOLLOUT
  5. “How do you prevent slow client attacks?”
    • Expected: Timeouts, request size limits, connection limits per IP

5.9 Books That Will Help

Topic Book Chapter
Socket Programming “UNIX Network Programming” by Stevens Chapters 1-6
epoll/kqueue “The Linux Programming Interface” by Kerrisk Chapter 63
HTTP Protocol RFC 7230-7235 All
High Performance “Systems Performance” by Gregg Networking chapters
Server Design “UNIX Network Programming” by Stevens Chapter 15

5.10 Implementation Phases

Phase 1: Basic Server (Week 1)

  • Blocking TCP server
  • HTTP/1.0 (no keep-alive)
  • Single route

Phase 2: Event Loop (Week 2)

  • epoll/kqueue integration
  • Non-blocking I/O
  • Connection management

Phase 3: HTTP Parser (Week 3)

  • State machine parser
  • Header parsing
  • Body handling

Phase 4: Router (Week 4)

  • Trie-based routing
  • Path parameters
  • Query string parsing

Phase 5: Middleware (Week 5)

  • Chain of responsibility
  • Common middleware (logging, CORS)
  • Error handling

Phase 6: Polish (Week 6-8)

  • Keep-alive
  • Static file serving
  • Timeouts
  • Benchmarking

5.11 Key Implementation Decisions

Decision Options Recommendation
Event loop epoll, kqueue, libuv Native epoll/kqueue for learning
Buffer std::string, custom Custom buffer for performance
Parser Hand-written, http-parser lib Hand-written for learning
JSON nlohmann/json, RapidJSON nlohmann for convenience

6. Testing Strategy

Unit Tests

// Parser tests
TEST(HttpParser, ParsesSimpleGet) {
    HttpParser parser;
    Buffer buf;
    buf.append("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");

    auto result = parser.parse(buf);
    EXPECT_EQ(result, ParseResult::Complete);
    EXPECT_EQ(parser.request().method, "GET");
    EXPECT_EQ(parser.request().path, "/");
}

TEST(HttpParser, HandlesPartialRequest) {
    HttpParser parser;
    Buffer buf;
    buf.append("GET /api");

    auto result = parser.parse(buf);
    EXPECT_EQ(result, ParseResult::NeedMore);

    buf.append("/users HTTP/1.1\r\nHost: localhost\r\n\r\n");
    result = parser.parse(buf);
    EXPECT_EQ(result, ParseResult::Complete);
    EXPECT_EQ(parser.request().path, "/api/users");
}

// Router tests
TEST(Router, MatchesStaticRoute) {
    Router router;
    router.addRoute("GET", "/api/health", healthHandler);

    auto match = router.match("GET", "/api/health");
    ASSERT_TRUE(match.has_value());
}

TEST(Router, ExtractsParams) {
    Router router;
    router.addRoute("GET", "/users/:id/posts/:postId", handler);

    auto match = router.match("GET", "/users/42/posts/100");
    ASSERT_TRUE(match.has_value());
    EXPECT_EQ(match->params["id"], "42");
    EXPECT_EQ(match->params["postId"], "100");
}

Integration Tests

#!/bin/bash
# test_server.sh

# Start server in background
./httpserver &
SERVER_PID=$!
sleep 1

# Test basic GET
RESPONSE=$(curl -s http://localhost:8080/)
if [[ "$RESPONSE" == "Hello, World!" ]]; then
    echo "PASS: Basic GET"
else
    echo "FAIL: Basic GET"
fi

# Test route params
RESPONSE=$(curl -s http://localhost:8080/api/users/42)
if [[ "$RESPONSE" == *'"id":"42"'* ]]; then
    echo "PASS: Route params"
else
    echo "FAIL: Route params"
fi

# Test POST with JSON
RESPONSE=$(curl -s -X POST http://localhost:8080/api/users \
    -H "Content-Type: application/json" \
    -d '{"name":"Bob"}')
if [[ "$RESPONSE" == *'"name":"Bob"'* ]]; then
    echo "PASS: POST JSON"
else
    echo "FAIL: POST JSON"
fi

# Cleanup
kill $SERVER_PID

Benchmark Tests

# Using wrk
wrk -t12 -c400 -d30s http://localhost:8080/

# Using ab (Apache Bench)
ab -n 100000 -c 100 http://localhost:8080/

# Using hey
hey -n 100000 -c 100 http://localhost:8080/

7. Common Pitfalls & Debugging

Problem Symptom Root Cause Fix
Connection hangs Request never completes Forgot to register EPOLLOUT Register after buffering write data
Memory grows RAM increases over time Connection not cleaned up Use RAII, check all paths
100% CPU Busy loop Edge-triggered without draining Read until EAGAIN
Slow clients Timeouts write() buffer full Buffer writes, use EPOLLOUT
Parse errors Malformed request CRLF handling Check for both \r\n patterns

Debugging Techniques

# Trace system calls
strace -f ./httpserver

# Network debugging
tcpdump -i lo port 8080

# Verbose curl
curl -v http://localhost:8080/

# Check file descriptors
ls -la /proc/$(pgrep httpserver)/fd/

# Memory checking
valgrind --leak-check=full ./httpserver

8. Extensions & Challenges

After completing the base project:

  1. HTTPS/TLS: Integrate OpenSSL for secure connections
  2. HTTP/2: Implement binary framing and multiplexing
  3. WebSocket: Upgrade connections to WebSocket
  4. Compression: gzip/deflate response compression
  5. Rate Limiting: Token bucket per IP
  6. Connection Pooling: Reuse connections to backends
  7. Health Checks: /health endpoint with metrics
  8. Graceful Shutdown: Drain connections on SIGTERM
  9. Worker Threads: Multi-threaded request handling
  10. io_uring: Modern Linux async I/O

9. Real-World Connections

Your Implementation Production Server
epoll event loop Nginx, Node.js
State machine parser http-parser, llhttp
Trie router httprouter (Go), actix-web
Middleware chain Express.js, Koa
Keep-alive All modern servers

Industry Examples:

  • Nginx: Event-driven, C, handles millions of connections
  • Envoy: C++, modern proxy with advanced features
  • Caddy: Go, automatic HTTPS
  • Drogon: C++, high-performance web framework

10. Resources

Primary Resources

Code References

  • llhttp - Node.js HTTP parser
  • libuv - Cross-platform async I/O
  • Drogon - C++ web framework

Video Resources


11. Self-Assessment Checklist

Before considering this project complete:

  • Can you explain the HTTP/1.1 request/response format?
  • Can you describe how epoll differs from select?
  • Can you trace a request through the entire system?
  • Does the server handle 1000+ concurrent connections?
  • Do keep-alive connections work correctly?
  • Are timeouts implemented for slow clients?
  • Does the server shut down gracefully?
  • What is the requests/second on your machine?

12. Submission / Completion Criteria

Your project is complete when:

  1. HTTP Parser Works
    • Parses all valid HTTP/1.1 requests
    • Handles partial reads correctly
    • Rejects malformed requests
  2. Event Loop Is Efficient
    • Uses epoll (Linux) or kqueue (macOS)
    • Handles 10,000+ connections
    • No busy waiting
  3. Router Matches Correctly
    • Static routes work
    • Path parameters extract correctly
    • Correct handler selected by method
  4. Middleware Chain Works
    • Multiple middleware execute in order
    • Early termination works
    • Response modifications work
  5. Performance Meets Targets
    • 100,000+ requests/second possible
    • Sub-millisecond latency
    • No memory leaks

Deliverables:

  • Source code with clear organization
  • Example server demonstrating features
  • Benchmark results
  • README with API documentation