Fundamentals of Python Socket Programming and Network Architecture

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()

Tags: python Network Programming Sockets tcp udp

Posted on Sun, 28 Jun 2026 17:22:00 +0000 by dnice