Concepts / Building a Web Server with Sockets

Building a Web Server with Sockets

A socket connection to port 80 establishes a reliable communication channel with a web server through a three-way handshake.

  • Programming

From Program to Web Server

A web browser can be understood as a program that opens a socket, connects to a web server, sends an HTTP request, receives the response in pieces, and displays the result. This lesson follows that process directly. The socket provides the communication channel, while HTTP defines the text-based request format sent through it.

ClientWeb serverSYNSYN-ACKACK
What messages are exchanged, and in what order, before the client and web server can communicate?

Connection Before Communication

Creating a socket gives the program an object through which it can communicate. Calling connect() with a server and port begins the connection process. For a connection to port 80, the computer initiates a TCP three-way handshake: it sends a SYN packet, the server responds with SYN-ACK, and the computer sends ACK. Once this exchange completes, a reliable communication channel exists between the program's socket and the server's listening socket.

SocketcreatedConnectionestablishedHTTP exchangesend and receiveSocketclosedconnect()request and responseconnection ends
What state is the socket in at each stage, and what changes when the connection is closed?
python

The code uses a placeholder server name so that the connection target is visible without assuming a particular real server. The important sequence is that the socket is created first and connect() is called with the remote server and port 80 second.

Anatomy of the GET Request

After the connection exists, the client sends an HTTP GET command. The request starts with GET, includes a resource path and an HTTP version, and ends with two consecutive line endings. In HTTP, the line-ending sequence is carriage return followed by line feed, written as \r\n. Therefore, the request terminator is \r\n\r\n.

GETmethod/resource pathHTTP versionprotocol version\r\nline ending\r\nblank lineRequest bytessent through socketspacespaceends request lineblank lineencode()
What lines and end-of-line sequences make up a valid HTTP GET request, and how are they sent to the server?
Code walkthrough
request = "GET / HTTP/1.1\r\n\r\n"
request_bytes = request.encode()
sock.send(request_bytes)
  1. 1
    The string contains the request line followed by the required blank line.
  2. 2
    The request is encoded because the socket sends bytes rather than the original text string.
  3. 3
    The encoded request is sent after the socket has connected to the server.
  • The request line contains the GET command, resource path, and HTTP version.
  • The two consecutive \r\n sequences create the blank line that ends the headers.
  • encode() converts the text request into bytes.
  • send() transmits those bytes through the connected socket.

Reading the Response Stream

The server response may arrive in multiple pieces rather than as one complete value. The client repeatedly calls recv(512), which retrieves up to 512 bytes at a time. Each returned chunk is decoded into a string before it is printed. The loop continues while data is returned. When the server has finished sending and closes its end of the connection, recv() produces an empty return, which tells the client to stop reading.

Connected socketrecv(512)up to 512 bytesData returnedempty or non-emptyDecoded textprint without extra newlineEnd of responseempty returnreadchunknon-emptynext chunkempty
How does data move from the server in multiple chunks, and how does the client know when to stop reading?
Code walkthrough
while True:
    chunk = sock.recv(512)
    if not chunk:
        break
    text = chunk.decode()
    print(text, end="")
  1. 1
    The loop has no fixed number of repetitions because the response length is not known in advance.
  2. 2
    recv(512) reads at most 512 bytes for the current iteration.
  3. 3
    The test stops the loop when no data is returned.
  4. 4
    decode() converts the raw byte chunk into a string.
  5. 5
    Printing with an empty end value keeps adjacent chunks continuous.
  • The loop asks the socket for up to 512 bytes.
  • An empty return means the response has ended.
  • A non-empty byte chunk is decoded before printing.
  • end="" prevents print() from adding another newline after each chunk.

The loop does not assume that one receive operation contains a complete HTTP response. It treats each receive as one chunk, processes that chunk, and then asks for another. The empty return is the stopping signal, so the client can finish even when the response requires many iterations.

A Complete Minimal Client

Worked example

Trace a complete HTTP exchange

