Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Python Network Programming: Protocol Fundamentals and Socket Implementation

Tech 1

Network Protocol Architecture

Computer networks rely on layered communication standards that define how devices exchange data. The OSI model conceptualizes seven distinct layers, though the TCP/IP stack commonly implements four functional layers in practice.

Physical and Data Link Layers

At the physical layer, transmission occurs via electrical signals—high voltage represents binary 1, low voltage represents 0. The data link layer organizes these signals into structured frames using Ethernet protocols. Each frame contains a header specifying source and destination MAC addresses (hardware identifiers for network interfaces) and a payload segment. Ethernet operates through broadcasting within local network segments, requiring MAC addresses to identify specific recipients.

Network Layer

To enable communication across different networks, the network layer introduces logical addressing via IP protocols. IPv4 addresses (ranging from 0.0.0.0 to 255.255.255.255) contain two components:

  • Network portion: Identifies the specific subnet
  • Host portion: Identifies individual devices within that subnet

Subnet masks distinguish these portions by applying bitwise AND operations. When two addresses yield identical results after ANDing with the subnet mask, they reside on the same network segment.

The Address Resolution Protocol (ARP) maps IP addresses to MAC addresses. When transmitting to a remote network, data first routes to a gateway device; ARP resolves the gateawy's MAC address for frame delivery.

Transport Layer

This layer establishes end-to-end communication channels using port numbers (0-65535, with 0-1023 reserved for system services). Two primary protocols operate here:

TCP (Transmission Control Protocol) provides reliable, ordered delivery through:

  • Connection establishment via three-way handshake (SYN, SYN-ACK, ACK)
  • Acknowledgment packets confirming receipt
  • Flow control and retransmission mechanisms
  • Connection termination via four-way handshake to ensure complete data transfer

UDP (User Datagram Protocol) offers connectionless, lightweight transmission with:

  • Minimal 8-byte headers
  • No delivery guarantees or ordering
  • Lower latency suitable for streaming applications

Socket Programming Implementation

Python's socket module provides interfaces to the BSD socket API, enabling network communication through file descriptor abstraction.

Basic TCP Communication

Server Implementation:

import socket

srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('127.0.0.1', 9000))
srv.listen(5)

print("Server initialized...")
client_sock, addr = srv.accept()
print(f"Established connection from {addr}")

payload = client_sock.recv(2048)
print(f"Received: {payload.decode()}")
client_sock.send(payload.swapcase())

client_sock.close()
srv.close()

Client Implementation:

import socket

clt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clt.connect(('127.0.0.1', 9000))

clt.send(b"Hello Server")
response = clt.recv(2048)
print(response.decode())
clt.close()

Robust Connection Handling

Production implementations require handling multiple connections and edge cases:

Iterative Server with Error Handling:

import socket
import sys

def create_server(host='0.0.0.0', port=9000):
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listener.bind((host, port))
    listener.listen(10)
    
    print(f"Listening on {host}:{port}")
    
    while True:
        sock, client_info = listener.accept()
        print(f"Client connected: {client_info}")
        
        try:
            while True:
                chunk = sock.recv(1024)
                if not chunk:
                    # Client gracefully closed (Linux/Unix behavior)
                    break
                    
                message = chunk.decode('utf-8').strip()
                if message:
                    sock.send(f"Echo: {message}".encode())
                    
        except ConnectionResetError:
            # Handle Windows-style abrupt disconnects
            print(f"Connection reset by {client_info}")
        except Exception as err:
            print(f"Error processing client: {err}")
        finally:
            sock.close()

if __name__ == "__main__":
    create_server()

Interactive Client:

import socket

def run_client(server_host='127.0.0.1', server_port=9000):
    tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_client.connect((server_host, server_port))
    
    try:
        while True:
            user_input = input("Enter message: ").strip()
            if not user_input:
                continue
                
            tcp_client.send(user_input.encode('utf-8'))
            data = tcp_client.recv(4096)
            print(f"Server response: {data.decode('utf-8')}")
            
    except KeyboardInterrupt:
        print("\nClosing connection...")
    finally:
        tcp_client.close()

if __name__ == "__main__":
    run_client()

Remote Command Execution

Sockets can transport shell command output between machines:

Command Server:

import socket
import subprocess
import shlex

def command_server(address=('127.0.0.1', 8080)):
    srv_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv_socket.bind(address)
    srv_socket.listen(5)
    
    print(f"Command server active on {address}")
    
    while True:
        session, remote_addr = srv_socket.accept()
        print(f"Session from {remote_addr}")
        
        with session:
            while True:
                try:
                    cmd_bytes = session.recv(1024)
                    if not cmd_bytes:
                        break
                        
                    command = cmd_bytes.decode('utf-8').strip()
                    if command.lower() == 'exit':
                        break
                    
                    process = subprocess.Popen(
                        shlex.split(command),
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        shell=True
                    )
                    
                    stdout_data, stderr_data = process.communicate()
                    session.sendall(stdout_data)
                    session.sendall(stderr_data)
                    
                except ConnectionResetError:
                    break
                except Exception as e:
                    session.send(f"Execution error: {str(e)}".encode())

if __name__ == "__main__":
    command_server()

Command Client:

import socket

def command_client(server=('127.0.0.1', 8080)):
    conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    conn.connect(server)
    
    try:
        while True:
            cmd = input("$ ").strip()
            if not cmd:
                continue
                
            conn.send(cmd.encode('utf-8'))
            
            # Collect potentially large output
            output_buffer = []
            while True:
                packet = conn.recv(4096)
                if not packet:
                    break
                output_buffer.append(packet)
                if len(packet) < 4096:
                    break
                    
            result = b''.join(output_buffer).decode('gbk', errors='ignore')
            print(result)
            
    except KeyboardInterrupt:
        pass
    finally:
        conn.close()

