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
- 2. Theoretical Foundation
- 3. Project Specification
- 4. Solution Architecture
- 5. Implementation Guide
- 6. Testing Strategy
- 7. Common Pitfalls & Debugging
- 8. Extensions & Challenges
- 9. Real-World Connections
- 10. Resources
- 11. Self-Assessment Checklist
- 12. Submission / Completion Criteria
1. Learning Objectives
By completing this project, you will:
- Master HTTP/1.1 protocol: Understand request/response format, headers, and body handling
- Implement event-driven I/O: Use epoll (Linux) or kqueue (macOS/BSD) for scalable networking
- Design routing systems: Pattern matching for URLs with path parameters
- Build middleware chains: Composable request/response processing
- Handle connection lifecycle: Keep-alive, timeouts, graceful shutdown
- Parse protocols efficiently: State machine-based HTTP parsing
- Optimize for performance: Minimize allocations, maximize throughput
- 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:
- HTTP Parser: State machine for request parsing
- Event Loop: epoll (Linux) or kqueue (macOS)
- Router: Trie-based URL matching with parameters
- Middleware: Composable request/response processing
- Static Files: Efficient file serving with sendfile
- JSON Support: Request/response body handling
- 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:
- Production-capable server: Can handle real traffic
- Deep networking knowledge: Sockets, protocols, async I/O
- Systems programming skills: Low-level optimization
- 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:
- What is the difference between blocking and non-blocking I/O?
- Reference: “The Linux Programming Interface” Chapter 63
- How does epoll differ from select/poll?
- Reference: “Linux System Programming” by Robert Love
- What is the HTTP/1.1 persistent connection model?
- Reference: RFC 7230
- How do you handle partial reads/writes on non-blocking sockets?
- Reference: Stevens “UNIX Network Programming”
- 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
- “Explain the C10K problem and how epoll solves it.”
- Expected: select/poll O(n), epoll O(1) for ready fds
- “What’s the difference between edge-triggered and level-triggered?”
- Expected: ET notifies once, LT notifies while condition true
- “How would you implement connection timeouts?”
- Expected: Timer wheel or priority queue with connection deadlines
- “What happens if you try to write() when the socket buffer is full?”
- Expected: EAGAIN/EWOULDBLOCK, register for EPOLLOUT
- “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:
- HTTPS/TLS: Integrate OpenSSL for secure connections
- HTTP/2: Implement binary framing and multiplexing
- WebSocket: Upgrade connections to WebSocket
- Compression: gzip/deflate response compression
- Rate Limiting: Token bucket per IP
- Connection Pooling: Reuse connections to backends
- Health Checks: /health endpoint with metrics
- Graceful Shutdown: Drain connections on SIGTERM
- Worker Threads: Multi-threaded request handling
- 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
- RFC 7230 - HTTP/1.1 Message Syntax
- epoll man page
- The C10K Problem - Classic article
Code References
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:
- HTTP Parser Works
- Parses all valid HTTP/1.1 requests
- Handles partial reads correctly
- Rejects malformed requests
- Event Loop Is Efficient
- Uses epoll (Linux) or kqueue (macOS)
- Handles 10,000+ connections
- No busy waiting
- Router Matches Correctly
- Static routes work
- Path parameters extract correctly
- Correct handler selected by method
- Middleware Chain Works
- Multiple middleware execute in order
- Early termination works
- Response modifications work
- 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