Il seguente progetto è un sistema client-server di posta elettronica sviluppato in Java per il corso di Programmazione III presso l'Università degli Studi di Torino.
L'applicativo, dotato di un'interfaccia grafica progettata in JavaFX, gestisce lo smistamento dei messaggi e le diverse caselle postali in maniera concorrente e thread-safe. Tutta la comunicazione di rete è gestita a basso livello tramite Socket TCP.
Il progetto si compone di due applicativi distinti: un client desktop reattivo per l'utente finale e un server multi-thread dotato di pannello di controllo UI per l'amministrazione e il live logging degli eventi.
L'implementazione si concentra sulle seguenti funzionalità principali:
Durante la fase di login il client invia una richiesta TCP al server, che verifica l'esistenza dell'indirizzo email inserito. Se il server non è raggiungibile o l'email non è registrata, l'utente riceve un messaggio di errore apposito senza che l'applicazione smetta di funzionare.
Nota: Trattandosi di un mock didattico, per semplicità, l'autenticazione è limitata al solo inserimento dell'indirizzo email e non richiede alcuna password.
Per ridurre il carico sul server, la connessione è stateless: il client invia una nuova richiesta TCP quando necessario per poi liberare le risorse del server.
Il processo di invio prevede un sistema di validazione a due livelli per ottimizzare il traffico di rete.
Il Client esegue prima una validazione sintattica tramite Regex per filtrare input malformati; successivamente, il Server intercetta la richiesta, verifica l'effettiva esistenza dei destinatari e instrada l'email nelle rispettive caselle garantendo la thread-safety.
Un sistema di background task sul client riproduce notifiche visive e sonore all'arrivo di nuovi messaggi.
L'applicativo supporta le funzioni di inoltro, risposta singola e risposta a tutti, oltre all'eliminazione delle email nella casella.
L'interfaccia grafica gestisce automaticamente il parsing del messaggio originale e la pre-compilazione dinamica dei form, riducendo l'attrito per l'utente e simulando l'esperienza di un vero client di posta elettronica.
Il client implementa un sistema di polling asincrono. Se la connessione cade, l'applicazione non va in crash: la UI notifica la disconnessione e un thread in background continua a tentare la riconnessione in modo trasparente, ripristinando lo stato appena il server torna online.
Lato server, un sistema di live logging visivo mappa e formatta in tempo reale tutte le richieste TCP e gli errori, agevolando il monitoraggio del traffico e il debugging.
Di seguito un estratto del codice che mostra la gestione del meccanismo di polling tramite ScheduledExecutorService e della perdita di connessione tramite la property isConnectionOnline.
// Listener reattivo allo stato dell'utente
currentUser.addListener((_, _, newValue) -> {
if(newValue != null) {
// Polling asincrono su ThreadPool dedicata
pollingTask = connectionExec.scheduleAtFixedRate(
() -> syncInbox(newValue), 0, 5, TimeUnit.SECONDS);
} else if(pollingTask != null) {
pollingTask.cancel(true);
}
});
// Intercettazione errori senza bloccare il thread grafico
catch(ConnectException e) {
Platform.runLater(() -> {
notificaUtente.set(new Notification(NotificationType.ERROR,
"Connessione col server persa. Tentativo di riconessione..."));
isConnectionOnline.set(false);
});
}
Di seguito alcuni dettagli implementativi sulle logiche di rete e gestione della concorrenza implementate per garantire affidabilità e thread-safety.
synchronized public void updateInbox() {
String user = emailAddr.split("@")[0];
// Assegnazione di un lock univoco per utente (Fairness = true)
ReentrantReadWriteLock rwLock = fileLocks.computeIfAbsent(user,
k -> new ReentrantReadWriteLock(true));
Lock writeLock = rwLock.writeLock();
writeLock.lock(); // Inizio sezione critica
try (FileWriter writer = new FileWriter("data/" + user + ".json")) {
gson.toJson(incomingEmails, writer);
} catch (Exception e) {
throw new RuntimeException("Errore critico I/O: " + e.getMessage());
} finally {
writeLock.unlock(); // Rilascio garantito
}
}
Per compensare l'assenza di un DBMS relazionale (richiesta esplicita della consegna), si è implementato un sistema di concorrenza manuale.
Utilizzando una ConcurrentHashMap con computeIfAbsent, ci si assicura che ogni casella postale abbia il proprio lock univoco.
L'uso di ReentrantReadWriteLock(true) garantisce equità ed evita starvation tra i thread concorrenti, liberando sempre in sicurezza le risorse fisiche tramite il blocco try-finally.
// Il lavoro di accept viene isolato su un SingleThreadExecutor demone
singleExec.execute(() -> {
try {
while (!serverSocket.isClosed()) {
Socket incoming = serverSocket.accept();
// La computazione viene delegata a un FixedThreadPool
// per evitare di saturare la memoria con thread infiniti
Runnable task = new ThreadedRequestHandler(
incoming, casellePostali, logger, idCounter);
exec.execute(task);
}
} catch (Exception e) {
// Se il socket viene chiuso volontariamente, si sopprime l'errore
if (serverSocket != null && serverSocket.isClosed()) return;
Platform.runLater(() -> onError.accept("Errore socket: " + e.getMessage()));
}
});
L'architettura di rete è divisa in due strati per massimizzare le prestazioni. Un singolo thread demone è dedicato unicamente all'accettazione delle richieste TCP (serverSocket.accept()), impedendo il blocco dell'interfaccia UI. Il processing vero e proprio del payload JSON e l'I/O su disco vengono invece instradati su una FixedThreadPool dedicata, dimensionata per prevenire esaurimenti di risorse sotto stress.