Skip to content

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rust Web Server

A stdlib-only multithreaded HTTP web server written in Rust. It listens on a TCP socket, accepts concurrent client connections, parses HTTP/1.1 requests, and routes them to registered handlers. Work is dispatched through a reusable thread pool instead of spawning a new thread for every request.

The project is built up in six incremental commits, each one a layer of polish — error handling, real HTTP parsing, routing, generic handlers, tests, and ergonomic shutdown. Every piece is designed to be testable in isolation and to map to a Rust concept you can talk through in an interview.

Project Structure

rust-web-server/
├── Cargo.toml          # Package manifest (no external dependencies)
├── hello.html          # Static page served at GET /
├── 404.html            # Legacy static page (the new 404 is built inline)
├── src/
│   ├── lib.rs          # ThreadPool + Worker (named threads, explicit shutdown)
│   ├── main.rs         # CLI args, TcpListener, dispatch to pool
│   ├── error.rs        # ServerError + Result<T>
│   ├── http.rs         # Request, Response, StatusCode, Method, parser
│   ├── handler.rs      # handle_connection<R: Read, W: Write>
│   └── router.rs       # Router with handler dispatch
└── README.md
File Role
src/lib.rs Fixed-size ThreadPool with named workers, graceful shutdown, and a typed job queue.
src/main.rs Reads port and pool_size from CLI, binds the listener, dispatches each connection.
src/error.rs ServerError enum with Display, Error, and From<io::Error> so ? works everywhere.
src/http.rs Request::parse, Response::write_to, Method, StatusCode.
src/handler.rs handle_connection<R: Read, W: Write> — the HTTP logic has zero socket coupling.
src/router.rs (Method, path) → handler dispatch, with 404 / 405 fallback.

Quick Start

Prerequisites: Rust (2024 edition)

# Build
cargo build

# Run with defaults (127.0.0.1:7878, 4 workers)
cargo run

# Or override the port and pool size
cargo run 8080 8

In another terminal:

# 200 OK — serves hello.html
curl http://127.0.0.1:7878/

# 200 OK — intentionally slow (10 s); useful for observing thread-pool concurrency
curl http://127.0.0.1:7878/sleep

# 200 OK — health check
curl http://127.0.0.1:7878/health

# 404 NOT FOUND — unknown route
curl http://127.0.0.1:7878/unknown

# 405 METHOD NOT ALLOWED — known path, wrong method
curl -X POST http://127.0.0.1:7878/

Routes

Route Method Status Response body
/ GET 200 hello.html (embedded at compile time via include_str!)
/sleep GET 200 hello.html after a 10 s sleep
/health GET 200 ok (text/plain)
Anything else GET 404 <h1>404 Not Found</h1>
Known path, wrong method * 405 <h1>405 Method Not Allowed</h1>

Architecture

Client                       Main thread                       Thread pool (4 workers)
  │                                │                                       │
  │  TCP connect                   │                                       │
  ├──────────────────────────────►│  TcpListener::accept                 │
  │                                │  pool.execute(handle_connection)     │
  │                                ├──────────────────────────────────────►│
  │                                │                                       │ parse request
  │                                │                                       │ route by (Method, path)
  │                                │                                       │ build response
  │  HTTP response                 │                                       │ write response
  │◄───────────────────────────────┼───────────────────────────────────────┤
  1. The main thread binds 127.0.0.1:<port> and accepts connections.
  2. Each accepted TcpStream is split via try_clone, wrapped in a closure, and sent to the thread pool via execute.
  3. A worker thread runs handle_connection: parses the request, looks up the handler in the router, and writes the response.

What This Project Demonstrates

1. Custom error type with ? propagation

ServerError is the single error type used in the request path. It implements Display, Error, and From<io::Error> so the ? operator works on any fallible I/O. No unwrap() in the request path.

2. Real HTTP parsing

Request::parse reads a real HTTP/1.1 request line and headers (via BufReader::read_line), capturing the method, path, and headers into a Request struct. Paths are trimmed; headers are lowercased.

3. Trait-object-free routing

Router uses function pointers (fn(&Request) -> Response) for zero-cost dispatch. Router::global() returns a &'static instance via OnceLock, so the dispatch is allocation-free per request. The router distinguishes 404 (no such path) from 405 (path exists for a different method).

4. Generic handler for testability

handle_connection<R: Read, W: Write> decouples the HTTP logic from TcpStream. The same function runs against a real socket in main.rs and against a &[u8] + Vec<u8> in tests, with no network, no port, no flakiness. This is the move that separates learning code from production code.

5. Thread-pool with named workers and graceful shutdown

ThreadPool::new spawns size workers named worker-0, worker-1, …, visible in htop and ps -L. The Drop impl drops the Sender, lets workers' recv() return Err, and joins them sequentially. ThreadPool::shutdown is the explicit, idempotent version of the same logic.

6. Concurrency primitives — why each one

  • mpsc::channel — multi-producer, single-consumer queue from std::sync::mpsc.
  • Arc<Mutex<Receiver<Job>>> — mpsc::Receiver is Send but not Sync, so we wrap it in Mutex to share across workers. Arc provides shared ownership; Rc would not cross thread boundaries.
  • Box<dyn FnOnce() + Send + 'static> — type-erased closure. Send because the job moves to a worker thread; 'static because the job may live in the queue indefinitely.
  • The MutexGuard is dropped at the end of the receiver.lock().unwrap().recv() statement — job() runs outside the lock, so workers don't serialize.

7. Tests + doc comments

Every public type has unit tests against &[u8] + Vec<u8> (no sockets). Public functions have /// doc comments with runnable examples — cargo test --doc proves them.

Build-up Commits

The repository history is intentionally a curriculum. Reading it in order tells six stories:

  1. add ServerError type, propagate Result in main — replace unwrap() with ? and a single error type.
  2. add http parser and response types — replace byte-prefix matching with a real HTTP/1.1 parser.
  3. add router with handler dispatch — replace the if/else routing chain with a static route table.
  4. extract generic handle_connection into handler.rs — make the HTTP logic generic over R: Read, W: Write so it can be tested without a socket.
  5. add unit tests, doc comments, and 405 Method Not Allowed — add 404/405 distinction, tests, and runnable doc examples.
  6. named worker threads, CLI args, explicit shutdown — name the workers (worker-0…), expose ThreadPool::shutdown, read port and pool_size from CLI args.

Limitations (by design)

This is an educational, minimal HTTP server — not production-ready:

  • No TLS/HTTPS
  • No HTTP/1.1 keep-alive or pipelining (Connection: close on every response)
  • No POST body parsing
  • No query string, no MIME sniffing beyond a static Content-Type
  • Single-process; no async runtime (Tokio, etc.)
  • No bounded queue (unbounded mpsc::channel)

These constraints keep the focus on sockets, HTTP basics, ownership, and concurrency — the same fundamentals the project highlights.

License

See repository settings for license information.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages