Post

Building a Simple HTTP Server with Python Sockets

Learn how to build a simple HTTP server using Python's socket module, handle GET and POST requests, and serve static files.

Building a Simple HTTP Server with Python Sockets

Most web servers, like Apache and Nginx, handle HTTP requests, but you can build a basic one using Python’s socket module. This post will walk you through creating a simple HTTP server that can handle basic GET requests.

1. Understanding HTTP and Sockets

HTTP (Hypertext Transfer Protocol) is the foundation of the web. When you visit a website, your browser sends an HTTP request to a server, which responds with an HTTP response containing the requested webpage.

How HTTP Works in a Simple Request-Response Model

  1. Client (Browser) Sends Request:
    1
    2
    
    GET / HTTP/1.1
    Host: localhost:8080
    
  2. Server Processes Request and Sends Response:
    1
    2
    3
    4
    5
    6
    7
    8
    
    HTTP/1.1 200 OK
    Content-Type: text/html
    
    <html>
        <body>
            <h1>Hello, World!</h1>
        </body>
    </html>
    

This basic exchange forms the backbone of how the internet works.


2. Setting Up the Basic Server

Let’s create a simple TCP server that listens for connections on port 8080.

Basic HTTP Server in Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import socket

# Define host and port
HOST = '127.0.0.1'  # Localhost
PORT = 8080         # Port number

# Create a socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((HOST, PORT))
server_socket.listen(5)  # Allow up to 5 clients in queue

print(f"Server running on http://{HOST}:{PORT}")

while True:
    client_socket, client_address = server_socket.accept()
    print(f"Connection from {client_address}")

    # Receive request
    request = client_socket.recv(1024).decode()
    print(f"Request:\n{request}")

    # Prepare HTTP response
    http_response = """
HTTP/1.1 200 OK
Content-Type: text/html

<html>
    <body>
        <h1>Hello, World!</h1>
    </body>
</html>
"""
    client_socket.sendall(http_response.encode())
    client_socket.close()

3. Breaking Down the Code

Creating a TCP Socket

1
2
3
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((HOST, PORT))
server_socket.listen(5)
  • socket.AF_INET: Specifies IPv4 addressing.
  • socket.SOCK_STREAM: Specifies TCP (reliable, connection-based communication).
  • bind((HOST, PORT)): Assigns the server to a specific IP and port.
  • listen(5): Allows up to 5 queued client connections.

Accepting and Processing Client Requests

1
2
3
client_socket, client_address = server_socket.accept()
request = client_socket.recv(1024).decode()
print(f"Request:\n{request}")
  • accept(): Waits for a client to connect.
  • recv(1024).decode(): Receives and decodes the HTTP request.

Sending an HTTP Response

1
2
3
4
5
6
7
8
9
10
11
12
http_response = """
HTTP/1.1 200 OK
Content-Type: text/html

<html>
    <body>
        <h1>Hello, World!</h1>
    </body>
</html>
"""
client_socket.sendall(http_response.encode())
client_socket.close()
  • Constructs a simple HTTP response with a 200 OK status.
  • Specifies Content-Type: text/html to tell the browser it’s an HTML response.

4. Testing the Server

Steps to Test

  1. Run the script:
    1
    
    python server.py
    
  2. Open your browser and visit http://127.0.0.1:8080.
  3. You should see “Hello, World!” displayed on the page.
  4. The terminal will log the incoming HTTP request.

5. Handling Multiple Requests Using Threads

Right now, our server handles one request at a time. To serve multiple clients, we can use threading.

Multi-Threaded HTTP Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import threading

def handle_client(client_socket):
    request = client_socket.recv(1024).decode()
    print(f"Request:\n{request}")

    response = "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<html><body><h1>Multi-threaded Server</h1></body></html>"
    client_socket.sendall(response.encode())
    client_socket.close()

while True:
    client_socket, _ = server_socket.accept()
    client_thread = threading.Thread(target=handle_client, args=(client_socket,))
    client_thread.start()

Why Use Threads?

  • Each client gets handled in a separate thread.
  • The server remains responsive, even with multiple simultaneous requests.

6. Expanding the Server

Here are a few ways to enhance this simple HTTP server:

1. Serve Static Files (HTML, CSS, JS)

Instead of hardcoding the response, read files from a directory:

1
2
3
4
5
6
7
def serve_static_file(filename):
    try:
        with open(filename, 'r') as file:
            content = file.read()
        return f"HTTP/1.1 200 OK\nContent-Type: text/html\n\n{content}"
    except FileNotFoundError:
        return "HTTP/1.1 404 Not Found\nContent-Type: text/html\n\n<h1>404 Not Found</h1>"

2. Handle Different Routes (/about, /contact)

1
2
3
4
5
def handle_request(request):
    if "GET /about" in request:
        return "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<h1>About Page</h1>"
    else:
        return "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<h1>Home Page</h1>"

3. Support POST Requests

1
2
3
if "POST" in request:
    body = request.split("\r\n\r\n")[1]  # Extract form data
    print("Received Data:", body)

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import socket
import os
import mimetypes

# Server Configuration
HOST = '127.0.0.1'  # Localhost
PORT = 8000  # Port to listen on
STATIC_FILES_DIRECTORY = "static"  # Directory for static files

# Function to serve static files
def serve_static_file(filename):
    try:
        # Get the MIME type based on the file extension
        mime_type, _ = mimetypes.guess_type(filename)
        if mime_type is None:
            mime_type = 'application/octet-stream'  # Default for unknown file types

        # Open and read the file in binary mode
        with open(filename, 'rb') as file:
            content = file.read()

        # Build the response header and content
        response = f"HTTP/1.1 200 OK\nContent-Type: {mime_type}\n\n".encode()
        return response + content
    except FileNotFoundError:
        # Return 404 if the file is not found
        return b"HTTP/1.1 404 Not Found\nContent-Type: text/html\n\n<h1>404 Not Found</h1>"

# Function to handle GET and POST requests
def handle_request(request):
    lines = request.splitlines()
    if len(lines) > 0:
        method, path, _ = lines[0].split()

        # Handling static files like HTML, CSS, JS
        if method == "GET":
            if path == "/":
                path = "/index.html"  # Serve index page by default
            file_path = STATIC_FILES_DIRECTORY + path
            return serve_static_file(file_path)

        # Handle different routes
        elif path == "/about":
            return "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<h1>About Page</h1>"
        elif path == "/contact":
            return "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<h1>Contact Page</h1>"

        # Handle POST requests (e.g., form submission)
        elif method == "POST":
            if path == "/submit":
                body = request.split("\r\n\r\n")[1]  # Extract POST body
                print("Received POST Data:", body)
                return "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<h1>Form Submitted</h1>"

    # If no matching route, return home page as fallback
    return "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<h1>Home Page</h1>"

# Function to parse and handle requests
def parse_request(client_socket):
    # Receive client request
    request = client_socket.recv(1024).decode()
    print(f"Request: {request}")

    # Handle the request based on GET or POST methods
    response = handle_request(request)

    # Send the response to the client
    client_socket.sendall(response.encode() if isinstance(response, str) else response)

    # Close the client connection
    client_socket.close()

# Main function to run the server
def run_server():
    # Create and bind the socket
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
        server_socket.bind((HOST, PORT))
        server_socket.listen(5)
        print(f"Server started at {HOST}:{PORT}, serving static files from '{STATIC_FILES_DIRECTORY}'...")

        # Continuously accept and handle incoming requests
        while True:
            # Accept a new connection
            client_socket, client_address = server_socket.accept()
            print(f"Connection from {client_address}")

            # Handle the client request
            parse_request(client_socket)

if __name__ == "__main__":
    run_server()

7. Conclusion

This tutorial covered how to build a simple HTTP server using Python’s socket module. While it’s not as powerful as Django or Flask, it provides a great low-level understanding of how web servers work.

Key Takeaways

✔ Learned how HTTP requests and responses work.
✔ Built a basic server to handle GET requests.
✔ Enhanced it with threading for handling multiple clients.
✔ Explored ways to expand the server with static files, routing, and POST requests.


8. What’s Next?

Now that you’ve built a basic server, consider:

Have any questions? Feel free to comment or experiment with the code!

This post is licensed under CC BY 4.0 by the author.