Server/Client Ping Pong in Python

Vogliamo realizzare un primo semplice esempio di applicativo di rete client/server che simuli una sorta di botta e risposta tra le due parti: il client invia un saluti “ping”, il server in ascolto legge il saluti e risponde “pong”. Sfruttiamo la tecnologia dei socket e il linguaggio Python.

Comunicazione in rete

L’esercizio rientra nello studio delle applicazioni di rete. L’idea è arrivare ad astrarre il concetto di socket nei livelli più alti dello stack TCP/IP. L’utilizzo dei socket risulta la base fondante dello scambio di messaggi in rete, ancora altamente performante ed utilizzata in sistemi real time o dispositivi embdedded tipo sensoristica, dove le risorse software ed hardware sono limitate e con usi critici.

Cos’è un Socket

Il socket è il mattone fondamentale della comunicazione tra dispositivi in una rete, che sia LAN o più estesa. Un socket è come una presa di corrente per la comunicazione tra computer: un punto di contatto attraverso cui due programmi possono “attaccarsi” per scambiarsi dati. Ogni socket è identificato da un indirizzo IP e da una porta, e permette a un’applicazione di collegarsi a un’altra, anche se si trovano su dispositivi diversi e in reti lontane. Esistono diversi tipi di socket: alcuni offrono una connessione stabile e affidabile (tipico del protocollo TCP), altri funzionano come messaggi veloci che possono anche perdersi per strada (tipico del protocollo UDP). In sostanza, i socket sono fondamentali: senza di loro, non esisterebbero chat, siti web, email o videogiochi online.

Come costruire un Socket

Entriamo nel vivo del codice. I socket sono astrazioni logiche presenti pressoché in modo analogo in tutti i linguaggi di programmazione. PEr questo vi è una chiamata/funzione piuttosto standard, anche in python. In particolare prende due parametri la FAMIGLIA e il TIPO:

socket.socket(socket.AF_INET, socket.SOCK_STREAM) 

Le famiglie dei Socket

Le famiglie di socket sono diverse. Nel nostro esempio, AF sta per Address Family, mentre la dicitura INET indica i socket gestiti con indirizzi IPv4 (sia privati che pubblici). Credo sia la famiglia più usata in assoluto. AF_iNET6 intuitivamente usa gli IPv6, AF_UNIX è la famiglia di socket utilizzata internamente i sistemi Unix per lo scambio di messaggi tra processi, AF_CAN utilizzata dai sistemi CAN Bus presenti nei veicoli/automotive e case domotiche, AF_BLUETOOTH per l’omologo protocollo, AF_PACKET per simulare e investigare pacchetti a livello Ethernet. Altri famiglie sono spesso appartenenti a tecnologie vetuste o poco utilizzate, certamente non didattiche.

I tipi di Socket

Discorso analogo per i tipi di scoket. SOCK_RAW (pacchetti grezzi), permette di inviare/ricevere pacchetti di rete a basso livello (es. ICMP usato da ping) ma richiede utente amministratore e ulteriori opzioni da definire. Sicuramente si potrebbe usare nellce esercitazioni didattiche, ma è già un po’ complesso da utilizzare. SOCK_STREAM è il tipo di socket orientato alla connessione con il protocollo TCP. La comunicazione è affidabile (se invio un messaggio, arriva o mi viene notificato l’errore), i dati arrivano nello stesso ordine in cui sono stati inviati. SOCK_DGRAM usa il protocollo UDP, senza connessione, più veloce ma meno affidabile (es. usato nei videogiochi online o streaming). Per le nostre applicazioni probabilmente il SOCK_STREAM è il più indicato.

Il server

Come costruiamo il nostro server? Per comodità e pulizia del codice usiamo la programmazione ad oggetti (vi invito a ripassare o vedere qualche rudimento sui manuali scolastici o online). Creiamo una classe in cui definiamo tre attributi, ovviamente attraverso il nostro costruttore __init__. Per gestire un socket ci servono infatti host/indirizzo, porta e la famiglia/tipologia di socket da usare.

Tralasciamo i metodi informativi di stampa che possono essere inseriti a piacere. Il metodo più importante è sicuramente quello che crea il socket e lo mette in attesa di messaggi. Qui lo chiamiamo start(). Le prime due importanti istruzioni sono:

        self.servizio.bind((self.host, self.porta))
        self.servizio.listen(1)

