Build a Minimal C++ HTTP Framework from Scratch with Asio [#48-2]

Build a Minimal C++ HTTP Framework from Scratch with Asio [#48-2]

이 글의 핵심

From scratch: parse HTTP requests, map method+path to handlers, chain middleware, and respond with async_write—minimal C++ HTTP server patterns with Asio.

Introduction: “A tiny Express-style HTTP stack in C++“

Why build it yourself?

Production HTTP servers combine routing, middleware, and async I/O. Boost.Beast or Crow are the right defaults for shipping code. A from-scratch minimal stack still helps when:

  1. Embedded / tight resources — whole Beast may be too large; a small parser + router fits in tens of KB.
  2. Mixed protocols — HTTP + WebSocket + custom TCP on one port; owning the pipeline helps.
  3. Learning / interviews — explaining routing tables and middleware chains is easier after you have built one.
  4. Minimal dependencies — only Asio, no full framework.

This article covers:

  • Parsing: request line, headers, body (line-by-line or small state machine)
  • Routing: method + path → handler (unordered_map or trie)
  • Middleware: wrap Next, logging, auth, shared headers
  • Async: accept → read → parse → handler → async_write

Prerequisites: Asio intro, Redis clone sessions.

Environment: Boost.Asio or standalone Asio, C++14+, e.g. vcpkg install boost-asio.


Architecture

  • io_context + tcp::acceptor accept connections; each session uses async_read_until / async_read until \r\n\r\n, then optional body via Content-Length.
  • Parsed RequestRoutermiddleware chainhandler → serialize Responseasync_write.
flowchart TB
    subgraph Client["Client"]
        C1[HTTP request]
    end
    subgraph Server["Server"]
        A[Acceptor] --> S[Session]
        S --> P[HTTP parser]
        P --> R[Router]
        R --> M[Middleware chain]
        M --> H[Handler]
        H --> W[async_write]
    end
    C1 --> A
    W --> C2[HTTP response]
    C2 --> Client

Parsing state (conceptual): request line → headers until blank line → optional body by Content-Length.

Routing: using Handler = std::function<Response(Request const&)>; keyed by (method, path).

Middleware: using Next = std::function<Response(Request const&)>; and Middleware = std::function<Response(Request const&, Next const&)>.

Async note: Handler + router can stay synchronous on a single thread; serialize Response to HTTP bytes and async_write. For multiple io_context::run threads, use a strand or make router read-only after init—see strand guide.


Common failures (summary)

IssueMitigation
Connection reset / EOFTreat eof / connection_reset as normal close
Chunked / missing Content-LengthMinimal server may skip body; full servers need chunked support
\r left on linesStrip trailing \r after getline
Use-after-free in handlersshared_from_this in async lambdas
Huge bodiesCap Content-Length (e.g. 413)
Keep-alive buffer reusebuffer_.consume after each request

Performance (rule of thumb)

Rough localhost numbers: minimal parser + map routing ~15k req/s; Beast-based higher; nginx much higher. Profile your real workload—DB and I/O usually dominate.


Production patterns

  • Cap concurrent connections; request timeouts; structured logging; graceful shutdown on SIGINT/SIGTERM; /health endpoint.
LibraryNotes
Boost.BeastProduction HTTP/WebSocket on Asio
CrowHeader-only, Express-like
DrogonFull async stack, C++17
CustomLearning, embedded, tight control

  • REST API with Beast
  • HTTP basics
  • Redis clone

FAQ

Q. When do I use this in production?
A. Prefer Beast/Crow for real services. Use this article for design literacy and custom minimal stacks.

Q. Next article?
A. Memory pool (#48-3)

Q. More reading?
A. cppreference, Boost.Beast docs.


Summary

PieceIdea
FlowAcceptor → Session → Parser → Router → Middleware → Handler → write
Routing(method, path) → handler
MiddlewarePre/post around next(req)
Safetyerror_code, shared_from_this, body limits

Previous: Redis clone #48-1
Next: Memory pool #48-3