ch10s3_NetworkingWithSockets

Networking allows computers to exchange data. In Python, **sockets** provide a low-level interface for this communication, enabling you to build servers, clients, and protocols like HTTP, FTP, or custom systems.

Chapter 10: Advanced Topics — Networking with Sockets

🌐 Networking with Sockets — Communication Between Devices

Networking allows computers to exchange data. In Python, sockets provide a low-level interface for this communication, enabling you to build servers, clients, and protocols like HTTP, FTP, or custom systems.


🧩 1. What Are Sockets?

A socket is an endpoint of a two-way communication channel between programs running on a network.
Think of it as a phone line — both the caller (client) and receiver (server) need one to talk.

TermDescription
IP AddressThe unique address of a machine on a network (like a phone number).
PortA number identifying a specific service or application (like an extension number).
ProtocolDefines how communication happens — usually TCP or UDP.

🔗 2. TCP vs UDP

FeatureTCP (SOCK_STREAM)UDP (SOCK_DGRAM)
TypeConnection-orientedConnectionless
ReliabilityGuaranteed (retransmission)Not guaranteed
SpeedSlowerFaster
OrderPreservedNot preserved
Use CaseWeb servers, file transferStreaming, games, IoT

🖥️ 3. Building a TCP Server

A server listens for incoming connections, accepts them, and exchanges data.

import socket

HOST = 'localhost'
PORT = 8080

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind((HOST, PORT))
    server_socket.listen(1)
    print(f"Server listening on {HOST}:{PORT}...")

    conn, addr = server_socket.accept()
    with conn:
        print(f"Connected by {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            print(f"Received: {data.decode()}")
            conn.sendall(b"Hello from the server!")

💻 4. Building a TCP Client

A client connects to a server, sends data, and waits for a response.

import socket

HOST = 'localhost'
PORT = 8080

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))
    message = "Hello from the client!"
    client_socket.sendall(message.encode())

    data = client_socket.recv(1024)
    print(f"Server replied: {data.decode()}")

💡 recv(1024) means “receive up to 1024 bytes.” Always ensure your buffer is large enough.


🔁 5. Two-Way Example — Client and Server

import socket, threading

def server():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(('localhost', 8080))
        s.listen()
        print("Server waiting for connection...")
        conn, addr = s.accept()
        with conn:
            print(f"Connected: {addr}")
            while True:
                data = conn.recv(1024)
                if not data:
                    break
                print("Received:", data.decode())
                conn.sendall(b"Message received!")

def client():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as c:
        c.connect(('localhost', 8080))
        for msg in ["Hello", "How are you?", "Goodbye"]:
            c.sendall(msg.encode())
            print("Server:", c.recv(1024).decode())

# Run server and client concurrently
threading.Thread(target=server, daemon=True).start()
threading.Thread(target=client).start()

📡 6. UDP Communication — Faster but Unreliable

UDP is faster because it doesn’t establish a connection before sending data.

UDP Server

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(('localhost', 9090))
print("UDP Server listening...")

while True:
    data, address = server_socket.recvfrom(1024)
    print(f"Received {data.decode()} from {address}")
    server_socket.sendto(b"Hello from UDP server!", address)

UDP Client

import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('localhost', 9090)

client_socket.sendto(b"Hello UDP!", server_address)
data, _ = client_socket.recvfrom(1024)
print("Received:", data.decode())
client_socket.close()

⚡ UDP is ideal for real-time applications like online games or video streaming.


👥 7. Handling Multiple Clients — Threaded TCP Server

To handle several clients at once, use threading.

import socket, threading

def handle_client(conn, addr):
    print(f"New connection: {addr}")
    with conn:
        while True:
            data = conn.recv(1024)
            if not data:
                break
            print(f"{addr}: {data.decode()}")
            conn.sendall(b"Echo: " + data)

def start_server():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(('localhost', 7070))
        s.listen()
        print("Server running on port 7070...")

        while True:
            conn, addr = s.accept()
            thread = threading.Thread(target=handle_client, args=(conn, addr))
            thread.start()

start_server()

🧮 8. Async Networking — Modern asyncio Example

For highly scalable applications, Python’s asyncio library allows non-blocking I/O without threads.

import asyncio

async def handle_client(reader, writer):
    data = await reader.read(100)
    message = data.decode()
    writer.write(f"Echo: {message}".encode())
    await writer.drain()
    writer.close()

async def main():
    server = await asyncio.start_server(handle_client, 'localhost', 8081)
    async with server:
        await server.serve_forever()

asyncio.run(main())

🧠 Async I/O handles thousands of connections efficiently (used by frameworks like FastAPI and aiohttp).


🔐 9. Security, Errors, and Cleanup

✅ Use with socket.socket(...) to ensure proper closure.
✅ Catch ConnectionResetError and handle gracefully.
✅ Avoid hardcoded ports (use configs or environment variables).
✅ Sanitize incoming data to avoid injection attacks.


⚖️ 10. Comparison Table

AspectTCPUDPasyncio
ConnectionRequired (3-way handshake)NoneNon-blocking event loop
ReliabilityGuaranteedNot guaranteedManaged by event loop
SpeedModerateVery fastScalable
Typical UseWeb apps, APIsGames, sensors, logsServers, microservices

💬 11. Real-World Example — Mini Chat Server

import socket, threading

clients = []

def handle_client(conn, addr):
    print(f"Client connected: {addr}")
    clients.append(conn)
    while True:
        data = conn.recv(1024)
        if not data:
            break
        for client in clients:
            if client != conn:
                client.sendall(data)
    clients.remove(conn)
    conn.close()

def start_chat_server():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(('localhost', 9000))
        s.listen()
        print("Chat server running on port 9000...")
        while True:
            conn, addr = s.accept()
            threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()

start_chat_server()

Each connected client broadcasts messages to others — the foundation of real-time chat systems.


🧭 12. Best Practices

✅ Always close sockets (with statement).
✅ Use try/except for safe cleanup.
✅ Use threading or asyncio for multi-client apps.
✅ Choose TCP for reliability, UDP for speed.
✅ Never trust raw input — validate and sanitize.
✅ Prefer socketserver or frameworks like asyncio for production systems.


🧠 Summary

ConceptDescriptionExample
SocketEndpoint of communication between programssocket.socket(AF_INET, SOCK_STREAM)
ServerListens for incoming connectionsbind(), listen(), accept()
ClientConnects to a serverconnect(), send(), recv()
TCPReliable stream communicationChat server
UDPFast, connectionless communicationIoT or games
asyncioAsynchronous I/O for scalabilityModern servers

Sockets are the foundation of all networked software — from web browsers to multiplayer games. Understanding them empowers you to build reliable, real-time, and scalable systems.