Fanno una cosa fondamentale: il bind associa al nostro socket l’indirizzo ip e la porta su cui attendere. Da questo momento i client potranno cercare il server all’indirizzo ip 127.0.0.1 e porta 5000. listen con parametro 1 indica che il socket rimane in attesa di 1 client alla volta ed 1 in attesa. In gergo tecnico è la backlog queue, cioè quanti client possono mettersi in attesa se il server è occupato (in questo caso massimo 1, le ulteriori vengono rifiutate).

A questo punto il nostro server può accettare eventuali connessioni/messaggi in arrivo. La funzione/chiamata accept() fa esattamente questo: quando arriva un messaggio restituisce l’indirizzo ip del mittente ed una connessione che conterrà una serie di informazioni più il messaggio. Il messaggio è in un formato bit grezzo che dobbiamo decodificare, ovvero tradurre, nel classico UTF-8. Un dettaglio non trascurabile è indicare la grandezza del buffer, ovvero lo spazio di dati che vogliamo ricevere. Per il momento come contenitore ci bastano 1024 byte che contengono bene il nostro testo ping/pong.

messaggio = connessione.recv(1024).decode()

Quindi non ci resta che decifrare il nostro messaggio. Se in effetti il messaggio arrivato è ping, allora vogli orispondere con un messaggio con su scritto pong. Se recv riceve, send invia e sempre con lo stesso trucco scompongo una scritta “pong” in byte con la funzione encode()

connessione.send("pong".encode())

Non ci resta che chiudere e lanciare un programma main con inizializzazione delle classe e del metodo start() che avvia il nostro server in ascolto.

import socket

class PingServer:

    def __init__(self, _host = "127.0.0.1",  _porta= 5000):
        self.host = _host
        self.porta = _porta
        self.servizio = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def stampa(self):
        print ("host:",self.host, " porta:",self.porta)

    def start(self):
        self.servizio.bind((self.host, self.porta))
        self.servizio.listen(1)
        print("Server in ascolto sulla porta ", self.porta)
        while True:
            connessione, indirizzo = self.servizio.accept()
            print("Ping ricevuto da ", indirizzo)
            messaggio = connessione.recv(1024).decode()
            if messaggio == "ping":
                connessione.send("pong".encode())
            connessione.close()    


if __name__ == "__main__":
    server = PingServer()
    server.start()

Il client

Il client risulta essere leggermente diverso. Qui infatti non devo creare un bind al socket aperto dal client ma mi collego direttamente al socket del server per eseguire le restanti operazioni di scambio messaggi. Ci sono più modi per farlo. Qualche libro riporta il socket come se fosse una sorta di file su cui scriviamo/leggiamo i dati. Oppure usiamo una modalità analoga al nostro server. Al nostro client dobbiamo comunque segnalre a che tipo di socket collegarsi e su quale indirizzo ip/host e porta sia in attesa.

A questo punto posso agire con send() e recv() e le rispettive funzioni di decodifica/codifica.

import socket

class PingClient:
    def __init__(self, _host="127.0.0.1", _porta=5000):
        self.host = _host
        self.porta = _porta
        self.servizio =  socket.socket(socket.AF_INET, socket.SOCK_STREAM) 

    def ping(self):          
            self.servizio.connect((self.host, self.porta))
            self.servizio.send("ping".encode())
            messaggio = self.servizio.recv(1024).decode()
            print("Risposta dal server: ",messaggio)
            self.servizio.close() 


if __name__ == "__main__":
    client = PingClient()
    client.ping()

Per completezza, riscriviamo seguendo la proposta utilizzando il socket come flusso file con la parola riservata with. Tralasciamo di commentare nuovamente. Il with

class ClientPing:
    def __init__(self, _host = "127.0.0.1", _porta=5000):
        self.host = _host
        self.porta = _porta
        self.servizio = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
   def ping(self):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((self.host, self.porta))
            s.send("ping".encode())
            messaggio = s.recv(1024).decode()
            print("Risposta del server ", messaggio)

if __name__ == "__main__":
    client = ClientPing()
    client.ping()            

Migliorie

Quello proposto è un esempio “da battaglia” per introdurre l’argomento. Il codice può senza dubbio essere migliorato, anche semplicemente introducendo le eccezioni. Stesso discorso vale per la gestione dei messaggi e la loro eventuale immissione da tastiera o inserimento in input sempre lato client dall’indirizzo/host a cui inviare il messaggio. Altro esercizio carino è conteggiare il tempo di risposta del server dal punto di vista del client e magari stamparlo a video iterando il ping 4/5 volte o all’infinito ad ogni avvio del client. Altra personalizzazione, lato server, è l’inserimento di un arresto del loop di ascolto magari con l’immissione di un tasto della tastiera.

Ultima modifica 21 Settembre 2025