Of course! The combination of TCP, struct, and Python is a very common and powerful pattern for network programming. It's used to send data that isn't just plain text, like numbers, binary data, or complex data structures.

Let's break it down into three parts and then combine them into a practical example.
The struct Module: Packing and Unpacking Binary Data
Python's built-in struct module converts between Python values and C-style data types represented as bytes objects. This is essential for creating a predictable binary format.
Key Functions:
struct.pack(format, ...): Takes a format string and Python values, and returns abytesobject.struct.unpack(format, data): Takes a format string and abytesobject, and returns a tuple of unpacked values.struct.calcsize(format): Calculates the size (in bytes) of the data structure described by the format string.
Format Characters (Common Examples):
| Character | C Type | Python Type | Size (bytes) |
| :-------- | :------------- | :---------- | :----------- |
| x | pad byte | No value | 1 |
| c | char | bytes (len 1)| 1 |
| b | signed char | int | 1 |
| B | unsigned char | int | 1 |
| h | short | int | 2 |
| H | unsigned short | int | 2 |
| i | int | int | 4 |
| I | unsigned int | int | 4 |
| f | float | float | 4 |
| d | double | float | 8 |
| s | char[] | bytes | |
| q | long long | int | 8 |

Important: The format string can be prefixed with a character to specify byte order:
<: little-endian (most common on x86/x64)>: big-endian (network byte order, often used for TCP/IP)- network byte order (same as
>) - native byte order, size, and alignment
- native byte order, standard size and alignment
TCP Sockets in Python
Python's socket module provides access to the BSD socket interface. The standard flow is:
Server:
socket.socket(): Create a socket object.socket.bind(): Assign an address (IP, port) to the socket.socket.listen(): Enable the server to accept connections.socket.accept(): Block and wait for an incoming connection. Returns a new socket for the connection and the client's address.socket.recv(): Receive data from the client.socket.sendall(): Send data to the client.socket.close(): Close the connection.
Client:
socket.socket(): Create a socket object.socket.connect(): Connect to the server's address.socket.sendall(): Send data to the server.socket.recv(): Receive data from the server.socket.close(): Close the connection.
The Challenge: Sending Structured Data
If you try to send a Python int directly, you'll get an error: TypeError: a bytes-like object is required, not 'int'. You must convert it to bytes.
The naive approach is to just pack the data and send it:
# This is problematic!
data = struct.pack('>i', 42) # 4 bytes
socket.sendall(data)
The problem is: How does the receiver know how many bytes to expect? The receiver's recv(1024) might get only 2 of those 4 bytes, or it might get your 4 bytes plus the first 4 bytes of the next message.
The Solution: A Message Header
The most robust solution is to prefix your message with a header that describes the length of the data to follow.
A common and simple structure is:
[Header (4 bytes, message length)][Payload (N bytes)]
Let's create a simple protocol to send a single integer.
Complete Example: Client/Server for a Single Integer
Here is a full, working example where a client sends a single integer to a server, which then sends back the square of that integer.
The Server (server.py)
import socket
import struct
# Use a port above 1024 to avoid needing root/admin privileges
HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
# Create a TCP socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
print(f"Server listening on {HOST}:{PORT}")
# accept() waits for a new connection and returns a new socket object 'conn'
# and the address of the client 'addr'
conn, addr = s.accept()
with conn:
print(f"Connected by {addr}")
while True:
# First, receive the header (4 bytes) to get the payload size
header = conn.recv(4)
if not header:
# If recv returns an empty object, the client has closed the connection
break
# Unpack the 4-byte unsigned integer from the header
# The '!' prefix ensures network byte order (big-endian)
payload_size = struct.unpack('!I', header)[0]
print(f"Received header. Payload size: {payload_size} bytes.")
# Now, receive the actual payload
payload_data = b''
# We need to keep receiving until we have the full payload
while len(payload_data) < payload_size:
# recv() returns up to 4096 bytes of data
chunk = conn.recv(4096)
if not chunk:
# Connection broken before full payload received
raise ConnectionError("Connection broken while receiving payload")
payload_data += chunk
print(f"Received full payload: {payload_data}")
# Unpack the integer from the payload
# We expect one 4-byte integer
number = struct.unpack('!i', payload_data)[0]
print(f"Unpacked number: {number}")
# Calculate the result
result = number * number
# Pack the result into a new payload
response_payload = struct.pack('!i', result)
# Create a header for the response
response_header = struct.pack('!I', len(response_payload))
# Send the header and the payload
conn.sendall(response_header + response_payload)
print(f"Sent response: {result}")
The Client (client.py)
import socket
import struct
import time
HOST = '127.0.0.1' # The server's hostname or IP address
PORT = 65432 # The port used by the server
# Create a TCP socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
print(f"Connected to server at {HOST}:{PORT}")
# The number we want to send
number_to_send = 12345
# 1. Pack the number into a payload
payload = struct.pack('!i', number_to_send)
# 2. Create a header that describes the payload's length
# 'I' is an unsigned int, 4 bytes, which is perfect for lengths.
header = struct.pack('!I', len(payload))
# 3. Send the header first, then the payload
print(f"Sending header: {len(payload)} bytes")
s.sendall(header)
print(f"Sending payload: {number_to_send}")
s.sendall(payload)
# 4. Receive the response from the server
# First, get the header
response_header = s.recv(4)
if not response_header:
raise ConnectionError("Server closed connection unexpectedly")
# Unpack the header to get the response payload size
response_payload_size = struct.unpack('!I', response_header)[0]
print(f"Received response header. Payload size: {response_payload_size} bytes.")
# Then, get the response payload
response_payload = b''
while len(response_payload) < response_payload_size:
chunk = s.recv(4096)
if not chunk:
raise ConnectionError("Connection broken while receiving response")
response_payload += chunk
# Unpack the result
result = struct.unpack('!i', response_payload)[0]
print(f"Received result from server: {result}")
How to Run It
- Save the two code blocks above as
server.pyandclient.py. - Open two terminal windows.
- In the first terminal, run the server:
python server.py
You will see:
Server listening on 127.0.0.1:65432 - In the second terminal, run the client:
python client.py
Expected Output:
Server Terminal:
Server listening on 127.0.0.1:65432
Connected by ('127.0.0.1', 54321)
Received header. Payload size: 4 bytes.
Received full payload: b'{'
Unpacked number: 12345
Sent response: 152399025
Client Terminal:
Connected to server at 127.0.0.1:65432
Sending header: 4 bytes
Sending payload: 12345
Received response header. Payload size: 4 bytes.
Received result from server: 152399025
Advanced: Sending More Complex Data (e.g., a String and an Integer)
You can extend this pattern. To send a string and an integer, you would:
- Define a protocol: For example,
[Header (4 bytes)][String Length (4 bytes)][String (N bytes)][Integer (4 bytes)]. - On the sender side:
- Encode the string to bytes (e.g.,
my_string.encode('utf-8')). - Pack the string length, the string bytes, and the integer into a single payload.
- Create a header for the total payload length.
- Send
header + payload.
- Encode the string to bytes (e.g.,
- On the receiver side:
- Receive the header to know the total length.
- Receive the full payload.
- Unpack the string length, then use that to slice out the string bytes.
- Unpack the integer from the remaining bytes.
