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.

SYNSYN-ACKACKClientWeb server
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.

connect()request and responseconnection endsSocketcreatedConnectionestablishedHTTP exchangesend and receiveSocketclosed
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.

spacespaceends request lineblank lineencode()GETmethod/resource pathHTTP versionprotocol version\r\nline ending\r\nblank lineRequest bytessent through socket
What lines and end-of-line sequences make up a valid HTTP GET request, and how are they sent to the server?

request = "GET / HTTP/1.1\r\n\r\n" request_bytes = request.encode() sock.send(request_bytes)

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.

readchunknon-emptynext chunkemptyConnected socketrecv(512)up to 512 bytesData returnedempty or non-emptyDecoded textprint without extra newlineEnd of responseempty return
How does data move from the server in multiple chunks, and how does the client know when to stop reading?

while True: chunk = sock.recv(512) if not chunk: break text = chunk.decode() print(text, end="")

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

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.

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.

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

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

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

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

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.
create and controlconnectHTTP requestresponse streamrecv(512), decode()Python clientSocketconnected channelPort 80HTTP endpointWeb serverResponse chunksdecoded and printed
How does control and data move from the client to port 80 and back through the socket?

Mistakes That Break the Exchange

  • Sending the request as an ordinary string

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

    Fix: Use sock.send(request.encode()).

  • Ending the request with only one line ending

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

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

  • Assuming one recv() call gets the complete response

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

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

  • Printing received bytes without decoding

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

    Fix: Decode each non-empty chunk before printing it.

  • Adding a newline after every chunk

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

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