Server/Client socket TODO in Python

Un classico esercizio che anche qui sul sito abbiamo visto in molti linguaggi e tecnologie. Questa volta lo affrontiamo in python utilizzando socket e dinamica client/server. Non teniamo conto di multi client con concorrenza ma vogliamo realizzare un client capace di inviare ad un server una sorta di lista delle cose da fare specificando un testo/descrizione, una tipologia prefissata ed una durata numerica in ore. Il server è capace di archiviare la lista ed eseguire le operazioni di cancellazione, visualizzazione, individuare il task con durata maggiore, minore o una media dei valori archiviati.

La classe Task

Gli esercizi precedenti, come Server/Client prenotazione in python o Server/Client Ping Pong in Python erano piuttosto semplici. Al di la dello scambio di messaggi client/server i dati e comandi inviati erano semplici e le informazioni archiviate riconducibili a dati primitivi (un singolo testo, piuttosto che un valore intero per i posti). Qui facciamo un passetto in più perché per ogni dato gestito dal server, abbiamo bisogno di un contenitore più strutturato come una classe. Proponiamo quindo la classe Task che conterrà descrizione. tipologia, durata e un timestamp per annotare quando tale task è stato creato. Per semplicità abbiamo scritto i metodi set e un metodo per tornare una stringa completa di tutti i gli attributi di un oggetto Task.

from Tipologia import Tipologia
import time

class Task:
    def __init__(self, _descrizione: str = '', _tipologia: Tipologia = Tipologia.CASA, _durata: int = 0):
        self.descrizione = _descrizione
        self.tipologia = _tipologia
        self.durata = _durata
        self.timestamp = time.time()

    def setDescrizione(self, _descrizione: str) ->None:
        self.descrizione = _descrizione

    def setTipologia(self, _tipologia: Tipologia) ->None:
        self.descrizione = _tipologia   

    def setDurata(self, _durata: int) ->None:
        self.durata = _durata

    def toString(self) ->str:
        return self.descrizione + " " + self.tipologia + " " + str(self.durata) + " " + str(self.timestamp)

La tipologia poi la possiamo gestire come stringa semplice, oppure utilizzare un classico enum che in realtà in python si scrivi comunque con una particolare classe

from enum import Enum

class Tipologia(Enum):
    CASA = 1
    LAVORO = 2
    HOBBY = 3

Il server

A questo punto siamo pronti a scrivere il nostro server. Tra i suoi attributi spicca il dizionario che conterrà un indice numerico progressivo e il relativo oggetto Task.

Tra i metodi, definiamo un metodo toString() che altro non fa che restituire una stringa composta dall’ id numerico progressivo dell’elenco più le informazioni contenute all’interno del Task stesso

    def toString(self) -> str:
        stampa = ''
        for id,task in self.elenco.items():
            stampa += str(id) +" " +task.toString()
        return stampa

A differenza degli esercizi citati, per capire quale comando viene inviato dal client facciamo un ulteriore variazione possibile. Prelevo una sottostriga da sinistra di 4 caratteri, lunghezza massima del comando LIST e QUIT, mentre gli altri ADD, DEL, MIN MAX, AVG sono tutti lunghi tre caratteri. Se la sosttostringa presenta quindi uno spazio, si tratterà sicuramente di un comando da tre lettere e mi basta eliminare lo spazio vuoto.

 comando = messaggio[:4].strip() #prendo i primi 4 caratteri e al limite tolgo lo spazio vuoto se sono 3

Poi quindi procedo con il solito costrutto match per eseguire le richieste di routine e rispondere al client. Non commentiamo più di tanto le operazioni per individuare min, max e media avg perché consistono banalmente nei meccanismi base di scorrere l’intero dizionario, estrapolare il task e la sua durata per poter individuare o sommare per la richiesta.

import socket
from Task import Task

class ServerTask:
    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)
        self.id = 0  #id progressivo dei task
        self.elenco = {} #dizionario dove aggiungere i task
    

    def toString(self) -> str:
        stampa = ''
        for id,task in self.elenco.items():
            stampa += str(id) +" " +task.toString()
        return stampa
        
    def start(self) -> None:
        self.servizio.bind((self.host,self.porta))
        self.servizio.listen(1)
        print("Servizio attivo su ", self.host, self.porta)

        while True:
            connessione, indirizzo = self.servizio.accept()
            print("Connessione ricevuta da ", indirizzo)
            messaggio = connessione.recv(1024).decode()
            
            comando = messaggio[:4].strip() #prendo i primi 4 caratteri e al limite tolgo lo spazio vuoto se sono 3
            
            match comando:
                case "ADD":
                    comando, testo, tipologia, giorni = messaggio.split(" ",4)
                    self.id += 1
                    task = Task(testo,tipologia, int(giorni))
                    self.elenco[self.id] = task #aggiungo un nuovo task
                    risposta = "OK:" + str(self.id) + " ts="+ str(task.timestamp)
                    connessione.send(risposta.encode())

                case "DEL":
                    comando, id = messaggio.split(" ",2)
                    if self.elenco[id]: 
                        del self.elenco[id]
                        connessione.send("OK".encode())
                    else:
                        connessione.send("ERR non trovato".encode())

                case "LIST":
                    lista = self.toString()
                    connessione.send(lista.encode())

                case "MIN":
                    giorno_min = -1  #o metti un numero enorme o forzi il primo giro
                    for id,task in self.elenco.items():
                        if task.durata < giorno_min or giorno_min == -1:
                            giorno_min = task.durata
                    
                    connessione.send(str(giorno_min).encode())
                    
                case "MAX":
                    giorno_max= 0
                    for id,task in self.elenco.items():
                        if task.durata > giorno_max:
                            giorno_max = task.durata
                    
                    connessione.send(str(giorno_max).encode())

                case "AVG":
                    somma_giorni= 0
                    for id,task in self.elenco.items():
                        somma_giorni += task.durata

                    avg = somma_giorni / len(self.elenco)
                    
                    connessione.send(str(avg).encode())

                case "QUIT":
                    print("Connessione in chiusura...")
                    connessione.close()
                    exit()
                        
                case "_":  # restituisce un errore
                    pass


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

Ovviamenete è fortemente migliorabile, valutando opportunamente la correttezza dei dati inseriti, dell’enum.

Il client

In realtà il client assomiglia a tutti gli esercizi che abbiamo visto sulla gestione dei socket python. Si limita a chiedere all’utente una lista comando ed inviarla al server. Non facciamo particolari controlli di corretta e valida immissione del comando e dei suoi parametri. Tale funzionalità per il momento non la implementiamo se non in parte sul server.

import socket

class ClientTask:
    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 start(self) -> None:
        print("Comandi ADD/DEL/LIST/MIN/MAX/AVG/QUIT")
        print("es. ADD pippo CASA 1, DEL 4, LIST, QUIT")

        comando = ""
        while True and comando != "QUIT":
            self.servizio = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.servizio.connect((self.host,self.porta))
            comando = input(">")    
            self.servizio.send(comando.encode())
            risposta = self.servizio.recv(1024).decode()            
            print(risposta)
        
        self.servizio.close()
        print("Connesione chiusa")

if __name__ == "__main__":
    client = ClientTask()
    client.start()

Ultima modifica 30 Ottobre 2025