if __name__ == "__main__":
    command_client()

Message Boundary Handling (Sticky Packet Solution)

TCP's stream-oriented nature combines small successive messages (Nagle's algorithm optimization), causing receivers to obtain concatenated data chunks rather than individual messages. This requires application-layer framing.

Header-Based Protocol Design

Implement a fixed-length header containing metadata (particularly payload size) followed by variable-length data:

import socket
import struct
import json
import subprocess

def framed_server(host='127.0.0.1', port=8080):
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((host, port))
    srv.listen(5)
    
    print("Framed protocol server running...")
    
    while True:
        client, addr = srv.accept()
        
        with client:
            while True:
                try:
                    # Receive command
                    cmd_data = client.recv(1024)
                    if not cmd_data:
                        break
                    
                    # Execute command
                    cmd = cmd_data.decode('utf-8')
                    proc = subprocess.Popen(
                        cmd, shell=True,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE
                    )
                    out, err = proc.communicate()
                    total_len = len(out) + len(err)
                    
                    # Construct metadata header
                    metadata = {
                        'filename': 'output.txt',
                        'hash': 'md5_placeholder',
                        'size': total_len
                    }
                    meta_json = json.dumps(metadata).encode('utf-8')
                    meta_length = len(meta_json)
                    
                    # Send: [4-byte length][header][payload]
                    client.send(struct.pack('I', meta_length))
                    client.send(meta_json)
                    client.send(out)
                    client.send(err)
                    
                except ConnectionResetError:
                    break

def framed_client(server=('127.0.0.1', 8080)):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(server)
    
    try:
        while True:
            command = input("cmd> ").strip()
            if not command:
                continue
                
            sock.send(command.encode('utf-8'))
            
            # Receive fixed-size header length (4 bytes)
            length_bytes = sock.recv(4)
            if not length_bytes:
                break
                
            header_size = struct.unpack('I', length_bytes)[0]
            
            # Receive actual header
            header_data = sock.recv(header_size)
            header = json.loads(header_data.decode('utf-8'))
            expected_size = header['size']
            
            # Receive payload with precise length tracking
            received = 0
            chunks = []
            
            while received < expected_size:
                remaining = expected_size - received
                chunk_size = min(4096, remaining)
                chunk = sock.recv(chunk_size)
                if not chunk:
                    raise ConnectionError("Incomplete data")
                chunks.append(chunk)
                received += len(chunk)
            
            response = b''.join(chunks).decode('gbk', errors='ignore')
            print(response)
            
    except KeyboardInterrupt:
        pass
    finally:
        sock.close()

if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1 and sys.argv[1] == 'server':
        framed_server()
    else:
        framed_client()

UDP Socket Communication

User Datagram Protocol provides connectionless, packet-based transmission without reliability guarantees.

UDP Echo Server:

from socket import socket, AF_INET, SOCK_DGRAM

def udp_server(bind_addr=('127.0.0.1', 9999)):
    srv = socket(AF_INET, SOCK_DGRAM)
    srv.bind(bind_addr)
    print(f"UDP server listening on {bind_addr}")
    
    try:
        while True:
            data, client_addr = srv.recvfrom(2048)
            print(f"Datagram from {client_addr}: {data.decode()}")
            srv.sendto(data.upper(), client_addr)
    except KeyboardInterrupt:
        print("\nShutting down...")
    finally:
        srv.close()

UDP Client:

from socket import socket, AF_INET, SOCK_DGRAM

def udp_client(server=('127.0.0.1', 9999)):
    clt = socket(AF_INET, SOCK_DGRAM)
    
    try:
        while True:
            msg = input("Message: ").strip()
            clt.sendto(msg.encode('utf-8'), server)
            
            reply, srv_addr = clt.recvfrom(2048)
            print(f"Reply: {reply.decode()}")
    except KeyboardInterrupt:
        pass
    finally:
        clt.close()

UDP characteristics include:

  • No connection establishment required (clients can transmit immediately)
  • Empty messages transmit succesfully
  • No boundary issues since datagrams maintain discrete packet boundaries
  • No delivery or ordering guarantees

Concurrent Server Architecture

The socketserver framework simplifies creating threaded or process-based concurrent servers.

Threaded TCP Server:

import socketserver
import threading

class ThreadedTCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        print(f"Thread {threading.current_thread().name} handling {self.client_address}")
        
        while True:
            try:
                data = self.request.recv(1024)
                if not data:
                    break
                
                message = data.decode('utf-8')
                print(f"Received: {message}")
                self.request.sendall(f"Processed: {message.upper()}".encode())
                
            except ConnectionResetError:
                break

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    allow_reuse_address = True
    daemon_threads = True

if __name__ == "__main__":
    with ThreadedTCPServer(('127.0.0.1', 8080), ThreadedTCPHandler) as server:
        print("Multi-threaded server running...")
        server.serve_forever()

Threaded UDP Server:

import socketserver

class UDPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        data, socket_obj = self.request
        client_addr = self.client_address
        
        print(f"Datagram from {client_addr}")
        socket_obj.sendto(data.upper(), client_addr)

class ThreadedUDPServer(socketserver.ThreadingMixIn, socketserver.UDPServer):
    allow_reuse_address = True

if __name__ == "__main__":
    with ThreadedUDPServer(('127.0.0.1', 9999), UDPHandler) as server:
        server.serve_forever()

The threading mixin enables simultaneous handling of multiple clients, while the BaseRequestHandler interface standardizes request processing across TCP (stream) and UDP (datagram) protocols.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.