A compact HTTP/1.x server written in C11 using POSIX TCP sockets and a fixed pthread worker pool. It exists to make the layers normally hidden by a web framework visible: accepting a TCP connection, finding an HTTP header boundary, reading a length-delimited body, mapping a safe URL path to disk, and coordinating concurrent work without a thread-per-request explosion.
This is intentionally a learning-oriented, portfolio-ready systems project—not a production reverse proxy. The code is split by responsibility and comments call out the non-obvious POSIX choices.
Linux/macOS with a C compiler and POSIX threads is required.
make
./webserverThe default server listens at http://127.0.0.1:8080, serves www/, and starts eight workers. Configuration is available without recompiling:
./webserver --port 9000 --root ./www --threads 12In another terminal:
curl -i http://127.0.0.1:8080/
curl -i http://127.0.0.1:8080/hello.txt
curl -i --data 'hello from curl' http://127.0.0.1:8080/echoPOST currently logs the request target and byte count, then returns the exact request body as application/octet-stream. This deliberately small behavior verifies correct Content-Length framing while leaving a clear place to add routing or persistence later.
Press Ctrl-C to stop accepting connections. Already accepted connections in the queue and active workers finish before the process exits.
- Raw IPv4 TCP listener with
SO_REUSEADDR GETfor files under the configured document root, including MIME types for HTML, CSS, JavaScript, PNG, JPEG, text, and JSONPOSTbody handling byContent-Length- Correct
Content-Length,Content-Type, status line, andConnection: closeresponse headers - Basic errors: 400 malformed requests, 404 missing files, 405 unsupported methods, 413 oversized headers/bodies, and 500 read failures
- Configurable fixed-size worker pool (default: 8) with a bounded FIFO socket queue
- Graceful
SIGINTshutdown that drains accepted work and joins every worker - Path traversal and document-root escape prevention, including symlinks resolved outside the root
The lifecycle and synchronization model are described in ARCHITECTURE.md. The important design choice is a fixed worker pool, not one thread per connection. A thread-per-connection server makes every burst create more stacks and scheduling overhead, can exhaust OS resources, and gives no natural backpressure. Here the queue has a finite capacity of 4 × worker_count; when full, accept pauses until a worker makes room.
The normal build treats warnings as errors:
make clean && makeOn Linux, build a ThreadSanitizer instrumented binary and exercise simultaneous requests:
make tsan
./webserver --threads 8
# Separate terminal
seq 1 100 | xargs -n1 -P16 -I{} curl -fsS http://127.0.0.1:8080/hello.txt >/dev/nullFor leak checks (with the normal, non-TSan build):
make clean && make
valgrind --leak-check=full --show-leak-kinds=all ./webserverThen issue a few curl requests, stop with Ctrl-C, and inspect Valgrind's final summary. Run either TSan or Valgrind at a time; their instrumentation should not be combined.
- HTTP/1.0 and HTTP/1.1 request lines are accepted, but every response closes the connection; keep-alive is not implemented.
- Only
GETandPOSTare supported; there is no routing layer, TLS, authentication, or directory listing. - Parsing is intentionally bounded and minimal, not a full RFC 7230 implementation. Headers are limited to 16 KiB, bodies to 1 MiB, and chunked transfer encoding is rejected.
- The listener is IPv4-only and is aimed at local development/learning, not internet exposure.
- A slow client can occupy one worker while it sends an incomplete request; production servers use timeouts and more sophisticated I/O models.
src/main.c CLI parsing and startup
src/server.c sockets, URL-to-file resolution, request handler, shutdown
src/http_parser.c bounded request/header/body parsing
src/http_response.c response serialization, MIME lookup, file streaming
src/threadpool.c bounded producer/consumer queue and worker lifecycle
www/ sample served content