Skip to content

Rujul-1105/rust-web-server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rust Web Server

A multithreaded HTTP web server written in Rust from first principles. It listens on a TCP socket, accepts concurrent client connections, parses minimal HTTP GET requests, and serves static HTML files with well-formed HTTP responses. Work is dispatched through a reusable thread pool instead of spawning a new thread for every request.

This project follows the patterns from The Rust Programming Language, Chapter 20, extended into a small library + binary crate layout.

Project Structure

rust-web-server/
├── Cargo.toml          # Package manifest (no external dependencies)
├── hello.html          # Static page served at GET /
├── 404.html            # Static page served for unknown routes
├── src/
│   ├── lib.rs          # Reusable ThreadPool implementation
│   └── main.rs         # TCP listener, HTTP handling, server entry point
└── README.md
File Role
src/main.rs Binds a TCP listener, accepts connections, and delegates each request to the thread pool
src/lib.rs Generic thread pool: fixed worker threads, job queue, graceful shutdown
hello.html / 404.html Static content read from disk and embedded in HTTP responses

Quick Start

Prerequisites: Rust (2024 edition)

# Build
cargo build

# Run the server (listens on 127.0.0.1:7878)
cargo run

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

# 404 NOT FOUND — serves 404.html
curl http://127.0.0.1:7878/unknown

Note: main.rs currently accepts only two connections (listener.incoming().take(2)) before shutting down. Remove .take(2) for a long-running server.

Architecture

Client                    Main thread                         Thread pool (4 workers)
  │                            │                                      │
  │  TCP connect               │                                      │
  ├───────────────────────────►│  TcpListener::accept               │
  │                            │  pool.execute(handle_connection)   │
  │                            ├─────────────────────────────────────►│
  │                            │                                      │ read request
  │                            │                                      │ match route
  │                            │                                      │ read HTML file
  │  HTTP response             │                                      │ write response
  │◄───────────────────────────┼──────────────────────────────────────┤
  1. The main thread binds 127.0.0.1:7878 and blocks on incoming TCP connections.
  2. Each accepted TcpStream is wrapped in a closure and sent to the thread pool via execute.
  3. A worker thread runs handle_connection: reads bytes from the socket, interprets the request line, loads the matching HTML file, and writes a complete HTTP response back.

How the Code Illustrates Each Concept

1. Multithreaded HTTP server over TCP sockets

The server is built entirely on Rust’s standard library networking and I/O primitives—no HTTP framework.

Concept Where in code What it does
TCP listening socket TcpListener::bind("127.0.0.1:7878") in main.rs Creates a bound socket and listens for inbound connections
Accepting clients for stream in listener.incoming() in main.rs Iterates over incoming TcpStream handles, one per client
Reading the request stream.read(&mut buffer) in handle_connection Reads raw bytes from the socket into a fixed buffer
Parsing HTTP buffer.starts_with(get) / starts_with(sleep) in handle_connection Matches the HTTP request line (GET / HTTP/1.1, etc.)
Serving static content fs::read_to_string(filename) in handle_connection Loads hello.html or 404.html from disk
Valid HTTP response format!("{}\r\nContent-Length: {}\r\n\r\n{}", ...) + stream.write_all Builds status line, Content-Length header, blank line, and body; writes to the socket
// main.rs — bind, accept, and dispatch
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);

for stream in listener.incoming().take(2) {
    let stream = stream.unwrap();
    pool.execute(|| {
        handle_connection(stream);
    });
}
// main.rs — request parsing and response generation
let (status_line, filename) = if buffer.starts_with(get) {
    ("HTTP/1.1 200 OK", "hello.html")
} else if buffer.starts_with(sleep) {
    thread::sleep(Duration::from_secs(10));
    ("HTTP/1.1 200 OK", "hello.html")
} else {
    ("HTTP/1.1 404 NOT FOUND", "404.html")
};

