Introduction
Every day, we type a URL into a browser without thinking about what actually happens behind the scenes. A single click triggers a fascinating chain of events: establishing a TCP connection, parsing an HTTP request, routing it to the correct handler, generating a response, and sending it back to the client—all within milliseconds.
As part of the 42 School Webserv project, my team and I decided to build an HTTP server completely from scratch in C++98. No web frameworks. No Boost. No existing HTTP libraries. Just low-level sockets, the C++ standard library, and a deep dive into how the web actually works.
The goal wasn't just to create another server—it was to understand every layer involved in serving a web page. From creating sockets and accepting client connections to parsing raw HTTP requests, implementing GET, POST, and DELETE methods, serving static files, handling CGI, and generating valid HTTP responses, every component had to be designed and implemented by us.
This project completely changed the way I think about web applications. Before starting, a browser request felt almost magical. Now I understand the complete lifecycle of an HTTP request—from the moment it leaves the browser until the server sends back the response.
In this article, I'll walk through the architecture of our server, explain how HTTP requests are parsed, how different HTTP methods are handled, and share some of the engineering challenges we faced while building everything from the ground up.
Project Overview
The objective is to build a fully functional HTTP/1.1 web server from scratch using C++98, without relying on existing web server implementations or HTTP libraries.
Instead of using mature solutions like Nginx or Apache, we had to implement the fundamental building blocks ourselves. This includes creating TCP sockets, accepting multiple client connections, parsing raw HTTP requests, serving static content, executing CGI scripts, handling file uploads, and generating standards-compliant HTTP responses.
Our implementation follows an event-driven architecture capable of managing multiple clients simultaneously using I/O multiplexing. Every incoming request passes through several stages before a response is returned to the client.
The project is composed of several independent components working together:
Configuration Parser – Reads and validates the server configuration file.
TCP Server – Creates listening sockets, binds to ports, and accepts incoming client connections.
Event Loop – Monitors multiple sockets efficiently using I/O multiplexing.
HTTP Request Parser – Converts raw bytes received from the network into structured HTTP requests.
Router – Determines which server block and location should process the request.
HTTP Method Handlers – Implements the behavior of GET, POST, and DELETE requests.
Static File Server – Serves HTML, CSS, JavaScript, images, and other resources.
CGI Executor – Executes external scripts and returns their output as HTTP responses.
Response Builder – Constructs valid HTTP responses, including status codes, headers, and body content.
Error Handling – Generates appropriate HTTP error pages such as 404 Not Found, 403 Forbidden, and 500 Internal Server Error.
At a high level, every request follows the same lifecycle:
Browser
│
▼
TCP Connection
│
▼
HTTP Request Parser
│
▼
Router
│
▼
HTTP Method (GET / POST / DELETE)
│
├──────────────┐
▼ ▼
Static Files CGI
│ │
└──────┬───────┘
▼
Response Builder
│
▼
HTTP Response
│
▼
Browser
Rather than focusing only on making the server work, our goal was to understand how modern web servers operate internally. Building each component from scratch gave us a much deeper understanding of networking, the HTTP protocol, operating system concepts, and software architecture.
Server Architecture
Before implementing HTTP methods or parsing requests, we first needed a solid server architecture. A web server is much more than a single loop waiting for incoming requests—it's a collection of independent components working together to transform raw network data into valid HTTP responses.
To keep the project modular and maintainable, we separated the server into multiple layers, each responsible for a single task.
+----------------------+
| Browser |
+----------+-----------+
|
| HTTP Request
|
▼
+-------------------------+
| TCP Socket Layer |
| socket() / bind() |
| listen() / accept() |
+-----------+-------------+
|
▼
+--------------------------+
| Event Loop |
| poll() / multiplexing |
+-----------+--------------+
|
▼
+--------------------------+
| HTTP Request Parser |
+-----------+--------------+
|
▼
+--------------------------+
| Router |
+-----------+--------------+
|
+----------------+----------------+
| | |
▼ ▼ ▼
GET Handler POST Handler DELETE Handler
| | |
+----------------+----------------+
|
▼
+--------------------------+
| Response Builder |
+-----------+--------------+
|
▼
HTTP Response
|
▼
Browser
1. TCP Socket Layer
Everything begins with the networking layer.
The server creates one or more listening sockets using the operating system's socket API.
The lifecycle follows the classic sequence:
socket()
↓
bind()
↓
listen()
↓
accept()
Each function has a specific responsibility:
socket() creates a communication endpoint.
bind() associates that socket with an IP address and a port.
listen() tells the kernel that the socket is ready to accept incoming connections.
accept() creates a new client socket for every connected browser while keeping the listening socket available for future clients.
At this stage, the server still has no knowledge of HTTP—it only deals with raw TCP connections.
2. Event Loop
Instead of creating one thread per client, our server uses an event-driven architecture.
The event loop continuously monitors all active sockets using I/O multiplexing (poll() in our implementation).
This allows a single process to manage many simultaneous clients efficiently.
while (server is running)
{
wait for socket events;
if (new client)
accept();
if (client sent data)
read request();
if (socket ready to write)
send response();
}
This design is lightweight, scalable, and forms the foundation of many high-performance web servers.
3. HTTP Request Parser
Once data arrives from a client socket, it is simply a sequence of bytes.
For example:
GET /index.html HTTP/1.1
Host: localhost
User-Agent: Chrome
Connection: keep-alive
The parser transforms this raw text into a structured object:
HttpRequest
{
method = GET;
path = "/index.html";
version = "HTTP/1.1";
headers = {...};
body = "";
}
Having a structured request object makes every subsequent component independent from the networking layer.
4. Router
The router decides which part of the server should handle the request.
It examines information such as:
Requested URI
Server block
Location block
HTTP method
Configuration rules
Depending on the request, it may choose to:
Serve a static file
Execute a CGI script
Upload a file
Delete a resource
Return an error page
5. HTTP Method Handlers
Each supported HTTP method is implemented independently.
This separation makes the code easier to maintain and extend.
For example:
GET
│
├── Validate request
├── Resolve file path
├── Read file
└── Build HTTP response
POST
│
├── Validate request
├── Read body
├── Save uploaded file
└── Return success response
DELETE
│
├── Validate request
├── Locate resource
├── Delete file
└── Return status response
Each handler is responsible only for its own business logic.
6. Response Builder
Regardless of which handler processes the request, the final step is always the same: generating a valid HTTP response.
A typical response contains:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Connection: keep-alive
<html>
...
</html>
The response builder is responsible for:
Choosing the correct status code.
Generating HTTP headers.
Calculating Content-Length.
Setting the appropriate Content-Type.
Appending the response body.
Why This Architecture?
We intentionally designed the project around separation of responsibilities.
Each component has a single purpose:
The networking layer only manages TCP connections.
The parser only understands HTTP syntax.
The router only decides where requests should go.
Method handlers implement application logic.
The response builder constructs HTTP-compliant responses.
This modular architecture made the project easier to debug, extend, and maintain, while closely reflecting the internal design of production-grade web servers such as Nginx and Apache.
5. From Browser to Response: The Complete HTTP Request Lifecycle
At this point, we have explored the different components that make up our web server. The next step is to understand how these components interact when a browser sends an HTTP request.
Although the process happens in just a few milliseconds, several independent layers collaborate before the browser finally receives a response.
The following diagram illustrates the complete request lifecycle implemented in our server.
┌──────────────┐
│ Browser │
└──────┬───────┘
│
│ GET /index.html HTTP/1.1
▼
┌──────────────────────────────┐
│ TCP Connection Established │
│ socket → accept() │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Receive Raw Bytes │
│ recv() │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ HTTP Request Parser │
│ Method │ URI │ Headers │ Body│
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Router │
│ Match Server & Location │
└──────────────┬───────────────┘
│
┌────────┴─────────┐
│ │
▼ ▼
Static File CGI Script
│ │
└────────┬─────────┘
▼
┌──────────────────────────────┐
│ HTTP Method Handler │
│ GET / POST / DELETE │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Response Builder │
│ Status │ Headers │ Body │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ send() │
└──────────────┬───────────────┘
│
▼
Browser Receives Page
Step 1 — The Browser Sends an HTTP Request
Everything begins when the user enters a URL into the browser.
For example:
GET /index.html HTTP/1.1
Host: localhost:8080
User-Agent: Chrome
Accept: text/html
Connection: keep-alive
At this stage, the browser doesn't send an object or a function call. It simply sends a stream of bytes over an established TCP connection.
Step 2 — The Server Accepts the Connection
Our listening socket continuously waits for new incoming connections.
When a client connects, the operating system creates a new socket dedicated to that client.
The listening socket remains available to accept future connections, while the new client socket is used for all communication with that specific browser.
Step 3 — Receiving Raw Data
The server reads data from the client socket using recv().
Initially, the received data is nothing more than raw text.
GET /index.html HTTP/1.1
Host: localhost
...
At this point, the server still doesn't know where the request should go or what action needs to be performed.
Step 4 — Parsing the Request
The parser analyzes the raw HTTP message and extracts every important component.
It converts the incoming bytes into a structured object similar to:
HttpRequest
{
method = GET;
path = "/index.html";
version = "HTTP/1.1";
headers = {...};
body = "";
}
From this point onward, every module works with structured data instead of raw network bytes.
Step 5 — Routing the Request
Once the request has been parsed, the router determines how it should be processed.
Based on the requested URI and the server configuration, it decides whether the request should:
Serve a static file
Execute a CGI program
Upload data
Delete a resource
Return an error page
This layer separates request interpretation from request execution.
Step 6 — Executing the HTTP Method
The router forwards the request to the appropriate HTTP method handler.
For example, a GET request follows a workflow similar to this:
GET Request
│
▼
Validate HTTP Method
│
▼
Validate Request URI
│
▼
Resolve Target Path
│
▼
Check File Existence
│
├── File Found ───────────────► Read File
│ │
│ ▼
│ Detect Content-Type
│ │
│ ▼
│ Build HTTP 200 Response
│
└── File Not Found ─────────────► Build HTTP 404 Response
The same principle applies to POST and DELETE, each implementing its own validation and business logic.
Step 7 — Building the HTTP Response
Regardless of which handler processes the request, every execution ends by constructing a valid HTTP response.
For example:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 512
Connection: keep-alive
<html>
...
</html>
The response builder ensures that the client receives a standards-compliant HTTP message, complete with the correct status line, headers, and body.
Step 8 — Sending the Response
Finally, the server transmits the response back to the client using send().
Once the browser receives the response, it begins rendering the HTML document, downloading additional resources such as CSS, JavaScript, and images, and displaying the page to the user.
From the user's perspective, the page appears almost instantly. Behind the scenes, however, it has passed through every layer of our web server—from the TCP socket to the HTTP parser, router, method handler, and response builder.

Top comments (0)