This is my project for a multi-threaded HTTP server, built from scratch in Python.
The goal was to understand how HTTP, sockets, and threading work together at a low level.
I built it using only Python's standard libraries, so no external packages are needed.
The server can serve static files for a simple website (HTML, images, text) and also handle JSON uploads through a POST request.
- Handles
GETandPOSTrequests for basic web functionality. - Serves static files like
.htmlpages,.jpgand.pngimages, and.txtfiles. - Uses a thread pool to manage multiple client connections at the same time without crashing.
- Supports
Keep-Aliveconnections with an idle timeout to manage resources efficiently. - Includes important security checks to prevent common attacks like path traversal.
- Python 3.7 or newer ✅
-
Clone or download the project files.
-
Make sure you have the
resourcesfolder with all the website files inside (index.html,Image1.jpg,jokes.txt, etc.). -
Run the server from your terminal:
# This runs the server with the default settings # (Host: 127.0.0.1, Port: 8080, Threads: 10) python3 server.py
You can also provide your own settings for the port, host, and thread count:
# Example: run on port 9000 with 5 threads python3 server.py 9000 127.0.0.1 5 -
Open your browser and go to
http://127.0.0.1:8080to see the home page.
To test the binary file download feature, navigate to the following URLs in your browser:
- To download a text file:
http://127.0.0.1:8080/jokes.txt - To download an image:
http://127.0.0.1:8080/Image1.jpg
In both cases, your browser should not display the file directly but will instead open a "Save As" dialog.
This behavior is triggered by the Content-Disposition: attachment header sent by the server.
You can test POST requests using curl or Postman.
curl -X POST \
[http://127.0.0.1:8080/upload](http://127.0.0.1:8080/upload) \
-H "Content-Type: application/json" \
-d '{"username": "shiva", "message": "Testing POST request!"}'- Set the method to POST.
- Set the URL to
http://127.0.0.1:8080/upload. - Go to the Body tab, select raw, and choose JSON from the dropdown.
- Paste your JSON content in the text area.
After sending the request, the server should respond with a 201 Created status and a JSON body confirming success.
You can then check the resources/uploads directory to see the newly created file.
This server is built around a few key functions that handle everything from listening for connections to sending back the final response.
Everything starts in the run_server function.
Socket Creation:
- A TCP socket is created using
socket.socket(socket.AF_INET, socket.SOCK_STREAM). AF_INETspecifies IPv4 networking, andSOCK_STREAMmeans a TCP socket, guaranteeing reliable, in-order data delivery.
Binding:
- The socket is bound to the specified host and port with
server.bind((host, port)). - Like assigning a specific phone number to a phone so it can receive calls.
Listening:
server.listen(LISTEN_BACKLOG)puts the socket in listening mode, ready to accept incoming client connections.
Main Loop:
- The server enters an infinite
while True:loop. server.accept()is a blocking call—it waits for a client to connect.
- To avoid being stuck handling one client at a time, the server uses a
ThreadPoolExecutor. - When
server.accept()receives a new connection, it submits the client-handling task to the thread pool:
pool.submit(client_wrapper, conn, addr)The client_wrapper function:
- Sets a descriptive name for the thread.
- Calls
serve_client, which handles a single client’s requests. - If the thread pool is full, new connections are placed in a
connection_queue.
When a thread finishes, it picks up the next waiting connection.
The serve_client function manages the entire lifecycle of a client connection.
- Handles multiple requests from the same client (up to
MAX_PERSISTENT_REQUESTS). - Core of Keep-Alive functionality.
- Uses
conn.recv(8192). - Blocking call; waits until data arrives.
conn.settimeout(KEEP_ALIVE_TIMEOUT)(usually 30s) prevents indefinite connections.- Raises
socket.timeoutif no data is received, then closes the connection.
The parse_request_bytes function:
- Decodes bytes into UTF-8 string.
- Separates headers & body by
\r\n\r\n. - Extracts request line (method, path, HTTP version).
- Example:
GET /index.html HTTP/1.1
- Example:
- Parses headers into a Python dictionary.
- Passed to
handle_get. - Locates the file, sets correct
Content-Type, reads content in binary mode.
- Passed to
handle_post. - Validates
Content-Type: application/json, parses JSON, saves toresources/uploads/, returns 201 Created.
- Responds with 405 Method Not Allowed for unsupported methods like
PUTorDELETE.
The build_response function:
-
Status Line: e.g.,
HTTP/1.1 200 OK -
Headers:
Date,Server,Content-Type,Connection, plus any custom headers -
Formatting: Headers joined with
\r\n -
Send Response: Encoded to bytes and sent via
conn.sendall() -
Keep-Alive continues loop; otherwise,
conn.close()ensures safe closure.
- Large Files: Reads entire file into memory; inefficient for huge files.
- Basic HTTP Support: Only supports a subset of HTTP/1.1; no HTTP/2, chunked encoding, or caching.
- Single
resourcesFolder: All files must reside inside it.
This project demonstrates:
- The core mechanics of HTTP communication.
- Using sockets for low-level networking.
- Using thread pools for handling concurrent client connections efficiently.
Author: Shiva Gupta
Feel free to explore, test, and improve it! 🚀