let response = format!(
    "{}\r\nContent-Length: {}\r\n\r\n{}",
    status_line,
    contents.len(),
    contents
);
stream.write_all(response.as_bytes()).unwrap();

The /sleep route deliberately blocks for 10 seconds. With the thread pool, other requests can still be processed by idle workers—demonstrating concurrent connection handling without blocking the accept loop on slow clients.


2. Reusable thread pool (replacing thread-per-request)

Instead of thread::spawn for every connection, the server reuses a fixed set of worker threads. This reduces thread creation/teardown overhead and caps resource usage under load.

Concept Where in code What it does
Pool construction ThreadPool::new(4) in main.rs Spawns 4 long-lived worker threads at startup
Job submission pool.execute(|| { ... }) in main.rs Sends a closure (the “job”) to an idle worker
Job queue mpsc::channel() in lib.rs Channel carries Box<dyn FnOnce() + Send> jobs from producers to workers
Shared receiver Arc<Mutex<mpsc::Receiver<Job>>> in lib.rs All workers share one receiver safely
Worker loop Worker::new in lib.rs Each worker blocks on recv(), runs the job, then waits for the next
Graceful shutdown impl Drop for ThreadPool in lib.rs Drops the sender, workers exit their loops, join() waits for clean termination
// lib.rs — enqueue a job without spawning a new thread
pub fn execute<F>(&self, f: F)
where
    F: FnOnce() + Send + 'static,
{
    let job = Box::new(f);
    self.sender.as_ref().unwrap().send(job).unwrap();
}
// lib.rs — worker threads pull jobs from the shared channel
let thread = thread::spawn(move || loop {
    let message = receiver.lock().unwrap().recv();
    match message {
        Ok(job) => job(),
        Err(_) => break,  // channel closed → shutdown
    }
});

Thread-per-request vs. thread pool: spawning a thread per connection works for small demos but scales poorly—each thread consumes stack memory and kernel bookkeeping. A pool amortizes creation cost and bounds concurrency to size workers, improving scalability for many simultaneous clients.


3. Core systems programming concepts

Concept Illustration in this codebase
Network programming std::net::{TcpListener, TcpStream} — low-level TCP server/client streams without a web framework
Socket communication read, write_all, and flush on TcpStream — byte-oriented bidirectional I/O over the network
HTTP protocol Manual request-line matching and response assembly (status line, headers, \r\n\r\n separator, body)
Ownership stream is moved into the execute closure (FnOnce); jobs are owned Box<dyn FnOnce()>; Arc shares the receiver across workers without copying it
Concurrency Multiple OS threads (thread::spawn), synchronized access via Mutex, message passing via mpsc, and JoinHandle for joining workers on shutdown
Performance / resource control Fixed pool size limits threads; channel decouples the accept loop from request handling; /sleep route shows non-blocking concurrency across workers

Rust’s type system enforces thread safety at compile time:

  • Send on closures ensures captured data can be transferred to worker threads.
  • 'static on job closures prevents use-after-free across thread lifetimes.
  • Arc<Mutex<Receiver>> provides shared, mutable access to the job queue without data races.

Supported Routes

Route Status Response body Notes
GET / 200 OK hello.html Default landing page
GET /sleep 200 OK hello.html Sleeps 10 s before responding (concurrency demo)
Anything else 404 NOT FOUND 404.html Unknown paths

Only the request line is parsed; headers and request bodies are ignored.

Limitations (by design)

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

  • No TLS/HTTPS
  • No HTTP/1.1 keep-alive or pipelining
  • No POST, PUT, or other methods
  • No query strings, routing framework, or MIME type detection
  • Single-process; no async runtime (Tokio, etc.)
  • Request buffer fixed at 1024 bytes

These constraints keep the focus on sockets, HTTP basics, ownership, and concurrency—the same fundamentals highlighted in the project goals above.

License

See repository settings for license information.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages