Software Development Architectures
Network communication applications generally fall into two primary architectural categories: Client/Server (C/S) and Browser/Server (B/S).
The C/S Architecture requires a dedicated client application installed on the user's machine. This architecture typically offers richer user interfaces and higher performance but involves maintenance overhead for updates on the client side. Examples include multiplayer games and instant messaging apps like Discord.
The B/S Architecture relies on a standard web browser as the client. The heavy lifting is done on the server, and the client merely renders HTML/CSS/JavaScript. This architecture simplifies deployment since users only need a browser, making it the dominant model for web services like e-commerce sites and wikis.
Network Foundations
For two programs to communicate over a network, they must be able to locate each other. This is achieved via IP Addresses and Ports. An IP address identifies a specific device on the network, while a port number identifies a specific application or process running on that device.
Network communication is governed by protocols. The OSI Seven-Layer Model conceptualizes these protocols from physical transmission up to application-specific logic. In practical programming, we often interact with the Socket layer, which serves as an abstraction for the TCP/IP protocol stack. A socket encapsulates the complexities of network routing and packet handling, providing a simple interface for sending and receiving data.
TCP vs UDP Protocols
Two dominant transport layer protocols are used in socket programming:
- TCP (Transmission Control Protocol): A connection-oriented protocol that guarantees reliable, ordered delivery of data. It establishes a connection (handshake) before transferring data and checks for errors. It is analogous to a phone call.
- UDP (User Datagram Protocol): A connectionless protocol that sends datagrams without establishing a connection. It is fast but does not guarantee delivery, order, or error checking. It is analogous to sending a postcard or a radio broadcast.
TCP Socket Implementation
TCP requires a server to bind to a port and listen, while the client initiates the connection.
TCP Server Example
import socket
# Create a TCP socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind and listen
host, port = '127.0.0.1', 65432
server_socket.bind((host, port))
server_socket.listen(5)
print(f"Server listening on {host}:{port}")
while True:
client_conn, client_addr = server_socket.accept()
print(f"Connection from {client_addr}")
try:
while True:
data = client_conn.recv(1024)
if not data:
break
print(f"Received: {data.decode('utf-8')}")
client_conn.sendall(b"ACK: " + data)
finally:
client_conn.close()
TCP Client Example
import socket
host, port = '127.0.0.1', 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
s.sendall(b"Hello, Server")
response = s.recv(1024)
print(f"Server replied: {response.decode('utf-8')}")
UDP Socket Implementation
UDP is connectionless. The server binds to a port to receive data, and the client sends data directly to that address.
UDP Server Example
import socket
server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_sock.bind(('127.0.0.1', 9999))
print("UDP Server started...")
while True:
data, addr = server_sock.recvfrom(1024)
print(f"Received {data.decode('utf-8')} from {addr}")
server_sock.sendto(b"Message received", addr)
UDP Client Example
import socket
client_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
message = b"Hello via UDP"
client_sock.sendto(message, ('127.0.0.1', 9999))
response, _ = client_sock.recvfrom(1024)
print(f"Response: {response.decode('utf-8')}")
The Sticky Packet Issue
In TCP, data is treated as a continuous stream. The "sticky packet" phenomenon occurs when multiple sends by the application are merged into a single TCP segment by the sender's Nagle algorithm, or when the receiver reads data in chunks that don't align with the application's logical message boundaries.
For example, if the client sends "Hello" and then "World", the server might receive "HelloWorld" in one `recv` call, or "Hel" and "loWorld" in two calls. UDP does not suffer from this because it preserves message boundaries.
Solving Sticky Packets
To solve this, we must define message boundaries. The standard approach is to send a header containing the length of the data before sending the actual data. The `struct` module is ideal for packing this length into a fixed number of bytes.
import socket
import struct
import
# Helper to send messages with a length header
def send_msg(sock, data):
# Serialize and pack length
serialized = .dumps(data).encode('utf-8')
length_prefix = struct.pack('!I', len(serialized))
sock.sendall(length_prefix + serialized)
# Helper to receive messages based on length header
def recv_msg(sock):
# Read the 4-byte length prefix
raw_msglen = recvall(sock, 4)
if not raw_msglen: return None
msglen = struct.unpack('!I', raw_msglen)[0]
# Read the payload
return .loads(recvall(sock, msglen).decode('utf-8'))
def recvall(sock, n):
data = b''
while len(data) < n:
packet = sock.recv(n - len(data))
if not packet: return None
data += packet
return data
# Usage in a Server
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 5000))
server.listen(5)
conn, addr = server.accept()
# Receiving structured data
payload = recv_msg(conn)
print("Received Payload:", payload)
conn.close()
server.close()
Socket Verification (HMAC)
To prevent unauthorized clients from connecting, we can implement a simple challenge-response authentication using `hmac`. The server sends a random string; the client must hash it with a shared secret key and return the result.
import socket
import hmac
import os
SECRET_KEY = b'my_super_secret_key'
def verify_connection(conn):
# Generate random challenge
challenge = os.urandom(32)
conn.sendall(challenge)
# Expected response
expected_digest = hmac.new(SECRET_KEY, challenge).digest()
# Actual response
client_response = conn.recv(len(expected_digest))
return hmac.compare_digest(client_response, expected_digest)
# Server Implementation
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(1)
print("Auth Server running...")
while True:
conn, addr = server.accept()
if verify_connection(conn):
print(f"Client {addr} authenticated successfully.")
conn.sendall(b"Access Granted")
else:
print(f"Client {addr} failed authentication.")
conn.close()
Concurrent Sockets with SocketServer
Handling multiple clients simultaneously requires concurrency. The `socketserver` module simplifies creating threaded or forking servers.
import socketserver
class MyTCPHandler(socketserver.BaseRequestHandler):
def handle(self):
# self.request is the TCP socket connected to the client
self.data = self.request.recv(1024).strip()
print(f"Received from {self.client_address[0]}")
# Process and send back uppercase
self.request.sendall(self.data.upper())
if __name__ == "__main__":
HOST, PORT = "localhost", 9999
# Create a threaded server to handle multiple clients
with socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler) as server:
server.serve_forever()