44.1 Introduction to Computer Networks
A computer network is a group of connected devices that exchange information. These devices can include computers, phones, printers, routers, servers, televisions, and smart devices. Networks allow users to open websites, send messages, share files, print documents, stream videos, and use online applications. A small home network may contain only a few devices, while the internet connects millions of networks around the world.
Example: Represent devices in a network
# Store the names of devices connected to a network.
devices = [
"Laptop",
"Phone",
"Printer",
"Router"
]
# Display the total number of devices.
print("Connected devices:", len(devices))
# Display each device.
for device in devices:
print("-", device)
Output:
Connected devices: 4
- Laptop
- Phone
- Printer
- Router
Output Explanation:
The list represents four devices connected to the same network. In a real network, these devices communicate by following agreed rules called protocols.
44.2 Clients and Servers
A client is a program or device that requests a service. A server is a program or device that receives requests and provides a service. A web browser is a client when it requests a webpage from a web server. A mobile application may act as a client when it requests account information from an API server. One server can often communicate with many clients.
Example: Simulate a client request
# Create a simple server function.
def server(request):
# Check what the client requested.
if request == "homepage":
return "Homepage content"
if request == "products":
return "Product list"
return "Resource not found"
# The client sends a request to the server.
client_request = "products"
# The server creates a response.
server_response = server(client_request)
# The client displays the response.
print("Client requested:", client_request)
print("Server responded:", server_response)
Output:
Client requested: products
Server responded: Product list
Output Explanation:
The client asks for products, and the server returns the related information. Real clients and servers exchange this information across a network.
44.3 IP Addresses
An IP address identifies a device or network interface. IPv4 addresses contain four numbers separated by periods, such as 192.168.1.10. IPv6 addresses are longer and were introduced because the number of available IPv4 addresses is limited. The address 127.0.0.1 normally refers to the current computer and is called the loopback or localhost address.
Example: Display local address information
# Import Python's networking module.
import socket
# Get the current computer's host name.
host_name = socket.gethostname()
# Try to find an IP address for this host.
host_ip = socket.gethostbyname(host_name)
print("Host name:", host_name)
print("Host IP:", host_ip)
print("Loopback address: 127.0.0.1")
Example Output:
Host name: my-computer
Host IP: 192.168.1.25
Loopback address: 127.0.0.1
Output Explanation:
The host name identifies the computer by name, while the IP address identifies its network interface. The exact values depend on the current computer and network.
44.4 Ports
A port identifies a particular network service running on a device. The IP address identifies the computer, while the port identifies the program or service. Port numbers range from 0 to 65535. For example, web servers commonly use ports 80 and 443. Custom applications should normally use an available non-privileged port, such as 5000, 8000, or 9000.
Example: Store service ports
# Store common services and port numbers.
service_ports = {
"HTTP": 80,
"HTTPS": 443,
"DNS": 53,
"Custom App": 5000
}
# Display each service and its port.
for service, port in service_ports.items():
print(f"{service}: port {port}")
Output:
HTTP: port 80
HTTPS: port 443
DNS: port 53
Custom App: port 5000
Output Explanation:
Different ports allow several network services to run on the same computer without confusing their incoming messages.
44.5 TCP
TCP means Transmission Control Protocol. It creates a connection between two endpoints before information is exchanged. TCP checks that data arrives, places packets in the correct order, and retransmits missing information when necessary. This reliability makes TCP suitable for webpages, email, file transfer, database connections, and other situations where missing data would cause problems.
Example: TCP-style ordered delivery
# Simulate pieces of a TCP message.
packets = {
3: "world!",
1: "Hello",
2: "network"
}
# Reassemble the pieces in the correct order.
message_parts = []
for sequence_number in sorted(packets):
message_parts.append(
packets[sequence_number]
)
message = " ".join(message_parts)
print(message)
Output:
Hello network world!
Output Explanation:
TCP uses sequence information to place received data in the correct order. The example sorts the message pieces before combining them.
44.6 UDP
UDP means User Datagram Protocol. UDP sends individual messages called datagrams without first creating a continuing connection. It does not guarantee delivery, ordering, or retransmission. This makes UDP simpler and faster for applications that can tolerate occasional loss, such as live audio, online games, network discovery, or real-time sensor information.
Example: UDP-style independent messages
# Simulate independent UDP messages.
datagrams = [
"Position: 10,20",
"Position: 11,20",
"Position: 12,21"
]
# Each message is handled independently.
for datagram in datagrams:
print("Received:", datagram)
Output:
Received: Position: 10,20
Received: Position: 11,20
Received: Position: 12,21
Output Explanation:
Each datagram is a separate message. UDP does not automatically confirm that every message arrived.
44.7 HTTP
HTTP means Hypertext Transfer Protocol. It defines how web clients and web servers exchange requests and responses. A browser sends an HTTP request for a resource, and the server returns an HTTP response. HTTP can transfer HTML pages, images, stylesheets, scripts, JSON information, downloads, and many other types of content.
Example: Basic HTTP request text
# Create a simplified HTTP request.
http_request = (
"GET /index.html HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n"
"\r\n"
)
print(http_request)
Output:
GET /index.html HTTP/1.1
Host: example.com
Connection: close
Output Explanation:
The first line requests /index.html. The Host header identifies the requested website, and the blank line marks the end of the request headers.
44.8 HTTPS
HTTPS means HTTP Secure. It protects HTTP communication by using TLS encryption. Encryption helps prevent other people on the network from reading or changing the exchanged information. HTTPS also allows the client to verify the server’s certificate. Websites that handle logins, payments, private information, or personal accounts should always use HTTPS.
Example: Check whether a URL uses HTTPS
# Store two example URLs.
urls = [
"http://example.com",
"https://secure.example.com"
]
# Check each address.
for url in urls:
if url.startswith("https://"):
print(url, "- encrypted connection requested")
else:
print(url, "- unencrypted HTTP requested")
Output:
http://example.com - unencrypted HTTP requested
https://secure.example.com - encrypted connection requested
Output Explanation:
The https:// scheme requests an encrypted connection. HTTPS protects data while it travels between client and server.
44.9 DNS
DNS means Domain Name System. It translates readable domain names into IP addresses. People remember names such as example.com more easily than numeric addresses. Before connecting to a website, a computer normally asks a DNS service to find the address associated with the domain name. DNS works like a directory for internet names.
Example: Resolve a domain name
# Import the socket module.
import socket
domain_name = "example.com"
try:
# Resolve the domain into an IP address.
ip_address = socket.gethostbyname(
domain_name
)
print("Domain:", domain_name)
print("IP address:", ip_address)
except socket.gaierror:
print("The domain could not be resolved.")
Example Output:
Domain: example.com
IP address: 93.184.216.34
Output Explanation:
DNS resolution converts the domain name into an IP address. The exact returned address may vary because some services use several servers.
44.10 URLs
A URL is a Uniform Resource Locator. It identifies the location of a resource and describes how to access it. A URL can contain a scheme, host, port, path, query string, and fragment. Understanding these parts helps developers create web requests, routes, links, redirects, and API addresses correctly.
Example: Parse a URL
# Import Python's URL parsing function.
from urllib.parse import urlparse
url = (
"https://example.com:443/"
"products?category=books&page=2"
"#results"
)
# Break the URL into parts.
parts = urlparse(url)
print("Scheme:", parts.scheme)
print("Host:", parts.hostname)
print("Port:", parts.port)
print("Path:", parts.path)
print("Query:", parts.query)
print("Fragment:", parts.fragment)
Output:
Scheme: https
Host: example.com
Port: 443
Path: /products
Query: category=books&page=2
Fragment: results
Output Explanation:
The parser separates the address into its important parts. The query contains options for the server, while the fragment identifies a location within the returned page.
44.11 Requests and Responses
Network communication often follows a request-response pattern. A client sends a request describing the desired action or resource. The server processes the request and returns a response containing a status, headers, and optional content. A successful response might contain HTML or JSON, while a failed response may contain an error message.
Example: Request and response dictionaries
# Create a simplified request.
request = {
"method": "GET",
"path": "/products/1",
"headers": {
"Accept": "application/json"
}
}
# Create a simplified response.
response = {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"id": 1,
"name": "Keyboard",
"price": 49.99
}
}
print("Request method:", request["method"])
print("Request path:", request["path"])
print("Response status:", response["status"])
print("Product:", response["body"]["name"])
Output:
Request method: GET
Request path: /products/1
Response status: 200
Product: Keyboard
Output Explanation:
The client requests one product, and the server returns a successful response containing product information.
44.12 HTTP Methods
HTTP methods describe the action a client wants to perform. GET normally retrieves information. POST normally creates a new resource. PUT replaces a complete resource, while PATCH changes part of a resource. DELETE removes a resource. Applications should use methods consistently so clients and servers clearly understand each request.
Example: Match methods to actions
# Store common HTTP methods.
methods = {
"GET": "Read information",
"POST": "Create information",
"PUT": "Replace information",
"PATCH": "Partially update information",
"DELETE": "Remove information"
}
# Display each method.
for method, action in methods.items():
print(f"{method}: {action}")
Output:
GET: Read information
POST: Create information
PUT: Replace information
PATCH: Partially update information
DELETE: Remove information
Output Explanation:
Each method expresses a different intention. The server uses the method and resource path together to decide what operation to perform.
44.13 HTTP Status Codes
HTTP status codes describe the result of a request. Codes in the 200 range usually indicate success. Codes in the 300 range relate to redirection. Codes in the 400 range describe client-side problems, while codes in the 500 range describe server-side problems. The status code allows the client to respond appropriately.
Example: Interpret common status codes
# Store common HTTP status codes.
status_codes = {
200: "OK",
201: "Created",
204: "No Content",
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
500: "Internal Server Error"
}
# Check selected responses.
responses = [200, 201, 404, 500]
for code in responses:
print(code, status_codes[code])
Output:
200 OK
201 Created
404 Not Found
500 Internal Server Error
Output Explanation:
A 200 response indicates success, 201 indicates creation, 404 means the resource was not found, and 500 indicates a server failure.
44.14 Headers
Headers provide extra information about a request or response. They can describe content type, accepted formats, authentication, caching, language, browser information, and connection behavior. Headers are written as name-value pairs. They should contain metadata rather than the main message body.
Example: Work with HTTP headers
# Create request headers.
request_headers = {
"Accept": "application/json",
"User-Agent": "BeginnerPythonClient/1.0",
"Connection": "close"
}
# Create response headers.
response_headers = {
"Content-Type": "application/json",
"Content-Length": "52",
"Cache-Control": "no-cache"
}
print("Request headers:")
for name, value in request_headers.items():
print(f"{name}: {value}")
print("\nResponse content type:")
print(response_headers["Content-Type"])
Output:
Request headers:
Accept: application/json
User-Agent: BeginnerPythonClient/1.0
Connection: close
Response content type:
application/json
Output Explanation:
The client says it accepts JSON, while the server confirms that the response body contains JSON information.
44.15 Cookies
A cookie is a small value that a server asks a browser to store. The browser may send the cookie back during later requests to the same website. Cookies can remember preferences, identify a session, or store limited state. Sensitive information should not be placed directly in an unprotected cookie.
Example: Parse a simple cookie header
# Store a simplified Cookie header.
cookie_header = (
"theme=dark; "
"language=en; "
"session_id=abc123"
)
# Convert the cookie text into a dictionary.
cookies = {}
for cookie_part in cookie_header.split(";"):
name, value = cookie_part.strip().split(
"=",
1
)
cookies[name] = value
print("Theme:", cookies["theme"])
print("Language:", cookies["language"])
print("Session ID:", cookies["session_id"])
Output:
Theme: dark
Language: en
Session ID: abc123
Output Explanation:
The cookie header contains three name-value pairs. The application separates and stores each value in a dictionary.
44.16 Sessions
A session allows a server to remember a user across several requests. The server normally creates a random session identifier and sends it to the client in a cookie. The browser sends that identifier back later. The server uses it to find session information such as the logged-in user, shopping cart, or preferences.
Example: Simple in-memory sessions
# Import secure random identifier support.
import secrets
# Store sessions in a dictionary for this example.
sessions = {}
# Create a new session identifier.
session_id = secrets.token_hex(16)
# Store information under the session ID.
sessions[session_id] = {
"user_id": 42,
"username": "Michael",
"logged_in": True
}
# Read the session later.
current_session = sessions.get(session_id)
print("Session created:", bool(session_id))
print("Username:", current_session["username"])
print("Logged in:", current_session["logged_in"])
Example Output:
Session created: True
Username: Michael
Logged in: True
Output Explanation:
A random session identifier points to server-side information. Real applications use protected storage, expiration, secure cookies, and careful session management.
44.17 Sockets Introduction
A socket is a software endpoint used for network communication. A socket is associated with an address, a port, and a communication protocol. A server socket normally waits for incoming clients, while a client socket connects to the server. Python sockets can be used for TCP streams or UDP datagrams.
Example: Socket address
# A network endpoint can be represented
# as a tuple containing host and port.
server_address = (
"127.0.0.1",
5000
)
host, port = server_address
print("Host:", host)
print("Port:", port)
print("Endpoint:", f"{host}:{port}")
Output:
Host: 127.0.0.1
Port: 5000
Endpoint: 127.0.0.1:5000
Output Explanation:
The host identifies the computer, and the port identifies the application waiting on that computer.
44.18 Python socket Module
Python’s built-in socket module provides low-level networking tools. The socket.socket() function creates a socket object. The address family determines the address type, while the socket type determines the protocol behavior. AF_INET is commonly used for IPv4, SOCK_STREAM for TCP, and SOCK_DGRAM for UDP.
Example: Create TCP and UDP sockets
# Import the socket module.
import socket
# Create an IPv4 TCP socket.
tcp_socket = socket.socket(
socket.AF_INET,
socket.SOCK_STREAM
)
# Create an IPv4 UDP socket.
udp_socket = socket.socket(
socket.AF_INET,
socket.SOCK_DGRAM
)
print("TCP socket created.")
print("UDP socket created.")
# Close both sockets.
tcp_socket.close()
udp_socket.close()
print("Sockets closed.")
Output:
TCP socket created.
UDP socket created.
Sockets closed.
Output Explanation:
The first socket uses TCP stream communication, while the second uses UDP datagram communication. Closing unused sockets releases operating-system resources.
44.19 Creating TCP Clients
A TCP client creates a stream socket and connects to a server’s host and port. It can then send bytes and receive bytes. Text must be encoded before sending and decoded after receiving. The client should use a timeout so it does not wait forever when the server is unavailable.
Example: TCP client
# tcp_client.py
# Import networking tools.
import socket
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 5000
def main():
# Create a TCP socket.
with socket.socket(
socket.AF_INET,
socket.SOCK_STREAM
) as client_socket:
# Stop waiting after five seconds.
client_socket.settimeout(5)
# Connect to the server.
client_socket.connect(
(
SERVER_HOST,
SERVER_PORT
)
)
print("Connected to the server.")
# Create a text message.
message = "Hello from the client!"
# Convert text to bytes and send it.
client_socket.sendall(
message.encode("utf-8")
)
print("Sent:", message)
# Receive up to 1024 bytes.
response_bytes = client_socket.recv(
1024
)
# Convert bytes back to text.
response = response_bytes.decode(
"utf-8"
)
print("Received:", response)
if __name__ == "__main__":
main()
Example Output:
Connected to the server.
Sent: Hello from the client!
Received: Server received your message.
Output Explanation:
The client connects to a server running on the same computer, sends a text message as bytes, receives a response, and decodes it back into text.
44.20 Creating TCP Servers
A TCP server creates a socket, binds it to a host and port, listens for clients, and accepts incoming connections. The accepted connection receives and sends information for one client. A simple server may handle one client at a time, while larger servers use threads, processes, asynchronous programming, or frameworks to handle many clients.
Example: TCP server
# tcp_server.py
# Import networking tools.
import socket
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 5000
def main():
# Create a TCP server socket.
with socket.socket(
socket.AF_INET,
socket.SOCK_STREAM
) as server_socket:
# Allow quick reuse of the address.
server_socket.setsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEADDR,
1
)
# Attach the socket to a host and port.
server_socket.bind(
(
SERVER_HOST,
SERVER_PORT
)
)
# Start listening for clients.
server_socket.listen(5)
print(
f"Server listening on "
f"{SERVER_HOST}:{SERVER_PORT}"
)
# Wait for one client.
connection, client_address = (
server_socket.accept()
)
# Manage the accepted connection.
with connection:
print(
"Client connected:",
client_address
)
# Receive up to 1024 bytes.
message_bytes = connection.recv(
1024
)
# Convert the bytes to text.
message = message_bytes.decode(
"utf-8"
)
print("Received:", message)
# Send a response.
response = (
"Server received your message."
)
connection.sendall(
response.encode("utf-8")
)
print("Response sent.")
if __name__ == "__main__":
main()
Example Server Output:
Server listening on 127.0.0.1:5000
Client connected: ('127.0.0.1', 51842)
Received: Hello from the client!
Response sent.
Output Explanation:
The server waits until a client connects. It receives the client’s message and returns a confirmation response.
How to run the TCP examples
Open two terminal windows in the folder containing the files. Start the server first:
python tcp_server.py
In the second terminal, start the client:
python tcp_client.py
The server must be running before the client attempts to connect.
44.21 UDP Programming
UDP programs use datagram sockets. The server binds to a host and port and receives messages with recvfrom(). The client sends messages with sendto(). UDP does not create a continuing connection, so each message includes destination information. Applications must decide how to handle missing, repeated, or out-of-order messages.
Example: UDP server
# udp_server.py
# Import networking tools.
import socket
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 6000
def main():
# Create an IPv4 UDP socket.
with socket.socket(
socket.AF_INET,
socket.SOCK_DGRAM
) as server_socket:
# Bind the socket to the local endpoint.
server_socket.bind(
(
SERVER_HOST,
SERVER_PORT
)
)
print(
f"UDP server listening on "
f"{SERVER_HOST}:{SERVER_PORT}"
)
# Receive one datagram and sender address.
message_bytes, client_address = (
server_socket.recvfrom(1024)
)
message = message_bytes.decode(
"utf-8"
)
print(
"Received from",
client_address,
":",
message
)
# Send a response to that client.
response = "UDP message received."
server_socket.sendto(
response.encode("utf-8"),
client_address
)
if __name__ == "__main__":
main()
Example: UDP client
# udp_client.py
# Import networking tools.
import socket
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 6000
def main():
# Create a UDP socket.
with socket.socket(
socket.AF_INET,
socket.SOCK_DGRAM
) as client_socket:
# Add a timeout.
client_socket.settimeout(5)
message = "Hello through UDP!"
# Send the datagram to the server.
client_socket.sendto(
message.encode("utf-8"),
(
SERVER_HOST,
SERVER_PORT
)
)
print("Sent:", message)
# Wait for a response datagram.
response_bytes, server_address = (
client_socket.recvfrom(1024)
)
response = response_bytes.decode(
"utf-8"
)
print(
"Received from",
server_address,
":",
response
)
if __name__ == "__main__":
main()
Example Client Output:
Sent: Hello through UDP!
Received from ('127.0.0.1', 6000): UDP message received.
Output Explanation:
The client sends one independent datagram. The server receives it and sends another datagram back to the client’s address.
How to run the UDP examples
Start the UDP server in the first terminal:
python udp_server.py
Start the UDP client in the second terminal:
python udp_client.py
44.22 Network Error Handling
Network operations can fail because a server is unavailable, a domain cannot be resolved, a connection times out, a port is already in use, or the remote side closes unexpectedly. Programs should use timeouts, catch specific socket exceptions, validate received information, close sockets reliably, and display clear messages without exposing sensitive information.
Example: Safe TCP connection attempt
# Import the socket module.
import socket
def connect_to_server(host, port):
try:
# Create a TCP connection with a timeout.
with socket.create_connection(
(host, port),
timeout=5
) as connection:
print(
f"Connected to {host}:{port}"
)
return True
except socket.timeout:
print(
"The connection attempt timed out."
)
except socket.gaierror:
print(
"The host name could not be resolved."
)
except ConnectionRefusedError:
print(
"The server refused the connection."
)
except OSError as error:
print(
"A network error occurred:",
error
)
return False
connect_to_server(
"127.0.0.1",
5000
)
Possible Output When Server Is Not Running:
The server refused the connection.
Output Explanation:
The operating system found the computer, but no server was accepting connections on port 5000. The program handles the error instead of crashing.
Example: Handle incomplete received data
# Create a function that validates received bytes.
def decode_message(message_bytes):
if not message_bytes:
return "The remote connection was closed."
try:
return message_bytes.decode("utf-8")
except UnicodeDecodeError:
return "The received data was not valid UTF-8."
print(decode_message(b"Hello"))
print(decode_message(b""))
Output:
Hello
The remote connection was closed.
Output Explanation:
An empty byte string commonly means the remote endpoint closed the connection. The function checks this condition before decoding.
Important network safety practices
- Use timeouts for client connections and receiving operations.
- Validate all messages received from another computer.
- Limit the maximum size of accepted messages.
- Do not trust client-supplied file names or commands.
- Do not expose development servers directly to the public internet.
- Use encryption and authentication for sensitive applications.
- Close sockets with context managers or finally blocks.
- Log errors without logging passwords or private information.
44.23 Chapter Project
In this project, you will create a local TCP message server and client. The client sends JSON messages to the server. The server validates each message, performs a requested operation, and returns a JSON response. The project demonstrates sockets, TCP, clients, servers, addresses, ports, encoding, message framing, JSON, timeouts, request-response communication, and error handling.
Project features
- A TCP server that listens on localhost.
- A menu-driven TCP client.
- JSON request and response messages.
- Length-prefixed message framing.
- Commands for ping, echo, time, uppercase, and addition.
- Validation of incoming requests.
- Network timeout and connection error handling.
- Multiple client connections handled one after another.
Step 1: Create the project structure
network_message_app/
│
├── client.py
├── server.py
└── protocol.py
Step 2: Create protocol.py
# protocol.py
# Import JSON conversion.
import json
# Import socket support.
import socket
# Import structured binary packing.
import struct
# Limit message size to one megabyte.
MAX_MESSAGE_SIZE = 1_000_000
# Store the length header as four bytes.
HEADER_SIZE = 4
def receive_exactly(connection, byte_count):
"""
Receive exactly the requested number of bytes.
A TCP recv() call may return less data than
requested, so this function keeps receiving.
"""
received_parts = []
remaining = byte_count
while remaining > 0:
# Receive another part of the message.
part = connection.recv(remaining)
# Empty bytes mean the connection closed.
if not part:
raise ConnectionError(
"The connection closed before "
"the complete message arrived."
)
received_parts.append(part)
remaining -= len(part)
# Join all received byte parts.
return b"".join(received_parts)
def send_json_message(connection, message):
"""
Convert a Python dictionary to JSON and send it
with a four-byte length prefix.
"""
# Convert the dictionary to JSON text.
json_text = json.dumps(message)
# Convert JSON text to UTF-8 bytes.
message_bytes = json_text.encode("utf-8")
# Validate the outgoing message size.
if len(message_bytes) > MAX_MESSAGE_SIZE:
raise ValueError(
"The message is too large."
)
# Pack the message length into four bytes.
header = struct.pack(
"!I",
len(message_bytes)
)
# Send the header and body together.
connection.sendall(
header + message_bytes
)
def receive_json_message(connection):
"""
Receive a length-prefixed JSON message and
convert it into a Python dictionary.
"""
# Receive the four-byte message header.
header = receive_exactly(
connection,
HEADER_SIZE
)
# Convert the header to an integer.
message_size = struct.unpack(
"!I",
header
)[0]
# Reject invalid message sizes.
if message_size <= 0:
raise ValueError(
"The message size must be positive."
)
if message_size > MAX_MESSAGE_SIZE:
raise ValueError(
"The incoming message is too large."
)
# Receive the complete JSON body.
message_bytes = receive_exactly(
connection,
message_size
)
try:
# Decode the UTF-8 text.
json_text = message_bytes.decode(
"utf-8"
)
except UnicodeDecodeError as error:
raise ValueError(
"The message is not valid UTF-8."
) from error
try:
# Convert JSON into a Python value.
message = json.loads(json_text)
except json.JSONDecodeError as error:
raise ValueError(
"The message does not contain "
"valid JSON."
) from error
# Require a JSON object.
if not isinstance(message, dict):
raise ValueError(
"The JSON message must be an object."
)
return message
Code Explanation:
TCP sends a continuous stream of bytes and does not preserve message boundaries. The project therefore sends a four-byte header before each JSON message. The header tells the receiver how many body bytes must be read. This prevents one message from being confused with another.
Step 3: Create server.py
# server.py
# Import date and time support.
from datetime import datetime
# Import socket networking.
import socket
# Import project protocol functions.
from protocol import (
receive_json_message,
send_json_message
)
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 7000
# Stop waiting on inactive clients after 30 seconds.
CLIENT_TIMEOUT = 30
def create_success_response(data):
"""
Create a consistent successful response.
"""
return {
"success": True,
"data": data,
"error": None
}
def create_error_response(message):
"""
Create a consistent error response.
"""
return {
"success": False,
"data": None,
"error": message
}
def require_text(value, field_name):
"""
Validate that a value is non-empty text.
"""
if not isinstance(value, str):
raise ValueError(
f"{field_name} must be text."
)
value = value.strip()
if not value:
raise ValueError(
f"{field_name} cannot be empty."
)
return value
def require_number(value, field_name):
"""
Validate that a value is an integer or float.
"""
# Boolean is a subclass of integer,
# so reject it separately.
if isinstance(value, bool):
raise ValueError(
f"{field_name} must be a number."
)
if not isinstance(value, (int, float)):
raise ValueError(
f"{field_name} must be a number."
)
return value
def process_request(request):
"""
Validate and process one client request.
"""
# Read the action field.
action = request.get("action")
if not isinstance(action, str):
return create_error_response(
"The action field is required."
)
action = action.strip().lower()
try:
# Test whether the server is available.
if action == "ping":
return create_success_response(
{
"message": "pong"
}
)
# Return client-provided text.
if action == "echo":
text = require_text(
request.get("text"),
"text"
)
return create_success_response(
{
"original": text
}
)
# Convert text to uppercase.
if action == "uppercase":
text = require_text(
request.get("text"),
"text"
)
return create_success_response(
{
"original": text,
"uppercase": text.upper()
}
)
# Return the server's local date and time.
if action == "time":
current_time = datetime.now()
return create_success_response(
{
"date": current_time.strftime(
"%Y-%m-%d"
),
"time": current_time.strftime(
"%H:%M:%S"
)
}
)
# Add two numeric values.
if action == "add":
first_number = require_number(
request.get("first"),
"first"
)
second_number = require_number(
request.get("second"),
"second"
)
result = (
first_number + second_number
)
return create_success_response(
{
"first": first_number,
"second": second_number,
"result": result
}
)
# Handle an unknown action.
return create_error_response(
f"Unknown action: {action}"
)
except ValueError as error:
return create_error_response(
str(error)
)
def handle_client(connection, client_address):
"""
Receive requests from one connected client.
"""
print(
"Client connected:",
client_address
)
# Add a timeout to this client connection.
connection.settimeout(
CLIENT_TIMEOUT
)
while True:
try:
# Receive one request.
request = receive_json_message(
connection
)
print(
"Request from",
client_address,
":",
request
)
# Allow the client to close politely.
if request.get("action") == "disconnect":
response = create_success_response(
{
"message": "Disconnected."
}
)
send_json_message(
connection,
response
)
break
# Process the request.
response = process_request(
request
)
# Send the response.
send_json_message(
connection,
response
)
except socket.timeout:
print(
"Client timed out:",
client_address
)
break
except ConnectionError:
print(
"Client closed the connection:",
client_address
)
break
except ValueError as error:
# Send a safe protocol error response.
error_response = create_error_response(
str(error)
)
try:
send_json_message(
connection,
error_response
)
except OSError:
pass
break
except OSError as error:
print(
"Socket error for",
client_address,
":",
error
)
break
print(
"Client disconnected:",
client_address
)
def run_server():
"""
Start the TCP server.
"""
# Create an IPv4 TCP socket.
with socket.socket(
socket.AF_INET,
socket.SOCK_STREAM
) as server_socket:
# Allow quick server restarts.
server_socket.setsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEADDR,
1
)
# Attach the socket to localhost and port.
server_socket.bind(
(
SERVER_HOST,
SERVER_PORT
)
)
# Allow queued client connections.
server_socket.listen(5)
print(
f"Message server listening on "
f"{SERVER_HOST}:{SERVER_PORT}"
)
print(
"Press Ctrl+C to stop the server."
)
try:
while True:
# Wait for the next client.
connection, client_address = (
server_socket.accept()
)
# Close each client connection
# automatically after handling it.
with connection:
handle_client(
connection,
client_address
)
except KeyboardInterrupt:
print("\nServer stopped.")
if __name__ == "__main__":
run_server()
Server Code Explanation:
The server listens on localhost port 7000. It accepts one client at a time and continues receiving requests until the client disconnects. Each request contains an action. The server validates the action, creates a response, and returns it as JSON.
Step 4: Create client.py
# client.py
# Import socket networking.
import socket
# Import project protocol functions.
from protocol import (
receive_json_message,
send_json_message
)
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 7000
CONNECTION_TIMEOUT = 5
def show_menu():
"""
Display available client actions.
"""
print("\nNetwork Message Client")
print("1. Ping server")
print("2. Echo text")
print("3. Convert text to uppercase")
print("4. Get server time")
print("5. Add two numbers")
print("6. Exit")
def display_response(response):
"""
Display a server response clearly.
"""
if response.get("success"):
print("Success:", response.get("data"))
else:
print("Server error:", response.get("error"))
def build_request(choice):
"""
Build a request based on the menu choice.
"""
if choice == "1":
return {
"action": "ping"
}
if choice == "2":
text = input(
"Enter text: "
)
return {
"action": "echo",
"text": text
}
if choice == "3":
text = input(
"Enter text: "
)
return {
"action": "uppercase",
"text": text
}
if choice == "4":
return {
"action": "time"
}
if choice == "5":
try:
first = float(
input("First number: ")
)
second = float(
input("Second number: ")
)
except ValueError:
print(
"Both values must be numbers."
)
return None
return {
"action": "add",
"first": first,
"second": second
}
return None
def run_client():
"""
Connect to the server and show the menu.
"""
try:
# Create and connect a TCP socket.
with socket.create_connection(
(
SERVER_HOST,
SERVER_PORT
),
timeout=CONNECTION_TIMEOUT
) as connection:
# Set a timeout for later operations.
connection.settimeout(
CONNECTION_TIMEOUT
)
print(
f"Connected to "
f"{SERVER_HOST}:{SERVER_PORT}"
)
while True:
show_menu()
choice = input(
"Choose an option: "
).strip()
if choice == "6":
# Tell the server to close politely.
send_json_message(
connection,
{
"action": "disconnect"
}
)
response = receive_json_message(
connection
)
display_response(response)
print("Goodbye.")
break
request = build_request(choice)
if request is None:
print("Invalid option.")
continue
# Send one JSON request.
send_json_message(
connection,
request
)
# Receive one JSON response.
response = receive_json_message(
connection
)
display_response(response)
except socket.timeout:
print(
"The network operation timed out."
)
except ConnectionRefusedError:
print(
"The server is not running or "
"refused the connection."
)
except ConnectionError as error:
print(
"The connection ended:",
error
)
except OSError as error:
print(
"A network error occurred:",
error
)
except ValueError as error:
print(
"A message error occurred:",
error
)
if __name__ == "__main__":
run_client()
Client Code Explanation:
The client connects once and then sends several requests through the same TCP connection. The menu creates correctly structured request dictionaries. The protocol module converts each dictionary to JSON, sends it, receives the server response, and converts it back to a dictionary.
Step 5: Run the server
Open a terminal inside the project folder and run:
python server.py
Step 6: Run the client
Open a second terminal in the same folder and run:
python client.py
Example client output
Output:
Connected to 127.0.0.1:7000
Network Message Client
1. Ping server
2. Echo text
3. Convert text to uppercase
4. Get server time
5. Add two numbers
6. Exit
Choose an option: 1
Success: {'message': 'pong'}
Choose an option: 2
Enter text: Hello server
Success: {'original': 'Hello server'}
Choose an option: 3
Enter text: learning python networking
Success: {
'original': 'learning python networking',
'uppercase': 'LEARNING PYTHON NETWORKING'
}
Choose an option: 4
Success: {
'date': '2026-07-19',
'time': '19:30:15'
}
Choose an option: 5
First number: 12.5
Second number: 7.5
Success: {
'first': 12.5,
'second': 7.5,
'result': 20.0
}
Choose an option: 6
Success: {'message': 'Disconnected.'}
Goodbye.
Output Explanation:
The client sends several different actions through one TCP connection. The server responds to ping, returns text, converts text to uppercase, reports its current time, adds numbers, and closes the connection when requested.
Example server output
Output:
Message server listening on 127.0.0.1:7000
Press Ctrl+C to stop the server.
Client connected: ('127.0.0.1', 52018)
Request from ('127.0.0.1', 52018): {'action': 'ping'}
Request from ('127.0.0.1', 52018): {
'action': 'echo',
'text': 'Hello server'
}
Request from ('127.0.0.1', 52018): {
'action': 'uppercase',
'text': 'learning python networking'
}
Request from ('127.0.0.1', 52018): {'action': 'time'}
Request from ('127.0.0.1', 52018): {
'action': 'add',
'first': 12.5,
'second': 7.5
}
Request from ('127.0.0.1', 52018): {
'action': 'disconnect'
}
Client disconnected: ('127.0.0.1', 52018)
Output Explanation:
The server displays the client address and each received request. The client’s temporary port is chosen automatically by the operating system and may be different on every run.
Project Summary
This project demonstrates TCP sockets, localhost addresses, ports, client-server communication, socket timeouts, JSON messages, UTF-8 encoding, binary length headers, request validation, response formatting, repeated requests, clean disconnection, connection errors, and safe message-size limits.