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:
- Embedded / tight resources — whole Beast may be too large; a small parser + router fits in tens of KB.
- Mixed protocols — HTTP + WebSocket + custom TCP on one port; owning the pipeline helps.
- Learning / interviews — explaining routing tables and middleware chains is easier after you have built one.
- 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_mapor 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::acceptoraccept connections; each session usesasync_read_until/async_readuntil\r\n\r\n, then optional body via Content-Length.- Parsed Request → Router → middleware chain → handler → serialize Response →
async_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)
| Issue | Mitigation |
|---|---|
| Connection reset / EOF | Treat eof / connection_reset as normal close |
| Chunked / missing Content-Length | Minimal server may skip body; full servers need chunked support |
\r left on lines | Strip trailing \r after getline |
| Use-after-free in handlers | shared_from_this in async lambdas |
| Huge bodies | Cap Content-Length (e.g. 413) |
| Keep-alive buffer reuse | buffer_.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;
/healthendpoint.
| Library | Notes |
|---|---|
| Boost.Beast | Production HTTP/WebSocket on Asio |
| Crow | Header-only, Express-like |
| Drogon | Full async stack, C++17 |
| Custom | Learning, embedded, tight control |
Related posts
- 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
| Piece | Idea |
|---|---|
| Flow | Acceptor → Session → Parser → Router → Middleware → Handler → write |
| Routing | (method, path) → handler |
| Middleware | Pre/post around next(req) |
| Safety | error_code, shared_from_this, body limits |
Previous: Redis clone #48-1
Next: Memory pool #48-3