studente: Claudio Candelori
matricola: 549710
corso: A
anno: Ottobre 2022
Worker
.Worker
e' assegnato il compito di rimanere in contatto con i client fino alla loro chiusura, e di eseguire qualsiasi comando valido gli venga chiesto, fornendo poi ai client un messaggio di risposta che ne indica il successo oppure le motivazioni del fallimento.Backuper
per il salvataggio in Json.Il server utilizza il paradigma manager-worker per la gestione delle connessioni con i clienti. I worker thread appartengono ad un threadpool controllato dal thread manager. Ad ogni nuova richiesta di connessione il manager fa partire un worker dandogli come parametro la socket del nuovo client, cosi' che potra' interagire direttamente con il client in autonomia, fino a che il client non si disconnettera' o il server non decida di chiudere forzatamente la connessione con tutti i client.
La comunicazione tra workers e clients avviene tramite una connessione TCP persistente, grazie alle socket delle classi Socket
e ServerSocket
e i relativi stream di Java IO. Pur essendo meno efficiente di NIO, l'utilizzo di Java IO e' sicuramente piu' intuitivo e meno error-prone della controparte. Un'altra criticita' di Java IO e' che i metodi di ascolto (read()
,readLine()
) sono bloccanti. In ogni caso il server e il client gestiscono adeguatamente le interrupt()
. In certi casi, tipo questo, ho preferito dare importanza alla leggibilita' e alla chiarezza del codice piuttosto che all'efficienza pura.
Il threadpool usato nell'implementazione attuale usa l'implementazione di default newCachedThreadPool
, cio' significa che se molti client si connettessero tutti nello stesso istante ad ognuno verrebbe assegnato un nuovo thread che rimarra' in ascolto solo per lui fino alla disconnessione. Con molti utenti questo innescherebbe frequenti context-switch che potrebbero rallentare il server, ma per natura del social network la maggiorparte del tempo i thread la passano bloccati sulla readLine()
in attesa di un comando. Quindi limitare il numero di thread con la corrente struttura del progetto sarebbe controproducente in quanto rischierebbe di far attendere tutti i clienti che arrivano dopo la saturazione del fixed threadpool anche per lunghi periodi.
I thread worker svolgono la maggiorparte del lavoro lato server, rimangono in contatto fino alla loro disconnessione con i client in attesa di comandi e li passano ad una funzione interna chiamata handleCmd()
. Questa funzione effettua il parsing della stringa e ne individua il comando. Ovviamente, non considerando la registrazione che puo' avvenire una tantum, il primo comando da eseguire per accedere a tutti i comandi disponibili, dovrebbe essere la login
. Il campo usr
contiene un riferimento all'utente loggato. I comandi vengono identificati ed eseguiti dentro ad uno switch-case piuttosto corposo, per ogni comando effettua i controlli e se tutto e' in regola lo esegue modificando la struttura dati adeguatamente, e producendo una stringa contenente l'esito dell'operazione richiesta o i dati che ha richiesto di visualizzare, che viene subito dopo restituita al client.
Sia nel client che nel server per ovviare in parte al problema dell'inefficienza di Java IO si fa ampio uso dei wrapper BufferedReader
,PrintWriter
e BufferedOutputStream
. Questi wrapper forniscono una bufferizzazione agli stream Java IO che li rende piu' efficienti degli stream base. Il metodo readLine()
puo' essere usato per leggere una linea in modo bufferizzato, il metodo println()
esposto da PrintWriter
puo' essere usato per scrivere sulla socket, cosi' si e' sempre sicuri che la readLine()
non si blocchera aspettando un carattere terminatore.
Il database di Winsome (SocialNetwork.java
) e' stato diviso in due strutture dati principali: utenti e post. Entrambi sono ospitati da una ConcurrentHashMap
. La progettazione delle classi che compongono il database ovvero Post
, Comment
, User
, Wallet
, Transaction
dovrebbe essere coerente con i principi OOP per cui ogni oggetto contiene al suo interno tutti i campi che servono a rappresentarlo, e tutti i metodi necessari alle operazioni che lo riguardano.
Per mantenere la concorrenza non solo delle strutture dati principali ma anche delle str. dati sottostanti ad ogni post ed ogni utente (come ad esempio la lista di like, o la lista di followers di un utente), anch'esse vengono tutte istanziate con strutture dati concorrenti e thread safe: ConcurrentHashMap.newKeySet()
per i set di utenti e ConcurrentLinkedDeque
per le liste di commenti. Le hash map e i set vengono utilizzati per qualsiasi cosa debba essere memorizzata senza ripetizioni, ad esempio ad ogni username deve corrispondere un solo utente ed ogni utente puo' stare solo una volta nella lista degli upvotes. La coda invece e' usata per tutto cio' che ammette ripetizioni, ad esempio possono esserci piu' commenti di uno stesso utente sotto ad un posts.
Inoltre, Sempre per garantire la thread safety, tutti gli eventuali contatori delle classi che mantengono i dati usano tipi atomici (AtomicInteger
). L'esempio principale di uso e' la variabile postId
che ad ogni nuovo post o rewin viene letta ed incrementata atomicamente. Cio' garantisce l'unicita' assoluta di ogni postId
e conseguentemente l'inserzione nella mappa (che ha come chiave proprio l'id) non puo' fallire.
Alcuni campi e strutture maggiormente utilizzati per semplicita' sono stati dichiarati static
e quindi sono accessibili da tutte le classi e thread del package in cui si trovano senza doverli passare nei costruttori. Tutti i parametri dei config e le strutture che mantengono i dati ne sono un esempio.
Gli ultimi due componenti del server degni di nota sono due thread di supporto, entrambi si attivano solo periodicamente secondo quanto specificato nel config file e sono bloccati su una sleep()
per la maggiorparte del tempo. Il thread Backuper
si occupa di effettuare periodicamente il backup delle strutture dati su due file Json (uno per gli utenti, uno per i post). Il codice di quest'ultimo e' molto semplice dato che le funzioni per eseguire il backup sono contenute nella classe SocialNetwork
. Il thread Rewarder
invece quando si attiva si occupa di visitare ogni post di Winsome per chiamare la funzione rewardPost()
che assegna le ricompense ad autore e curatori esattamente come indicato nella specifica di progetto. Inoltre si occupa di inviare una notifica di quanto avvenuto su indirizzo multicast a tutti i client connessi.
La struttura del client Winsome e' molto piu' breve e lineare di quella del server. Deve semplicemente inizializzare/connettersi a tutti i servizi aggiuntivi e connettersi al server. Se questo non e' gia' attivo, ritenta alcune volte a connettersi prima di chiudersi. Il ciclo principale del client e' quello in cui il client recupera un comando dal terminale e lo invia. Ma prima ne effettua un primo parsing, per far si che i comandi che sfruttano servizi alternativi non vengano inviati (es: register
e list followers
come da specifica non devono essere eseguiti dal server). Il client non effettua controlli di correttezza che vengono delegati interamente al server.
Il client inoltre possiede una classe Config
analoga a quella del server ma con meno parametri, espone uno stub di FollowersClient
per ricevere callback riguardanti i nuovi follower e lancia un unico thread RewardListener
che rimane in ascolto delle notifiche multicast riguardanti le ricompense ed eventualmente aggiorna la lista locale.
register
). Il client puo' quindi invocare da remoto la funzione server register(username, password, tags)
e se il nome utente non e' gia' stato preso, la registrazione ha successo, e la funzione restituisce al client una string contenente l'esito.followers
) con dei metodi per attivare e disattivare le notifiche, che vanno invocati rispettivamente dopo login/logout ma solo se andati a buon fine, ed un metodo che permette al client di recuperare la lista di followers (getCurrFollowers()
). Concettualmente pero' non vogliamo che il client chieda ogni volta la lista aggiornata, ma bensi' che venga informato di un eventuale follow o unfollow e che tenga aggiornata la propria lista localmente. Pertanto anche la getCurrFollowers()
verra' invocata solamente una volta dopo un login andato a buon fine. Il client espone a sua volta un'interfaccia RMI di callback esponendo il metodo followerNotification()
che il server usera' per far arrivare la notifica solamente al client interessato. Il client per registrarsi al servizio invoca remotamente la funzione activateFollowersUpdates(username, stub)
con la quale passa un riferimento al server dello stub sul quale deve essere contattato. Il server memorizza queste informazioni in una Map concorrente, e quindi fino al logout ha sempre un riferimento per notificare il client riguardo ai cambiamenti nella lista di followersI comandi vanno avviati dalla cartella del progetto "Winsome", quella che contiene tutte le altre cartelle del progetto (client, server, shared, … ). Con questi comandi i file delle classi compilate andranno direttamente nella cartella "bin". I seguenti comandi funzionano in ambiente Windows usando la powershell:
Compilazione Client: javac ./client/*.java -d bin
Esecuzione Client: java -cp bin client.ClientMain
Compilazione Server: javac -cp ".;./lib/gson-2.9.0.jar" ./server/*.java -d ./bin
Esecuzione Server: java -cp ".;./bin;./lib/gson-2.9.0.jar" server.ServerMain
I file Json contengono gia' un esempio di esecuzione di Winsome con i personaggi di star trek e tutte le funzionalita' testate, per resettare il social network basta semplicemente eliminare i file Json e lo stato ripartira' da 0. Se si vuole accedere con uno degli utenti del mockup hanno tutti come password l'iniziale del proprio nome:
Ho sviluppato il progetto con VSCodium su Windows ma il codice dovrebbe essere compilabile senza effetti collaterali anche su altri sistemi operativi, avendo avuto l'accortezza di usare codice non OS-dependant (es: System.lineSeparator()
invece che \r\n
) anche se in certi casi produce codice piu' corposo.
Il naming di tutte le variabili e' stato pensato per essere il piu' leggibile possibile pertanto molti passaggi e metodi semplici come i getters sono auto-esplicativi.
Il codice e' commentato in inglese nei punti critici, e quasi tutti i metodi sono provvisti di documentazione Javadoc, per tanto al passaggio del mouse si puo' leggere una descrizione rapida del funzionamento.