python socket TCP UDP threading networking

Sockets in Python — TCP, UDP and Threading

Sockets are a software abstraction that enables communication between processes, either on the same machine or on different machines connected over a network. In Python, the socket module (part of the standard library) provides an interface to the operating system’s network APIs.

A socket is identified by the combination of:

  • IP address (or hostname) of the machine
  • port number on which the process is listening
  • transport protocol used (TCP or UDP)

Creating a socket

The socket.socket() function accepts two main arguments:

OptionDescription
AF_INETIPv4 address family
AF_INET6IPv6 address family
SOCK_STREAMTCP protocol (reliable, byte-stream connection)
SOCK_DGRAMUDP protocol (connectionless datagrams)
import socket

# TCP socket with IPv4 (most common combination)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# UDP socket with IPv4
s_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Default parameters are AF_INET and SOCK_STREAM
s = socket.socket()

Main methods

MethodDescription
bind(address)Assigns a local (host, port) address to the socket. Used by servers.
listen(backlog)Puts the socket into listening mode. backlog = max queued connections.
accept()Accepts an incoming connection. Returns (conn, address).
connect(address)Establishes a connection to a server. Used by clients.
send(data)Sends data. Warning: may not send all bytes!
sendall(data)Sends all data, guaranteeing complete transmission.
recv(bufsize)Receives at most bufsize bytes. Returns b'' when the connection closes.
sendto(data, addr)Sends data to a specific address (UDP).
recvfrom(bufsize)Receives data and returns (data, address) (UDP).
settimeout(secs)Sets a timeout in seconds. None = blocking, 0 = non-blocking.
close()Closes the socket and releases resources.

Warning: send() vs sendall() — send() may send fewer bytes than intended if the kernel buffer is full. Always use sendall() to guarantee complete transmission.

TCP Sockets

TCP connection lifecycle

SERVER                          CLIENT
socket()                        socket()
bind()                          |
listen()                        |
accept() <------- connect() ----+
    |                               |
recv() <---------- sendall() -------+
    |                               |
sendall() ---------> recv() --------+
    |                               |
close()                         close()

Basic TCP server

import socket

HOST = '127.0.0.1'
PORT = 9000

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SO_REUSEADDR avoids 'Address already in use' after a quick restart
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f'Server listening on {HOST}:{PORT}')

conn, addr = server.accept()
print(f'Connection accepted from {addr}')

while True:
    data = conn.recv(1024)
    if not data:
        break
    print(f'Received: {data.decode()}')
    conn.sendall(data)  # echo

conn.close()
server.close()

TCP client

import socket

HOST = '127.0.0.1'
PORT = 9000

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))

message = 'Hello, I am the client!'
client.sendall(message.encode())

response = client.recv(1024)
print(f'Server response: {response.decode()}')

client.close()

The with statement guarantees automatic socket closure even if an exception occurs:

import socket

HOST = '127.0.0.1'
PORT = 9000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
    client.connect((HOST, PORT))
    client.sendall(b'Test message')
    response = client.recv(1024)
    print(f'Response: {response.decode()}')
# Socket is already closed here automatically

Exception handling

import socket

HOST = '127.0.0.1'
PORT = 9000

try:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
        client.settimeout(5)
        client.connect((HOST, PORT))
        client.sendall(b'Test')
        response = client.recv(1024)
        print(f'Received: {response.decode()}')

except socket.timeout:
    print('Error: server did not respond within 5 seconds')
except ConnectionRefusedError:
    print(f'Error: no server running on {HOST}:{PORT}')
except OSError as e:
    print(f'Network error: {e}')

Multi-client TCP server with Threading

A thread is created for each connecting client, allowing multiple concurrent connections:

import socket
import threading

HOST = '127.0.0.1'
PORT = 9000

def handle_client(conn, addr):
    """Function executed in a separate thread for each client."""
    print(f'[+] Connected: {addr}')
    try:
        conn.sendall(b'Welcome! Type something and press enter.')
        while True:
            data = conn.recv(1024)
            if not data:
                break
            print(f'[{addr}] Received: {data.decode().strip()}')
            conn.sendall(data)  # echo
    except OSError as e:
        print(f'[{addr}] Error: {e}')
    finally:
        conn.close()
        print(f'[-] Disconnected: {addr}')

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f'Server listening on {HOST}:{PORT}')

try:
    while True:
        conn, addr = server.accept()
        t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
        t.start()
        print(f'Active threads: {threading.active_count() - 1}')
except KeyboardInterrupt:
    print('\nServer stopped with Ctrl+C')
finally:
    server.close()

Daemon threads

A daemon thread is automatically terminated when the main program finishes:

import threading

t = threading.Thread(target=my_function, daemon=True)
t.start()
# When main exits, t is automatically terminated

UDP Sockets

UDP is a connectionless protocol: data is sent as independent datagrams with no guarantee of delivery or ordering. It is faster than TCP.

TCP (SOCK_STREAM)UDP (SOCK_DGRAM)
ConnectionOriented, reliableConnectionless
Packet orderGuaranteedNot guaranteed
SpeedSlowerFaster
Typical useWeb, email, filesStreaming, games, DNS

UDP server

import socket

HOST = '127.0.0.1'
PORT = 9001

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
    server.bind((HOST, PORT))
    print(f'UDP server listening on {HOST}:{PORT}')

    while True:
        data, addr = server.recvfrom(1024)
        print(f'Received from {addr}: {data.decode()}')
        server.sendto(data, addr)  # echo

UDP client

import socket

HOST = '127.0.0.1'
PORT = 9001

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
    message = 'Hello via UDP!'
    client.sendto(message.encode(), (HOST, PORT))

    response, server_addr = client.recvfrom(1024)
    print(f'Response from {server_addr}: {response.decode()}')

Useful options

Host information

import socket

print(socket.gethostname())                                      # local hostname
print(socket.gethostbyname(socket.gethostname()))                # local IP
info = socket.getaddrinfo('www.google.com', 80, socket.AF_INET)
print(info[0][4])                                                # resolved IP

Timeout and non-blocking mode

s.settimeout(None)    # blocking (default)
s.settimeout(5.0)     # 5 second timeout
s.setblocking(False)  # non-blocking (equivalent to settimeout(0))

Socket options with setsockopt

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)   # avoids 'Address already in use'
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)   # detects dropped connections
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)   # disables Nagle's algorithm

Exercises

Basic level

  • Modify the TCP client to send 5 messages in sequence ('Message 1', 'Message 2', …) and print each response.
  • Create a TCP server that always replies with the received message converted to UPPERCASE.
  • Test the UDP server by sending three consecutive messages from the client.

Intermediate level

  • Create a network calculator: the client sends a string like '10 + 5' and the server replies with the result. Handle division by zero and invalid operators.
  • Modify the multi-client server to count connected clients and include the count in the welcome message.
  • Implement a timeout in the client: if the server doesn’t respond within 3 seconds, print an error message.

Advanced level

  • Create a file server: the client sends a filename and the server responds with its contents.
  • Implement a multi-user chat: the server receives messages from each client and broadcasts them to all others.

Summary

ConceptRemember
sendall()Always use instead of send()
SO_REUSEADDRAlways set on servers
with statementGuarantees automatic closure
ExceptionsAlways handle with try/except
Daemon threadsTerminate automatically with the main process
TCP vs UDPTCP = reliable; UDP = fast