Client-Server and Socket Programming

You can read this note directly if you know Python functions, strings, loops, and basic file-running skills. Socket programming lets two running programs exchange data across a network.

The key idea is simple:

program -> bytes -> socket -> network -> socket -> bytes -> program

However, beginners often make one important mistake. They imagine that two Python programs share variables once they are connected. They do not. Each program has its own memory. They communicate only by sending and receiving bytes.

Beginner Problem

Suppose a client program wants to send the text message hello to a server program.

The message must go through several transformations:

client string -> encode -> bytes -> socket -> bytes -> decode -> server string

So both programs must agree on several things:

QuestionExample answer
Which computer should the client contact?127.0.0.1
Which service on that computer should it contact?port 50007
What kind of socket is used?TCP socket
How is text converted to bytes?UTF-8 encoding
How does the receiver know a message is complete?for example, message ends with \n
What does the message mean?application-level protocol rules

This note explains these ideas step by step.

Client and Server

In a client-server architecture:

  • the server waits for requests;
  • the client initiates contact;
  • the server processes a request and sends a response.

Caption: Client and server processes communicate using sockets.

Example:

ApplicationClientServer
web browsingbrowserweb server
emailemail appmail server
online gamegame clientgame server
simple socket programPython client programPython server program

The words client and server describe roles in a communication session.

A client is usually the process that starts the communication. A server is usually the process that waits to be contacted. In simple H2 Computing socket programs, the server is started first and waits at accept(), while the client connects later using connect().

Process, Host, IP Address, and Port

A network message must reach the correct process.

There are two levels to think about:

LevelIdentifierMeaning
host/deviceIP addresswhich computer to contact
process/serviceport numberwhich program or service on that computer to contact

A socket endpoint is identified by an IP address and port number.

127.0.0.1:50007

127.0.0.1 is the loopback address. It means this same computer. This is useful when testing a server and client on one machine.

50007 is the port used by the example service.

A useful beginner analogy is:

Network ideaDelivery analogy
IP addressbuilding address
port numberroom or counter inside the building
socketcommunication doorway used by a program

The analogy is not perfect, but it helps separate the roles.

Why Ports Are Needed

One computer may run many network programs at the same time.

For example, a computer may run:

  • a web server;
  • an email service;
  • a game server;
  • a custom Python socket server.

The IP address tells the network which host to reach. The port number tells the host which process or service should receive the data.

Common examples:

ServiceCommon port
HTTP web server80
HTTPS web server443
SMTP mail sending25
custom classroom socket programoften a large unused port such as 50007

For classroom examples, choose the same port number in the server and client. If the server binds to port 50007 but the client connects to port 50008, the client is trying to contact the wrong service.

Sockets as the Program-Network Interface

A socket is a software interface that lets a program send data to, and receive data from, the network.

For H2 Computing, the most common socket model is a TCP socket:

socket.socket(socket.AF_INET, socket.SOCK_STREAM)

This means:

PartMeaning
socket.AF_INETuse IPv4 addresses
socket.SOCK_STREAMuse TCP stream sockets

A TCP socket gives the program a reliable ordered byte stream. This means TCP helps deliver bytes in order, but the application still needs to decide where each complete message begins and ends.

This point is important. TCP does not automatically understand your message structure. It only carries bytes.

Bytes and Encoding

Sockets send bytes, not Python strings directly.

message = "HELLO"
data = message.encode("utf-8")
text = data.decode("utf-8")

Encoding turns a string into bytes. Decoding turns bytes back into a string.

The following are different Python objects:

"HELLO"      # str
b"HELLO"     # bytes

A common beginner error is to write:

connection.sendall("HELLO")      # wrong: this is a string

A socket needs bytes:

connection.sendall("HELLO".encode("utf-8"))

or:

connection.sendall(b"HELLO")

Use .encode() when you start from a string. Use .decode() when you receive bytes and want to process them as text.

Server Socket and Connection Socket

A beginner should distinguish two server-side sockets.

SocketRole
listening socketwaits for new clients
connection socketcommunicates with one accepted client

In the code below, server is the listening socket. connection is the new socket returned by accept() for one client.

connection, address = server.accept()

The server uses connection.recv(...) and connection.sendall(...) to communicate with that client.

This distinction helps explain why a server can keep listening for future clients after accepting one connection.

Iterative Server

An iterative server handles one client connection at a time. It accepts a connection, serves it, closes it, then goes back to wait for another connection.

It is easier to write than a concurrent server, but it cannot truly serve many clients at the same time.

Run order for the examples:

  1. start the server program;
  2. leave it running;
  3. run the client program in another terminal or process;
  4. read the response printed by the client;
  5. run the client a second time, because this example server accepts two clients.

Small echo-style server:

import socket
 
HOST = "127.0.0.1"
PORT = 50007
 
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen()
 
for _ in range(2):
    connection, address = server.accept()
    data = connection.recv(1024)
    message = data.decode("utf-8")
    reply = "ACK: " + message
    connection.sendall(reply.encode("utf-8"))
    connection.close()
 
