CServe
Project Status: Deprecated & Archived
CServe was my first attempt at building a web server in C. It is now archived and has been completely succeeded by KinetiC. CServe was built under tight product constraints, leading to ad-hoc parsing logic and minor memory bugs under heavy load. The lessons learned here directly inspired KinetiC's strict RFC-compliant, invariant-based FSM design.
Architectural Milestones (v0.1.0)
Before it was archived at version 0.1.0 (commit 5dd04dc), CServe evolved through several key milestones. Here is the detailed story of its implementation based on the commit history.
graph TD
A[Basic TCP Sockets] --> B[Dynamic Array & Parsing]
B --> C[HTTP Request-Response Pipeline]
C --> D[Static Files & Config Parsing]
D --> E[Reverse Proxy & epoll Loop]
E --> F[Keep-Alive & Connection Lifecycle]
Step 0: Basic TCP Server (Commits d673871 - 84e6622)
The server's foundation began by initializing standard POSIX sockets:
- Socket Construction (
server_constructor): Configured options likeSO_REUSEADDRto prevent socket binding failures caused by the standard TCPTIME_WAITstate, allowing immediate port reuse during crashes or restarts. - Connection Acceptance (
launch): Implemented a basic blocking loop runningaccept()to capture incoming files, logging connection handles to standard output before processing payloads.
Step 1: Custom Vector Struct (Commits 3a04179 - 3004d56)
To handle dynamic headers and request metadata without depending on external libraries, CServe implemented its own dynamic array structure:
- Initial Trials: The vector structure used a basic pointer struct containing a pointer to data, current length, and capacity. Memory was managed using
realloc()blocks to scale when capacity limits were hit. - Refactoring to Vector: Refactored the array into a generic
Vectorcontainer to hold custom string buffers. - Safety Testing: Integrated the Check unit testing framework and Valgrind memory analysis to find and fix dynamic allocation memory leaks (such as missing
free()calls during vector deallocations).
Step 2: HTTP Parsing & First Responses (Commits 5a9565d - c429e66)
The server transitioned from a TCP echo service to a basic HTTP server:
- Request Structures (
HTTPRequest): Defined fields for the HTTP method, path, HTTP version, and a list of headers. - Request Line Splitting: Parsing was implemented using standard string-manipulation functions like
strtokandstrchrto split the payload by carriage return / line feeds (\r\n). - Dynamic Allocation: Evaluated whether to store connection descriptors and parsed data using static stacks or dynamic heap buffers (
HTTPServerallocation questions). This was resolved by dynamically allocating methods, paths, and headers, and introducinghttp_request_freehelpers.
Step 3: Configuration Parsing & Static File Serving (Commits 5a1bf62 - ffa0092)
Hardcoded values were replaced with configurable environments:
- INI File parsing (
parse_config): A dedicated parser was written to read port, static directories, and proxy backend ports line-by-line, parsing lines into key-value pairs. - File System Interaction: Implemented dynamic file reads based on request paths. The server opened files from the local directory, resolved basic content types, and wrote content buffers directly into the client sockets.
Step 4: Reverse Proxying & Epoll Loop (Commits b65ad43 - dc0b2b4)
To support advanced load distribution:
- Reverse Proxying: Forwarded client requests to backend processes (e.g. FastAPI on Uvicorn) via secondary sockets. CServe rebuilt the client request string, connected to the backend host, and piped the backend response back to the client.
- Round-Robin Load Balancing: Added load balancing across multiple backends, tracking the target index via a simple round-robin function (
choose_backend) and redirecting requests accordingly. - Epoll Integration: Replaced the blocking
accept()loop with a Linux-specificepollasynchronous event loop to handle multiple incoming client connections without spawning one thread per connection.
Step 5: Keep-Alive & Parser Refactoring (Commits eff3867 - 220522e)
During testing, strtok parsing showed significant limits with fragmented TCP packets:
- Indices-based parsing: The parser was refactored to use index offset pointers instead of destructive
strtok()calls, allowing parsing of request lines without modifying the raw read buffer. - Keep-Alive and Buffering: Introduced client
Connectiontracking structures, individual read buffers, and Keep-Alive state flags. - Archiving: Difficulties resolving socket resets and pipelining errors on persistent connections led to freezing CServe at version
0.1.0and beginning the clean-room KinetiC project.