We have successfully transformed a single-connection socket program into a fully-functional, multi-process HTTP server. Below is a summary of the architectural changes and what was accomplished during this project.
- Makefile: We added a standard
Makefilethat compilessrc/server.cinto an executable calledhttp-server. This ensures builds are reproducible and scalable as we add more files.
- Instead of just printing raw bytes, the server now parses the HTTP Request Line using
sscanf(). It successfully extracts theMethod(e.g., GET), theURI(e.g.,/index.html), and theHTTP Version. - It now correctly identifies the end of the HTTP headers (
\r\n\r\n) so that it doesn't wait indefinitely for the client to close the connection.
- We implemented a routing mechanism:
- We created a
publicdirectory serving as the root folder for web assets. - When a user requests
/, the server automatically routes them topublic/index.html. - The server utilizes
open(), reads the file size withfstat(), and sends back dynamicContent-Lengthheaders along with the binary content of the file.
- We created a
- If a client requests a file that does not exist, the server sends a correct
404 Not Foundresponse.
- To handle multiple users concurrently, we integrated a multi-process architecture using
fork(). - How it works:
- The parent process sits in an infinite loop calling
accept(). - When a client connects, the parent
fork()s a child process. - The child process handles reading, parsing, and serving the requested file, then exits.
- The parent immediately loops back to
accept(), ready for the next client without waiting.
- The parent process sits in an infinite loop calling
- Zombie Process Cleanup: We added a signal handler for
SIGCHLDusingsigaction()andwaitpid()to ensure that when child processes finish, their resources are properly reaped by the OS.
- Building upon the new
HttpHeaderparsing logic, the server now intelligently parses arbitrary HTTP headers, particularly looking forContent-Length. - When a
POSTrequest is made to the/echoendpoint, the server dynamically allocates memory, reads the entire body from the socket (spanning multipleread()calls if necessary), and sends back an exact copy of the payload to the client.
- To prepare the server for larger-scale feature additions, we separated the monolithic
server.cinto distinct, maintainable modules:http_parser: Extracts raw socket data and populates a structuredHttpRequestobject.router: Inspects the method and URI to determine the correct response path.response: Handles formatting valid HTTP responses and transmitting file buffers.server: Now purely dedicated to network socket setup and process management.
- We replaced the heavy, process-based
fork()concurrency model with a lightning-fast Thread Pool architecture using POSIX threads (pthreads). - How it works:
- Upon startup, the server spawns a fixed pool of worker threads (e.g., 8).
- The main thread rapidly loops over
accept()and immediately pushes the new client connection file descriptors into a custom, thread-safe queue. - Worker threads sleep efficiently using condition variables (
pthread_cond_t) until a connection is available. They then wake up, pop the connection from the queue, parse/route/respond, and then close the connection before waiting for the next one.
- This approach drastically reduces the overhead of handling high volumes of concurrent requests while seamlessly integrating with our blocking parsing logic.
- The server now dynamically determines the
Content-Typeheader based on the file extension of the requested file. - It correctly serves
.html,.css,.js,.json,.jpg,.png, and defaults toapplication/octet-streamfor unknown types.
- The server is no longer limited to static files! It can now execute dynamic scripts (like Python or Bash) and stream their output directly to the client.
- How it works:
- Any request with a URI starting with
/cgi-bin/is mapped to thepublic/cgi-bin/directory and routed to a dedicatedhandle_cgi()function. - The server creates two pipes for inter-process communication, uses
fork()to spawn a child process, sets basic CGI environment variables (likeREQUEST_METHODandCONTENT_LENGTH), and executes the script usingexecl(). - Using
dup2(), the child'sstdinandstdoutare redirected to the pipes. The worker thread writes the POST body (if any) to the script, then reads the script's output and streams it back to the client socket.
- Any request with a URI starting with
- Added security checks in the router to explicitly prevent clients from requesting files outside the designated
public/folder. - If a client requests a URI containing
..(e.g.,/../../etc/passwd), the server immediately rejects the request and returns a403 Forbiddenresponse.
- Upgraded the server to support HTTP
Keep-Alive, allowing clients to reuse the same TCP socket for multiple sequential requests. - How it works:
- The
http_parserextracts theConnectionheader. If it is set tokeep-alive, a flag is passed down the pipeline. - In
server.c, the worker thread enters ado-whileloop for that client socket instead of closing it immediately. - A 5-second socket read timeout (
SO_RCVTIMEO) is enforced to ensure the worker thread does not hang indefinitely if the client idles or drops the connection. - Responses dynamically inject
Connection: keep-aliveheaders so the client knows the connection remains open.
- The