server.close()

Important calls:

CallMeaning
socket.socket(...)create a TCP socket
bind((HOST, PORT))attach server socket to address and port
listen()allow the socket to wait for incoming connections
accept()accept one client connection; blocks while waiting
recv(1024)receive up to 1024 bytes
sendall(...)send bytes through the socket
close()close the socket

Server trace for one client:

StepServer action
1accept() waits until a client connects
2recv(1024) receives bytes from that client
3decode("utf-8") turns bytes into text
4server builds a reply string
5encode("utf-8") turns reply text into bytes
6sendall(...) sends the reply
7connection closes
8the loop returns to accept() for the next client

Matching Client

import socket
 
HOST = "127.0.0.1"
PORT = 50007
 
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))
client.sendall("hello".encode("utf-8"))
data = client.recv(1024)
client.close()
 
print(data.decode("utf-8"))

Expected output:

ACK: hello

Client trace:

StepClient action
1creates a socket
2connects to server IP and port
3encodes and sends "hello"
4receives reply bytes
5closes the socket
6decodes bytes and prints the reply

The client and server must use the same IP address and port number.

In this example:

Server lineMatching client line
HOST = "127.0.0.1"HOST = "127.0.0.1"
PORT = 50007PORT = 50007
server.bind((HOST, PORT))client.connect((HOST, PORT))

Blocking Calls

Some socket calls may block. A blocked program is waiting for something to happen.

CallWhy it may block
accept()waits until a client connects
recv(1024)waits until some bytes arrive or the connection closes

This is why a server may look stuck after it is started. It is often not broken. It is waiting at accept().

What recv(1024) Really Means

recv(1024) means receive up to 1024 bytes.

It does not mean:

  • receive exactly 1024 bytes;
  • receive one complete message;
  • receive everything the other side sent.

This is one of the most important ideas in socket programming.

For example, suppose the server sends:

connection.sendall(b"Hello from server\n")

The client may receive all the bytes in one call:

b"Hello from server\n"

But for longer messages, or in real network conditions, a program may receive only part of a message first. The remaining bytes may arrive later.

Therefore, a socket program should not assume that one recv() call always gives one complete application message.

Designing a Simple Protocol

A protocol is an agreed set of rules for communication.

A socket only moves bytes. The application protocol gives meaning to those bytes.

A protocol should answer questions such as:

  • What encoding is used?
  • How does the receiver know a message is complete?
  • What commands are allowed?
  • What reply should be sent for each command?
  • What happens if the input is invalid?

A very simple protocol may be:

Client sends one line of UTF-8 text ending with \n.
Server replies with ACK: followed by the same text, also ending with \n.

Example communication:

client sends: hello\n
server sends: ACK: hello\n

The newline character \n is used as a delimiter. It marks the end of one message.

Caption: A simple line-based protocol uses \n to mark the end of each message.

Receiving Until the End of a Message

If the protocol says that each message ends with \n, the receiver can keep calling recv() until it sees b"\n".

data = b""
while b"\n" not in data:
    chunk = connection.recv(1024)
    if chunk == b"":
        break
    data += chunk
 
message = data.decode("utf-8")

This is safer than assuming one recv(1024) call receives a complete message.

If recv() returns b"", the other side has closed the connection. The loop should stop instead of waiting forever.

For beginner H2 examples, this pattern is often enough:

create empty bytes variable
repeat receiving bytes
append new bytes to previous bytes
stop when newline is found
stop if the connection closes
decode the complete bytes into text

Improved Line-Based Echo Server

The previous echo server receives once. The following version follows a simple line-based protocol.

import socket
 
HOST = "127.0.0.1"
PORT = 50007
 
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen()
 
for _ in range(2):
    connection, address = server.accept()
 
    data = b""
    while b"\n" not in data:
        chunk = connection.recv(1024)
        if chunk == b"":
            break
        data += chunk
 
    message = data.decode("utf-8").strip()
    reply = "ACK: " + message + "\n"
    connection.sendall(reply.encode("utf-8"))
    connection.close()
 
server.close()

Matching client:

import socket
 
HOST = "127.0.0.1"
PORT = 50007
 
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))
 
client.sendall("hello\n".encode("utf-8"))
 
data = b""
while b"\n" not in data:
    chunk = client.recv(1024)
    if chunk == b"":
        break
    data += chunk
 
client.close()
print(data.decode("utf-8").strip())

Expected output:

ACK: hello

The main improvement is not the reply itself. The main improvement is that both sides now know how to detect the end of a message.

Mini Protocol Example: PING and PONG

Many exam-style socket questions give either the server code or the client code and ask students to write the matching program.

Consider this protocol:

Client sends PING\n.
Server replies PONG\n.
Any other message receives ERROR\n.

Server:

import socket
 
HOST = "127.0.0.1"
PORT = 50007
 
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen()
 
connection, address = server.accept()
 
data = b""
while b"\n" not in data:
    chunk = connection.recv(1024)
    if chunk == b"":
        break
    data += chunk
 
