Skip to content

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 like SO_REUSEADDR to prevent socket binding failures caused by the standard TCP TIME_WAIT state, allowing immediate port reuse during crashes or restarts.
  • Connection Acceptance (launch): Implemented a basic blocking loop running accept() 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 Vector container 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 strtok and strchr to 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 (HTTPServer allocation questions). This was resolved by dynamically allocating methods, paths, and headers, and introducing http_request_free helpers.

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-specific epoll asynchronous 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 Connection tracking 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.0 and beginning the clean-room KinetiC project.