Construct a minimal Python client that connects to port 80, sends a GET request for the root resource, prints the response, and stops when the server closes its end of the connection.

  1. 1
    Create and connect

    Create a socket object and connect it to a remote server on port 80. The connection call initiates the three-way handshake.

  2. 2
    Format the request

    Build a request beginning with GET, include the root path and HTTP version, and finish with \r\n\r\n.

  3. 3
    Convert and send

    Encode the request string into bytes and send those bytes through the connected socket.

  4. 4
    Receive repeatedly

    Call recv(512) in a loop. Decode each non-empty chunk before printing it.

  5. 5
    Stop and close

    Break when recv() returns an empty value, then close the socket to finish its lifecycle.

Result

The client follows the complete sequence: socket creation, connection, byte-encoded HTTP request, chunked response processing, end-of-response detection, and closure.

python
Output
The program prints the response text continuously as chunks are decoded. The exact response content depends on the server contacted; the important behavior is that printing continues until recv() returns an empty value.
Python clientSocketconnected channelPort 80HTTP endpointWeb serverResponse chunksdecoded and printedcreate and controlconnectHTTP requestresponse streamrecv(512), decode()
How does control and data move from the client to port 80 and back through the socket?

Mistakes That Break the Exchange

  • Common mistake

    Sending the request as an ordinary string

    What it looks like
    sock.send(request)
    Why it fails

    The socket request must be encoded as bytes before it is sent.

    Do this instead
    Use sock.send(request.encode()).
  • Common mistake

    Ending the request with only one line ending

    What it looks like
    request = "GET / HTTP/1.1\r\n"
    Why it fails

    The HTTP request needs \r\n\r\n so the blank line signals the end of the headers.

    Do this instead

    End the request with two consecutive \r\n sequences.

  • Common mistake

    Assuming one recv() call gets the complete response

    What it looks like
    response = sock.recv(512)
    Why it fails

    The response arrives as a stream and recv(512) retrieves up to 512 bytes at a time.

    Do this instead

    Use a loop and continue receiving until the return is empty.

  • Common mistake

    Printing received bytes without decoding

    What it looks like
    print(chunk)
    Why it fails

    Received chunks are raw bytes and must be decoded into a string before printing.

    Do this instead

    Decode each non-empty chunk before printing it.

  • Common mistake

    Adding a newline after every chunk

    What it looks like
    print(chunk.decode())
    Why it fails

    Each print call would add its own newline, potentially separating chunks that belong together.

    Do this instead

    Use print(chunk.decode(), end="") to keep the chunks continuous.

Practice the Lifecycle

MEDIUM

Write the missing lines for a client that has already created sock and connected it to a server on port 80. The client must send a GET request for the root resource, receive up to 512 bytes per iteration, decode each non-empty chunk, print continuously, and stop when the response ends.

Hints
  • The request must end with \r\n\r\n before it is encoded.
  • Use a loop because the response can arrive in multiple chunks.
  • An empty return from recv() is the stopping condition.
python

What do you think happens?

What should happen when recv(512) returns an empty value?

  • Decode and print it

  • Call recv(512) again immediately

  • Break out of the receive loop

Reveal answer
Answer:

Break out of the receive loop

An empty return signals that the server has finished sending and closed its end of the connection, so there is no more response data to process.

Complete Exchange Checklist

  1. 1
    Create a socket object.
  2. 2
    Connect it to the remote server on port 80, allowing the three-way handshake to establish the channel.
  3. 3
    Build a GET request with a resource path and HTTP version.
  4. 4
    End the request with \r\n\r\n.
  5. 5
    Encode the request string into bytes and send it.
  6. 6
    Call recv(512) repeatedly because the response arrives in chunks.
  7. 7
    Stop when recv() returns an empty value.
  8. 8
    Decode each non-empty chunk before printing it.
  9. 9
    Close the socket after the exchange ends.

The central idea is to keep the socket lifecycle and HTTP message format distinct but connected. TCP provides the reliable channel after the handshake. HTTP supplies the GET request and its required line endings. The receive loop handles the response stream, while decoding turns each byte chunk into readable text.

Key Takeaways

  • A connection to port 80 begins with a TCP three-way handshake involving SYN, SYN-ACK, and ACK.
  • An HTTP GET request must be encoded as bytes and end with \r\n\r\n to mark the end of its headers.
  • A response can arrive in many chunks, so the client repeatedly calls recv(512).
  • Each non-empty byte chunk must be decoded before printing, and an empty return signals that reading should stop.
  • Closing the socket completes the lifecycle after the request and response exchange.

Related concepts

More concepts starting with B