message = data.decode("utf-8").strip()
 
if message == "PING":
    connection.sendall(b"PONG\n")
else:
    connection.sendall(b"ERROR\n")
 
connection.close()
server.close()

Matching client:

import socket
 
HOST = "127.0.0.1"
PORT = 50007
 
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))
client.sendall(b"PING\n")
 
data = b""
while b"\n" not in data:
    chunk = client.recv(1024)
    if chunk == b"":
        break
    data += chunk
 
client.close()
print(data.decode("utf-8").strip())

Expected output:

PONG

When writing the matching client, look for:

Server behaviourClient must do
server binds to 127.0.0.1, 50007connect to 127.0.0.1, 50007
server waits for a line ending with \nsend a line ending with \n
server expects PINGsend PING
server replies with bytesreceive bytes and decode if needed

How to Read Socket Exam Questions

When given server code and asked to write the client, ask:

  1. What IP address and port should the client connect to?
  2. Does the server receive first or send first?
  3. What exact bytes or text does the server expect?
  4. Does the message need a delimiter such as \n?
  5. What reply should the client expect?
  6. Should the client decode the reply before printing?

When given client code and asked to write the server, ask:

  1. What IP address and port does the client connect to?
  2. Does the client send first or receive first?
  3. What exact message does the client send?
  4. What response does the client expect?
  5. Should the server loop to serve more than one client?
  6. When should sockets be closed?

This is usually more important than memorising code line by line.

Iterative Server with Continuous Service

The earlier server accepts only two clients. A more realistic iterative server may run continuously.

import socket
 
HOST = "127.0.0.1"
PORT = 50007
 
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen()
 
while True:
    connection, address = server.accept()
 
    data = b""
    while b"\n" not in data:
        chunk = connection.recv(1024)
        if chunk == b"":
            break
        data += chunk
 
    message = data.decode("utf-8").strip()
    reply = "ACK: " + message + "\n"
    connection.sendall(reply.encode("utf-8"))
    connection.close()

This server is still iterative. It can serve many clients over time, but only one at a time.

If one client takes a long time, the server cannot handle another client until the current one is finished.

A concurrent server can handle multiple clients at the same time, for example by using threads. Concurrent servers are useful in real systems, but they are outside the core beginner socket programming requirement here.

Turn-Based Communication Pattern

Some H2 questions use a simple turn-based game, such as tic-tac-toe.

A turn-based socket protocol often follows this pattern:

server sends move
client receives move
client sends move
server receives move
repeat

The exact commands must be agreed.

Example protocol:

MOVE5\n means player chooses cell 5.
END\n means player quits or the game ends.

If the server sends first, the client must receive first. If the client sends first, the server must receive first.

This is a common source of errors. If both programs call recv() first, both sides wait forever. If both programs call sendall() first, the logic may not match the intended turn order.

Common Mistakes

MistakeWhy it is wrong
Forgetting to run the server before the clientthe client has nothing to connect to
Using different port numbers in client and serverthe client contacts the wrong service
Sending a string instead of bytessockets send bytes, not Python strings
Forgetting to decode received bytesreceived data may be bytes, not str
Thinking recv(1024) receives the whole messageit receives up to 1024 bytes
Forgetting a message delimiterreceiver may not know when a message ends
Expecting an iterative server to handle many clients at the same timeit handles one client at a time
Using accept() in the clientaccept() is for the server side
Using connect() in the server listening socketconnect() is for the client side
Forgetting to close sockets after useresources may remain open

Exam-Answer Wording

Useful wording for short-answer questions:

Question typeGood answer
What is client-server architecture?A model where a client initiates communication by sending requests to a server, and the server waits for requests, processes them, and returns responses.
What does a port number identify?A process or service on a host.
Why are .encode() and .decode() used?Sockets transmit bytes, while Python text is represented as strings. Encoding converts strings to bytes; decoding converts bytes back to strings.
What does accept() do?It waits for an incoming client connection and returns a new socket for communicating with that client.
What does recv(1024) do?It receives up to 1024 bytes from the socket.
What is an iterative server?A server that handles one client at a time, then returns to wait for the next client.
Why is a protocol needed?It defines agreed rules for message format, meaning, and how to detect complete messages.

Check Your Understanding

  1. Which process initiates contact in client-server architecture?
  2. What does a port number identify?
  3. Why does socket code use .encode() and .decode()?
  4. What does an iterative server do after serving one client?
  5. Why is recv(1024) not guaranteed to return a complete message?
  6. What problem does a newline delimiter \n solve?
  7. In a turn-based game, what may happen if both sides call recv() first?

Answers:

  1. The client.
  2. A process or service on a host.
  3. Sockets send bytes, while Python text is stored as strings.
  4. It closes that connection and returns to wait for the next client.
  5. Because recv(1024) receives up to 1024 bytes that are currently available, not necessarily one complete application-level message.
  6. It marks the end of a message so the receiver knows when the complete message has arrived.
  7. Both programs may block while waiting for the other side to send data.