Try   HackMD

UNIPI RCL: Relazione Winsome

studente: Claudio Candelori
matricola: 549710
corso: A
anno: Ottobre 2022

Struttura del codice:

  • SERVER:
    • ServerMain.java: Inizializza tutte le strutture e le funzionalita' del server, dopodiche' lancia tutti i thread necessari a far funzionare Winsome. Rimane poi in ascolto di un eventuale comando per la chiusura del server e, in caso, fa partire il cleanup.
    • Manager.java: Thread "maggiordomo" che si occupa di ricevere tutte le nuove connessioni dai client e li assegna uno per uno ad un nuovo thread Worker.
    • Worker.java: Ai thread 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.java: Thread che periodicamente si occupa di eseguire il backup delle strutture dati in formato Json.
    • Rewarder.java: Thread che periodicamente controlla tutti i post e ricompensa in Wincoin tutti gli autori e i curatori dei post idonei, ovvero quelli che hanno ricevuto interazioni nell'ultimo ciclo di ricompense.
    • Config.java: Classe che si occupa della lettura del file di configurazione del server e ne ospita tutti i parametri letti.
    • Register.java: Implementazione dell'omonima interfaccia RMI esposta per fornire il servizio di registrazione dei nuovi utenti.
    • FollowerServer.java: Implementazione dell'omonima interfaccia RMI esposta per fornire il servizio di notifica dei nuovi followers.
    • SocialNetwork.java: Classe che ospita le due principali strutture dati del server, ovvero, una map degli utenti e una map dei post, e alcuni metodi relativi come ad esempio quelli che vengono invocati dal Backuper per il salvataggio in Json.
    • Posts.java: Oggetto contenente tutte le informazioni di un singolo post: titolo, autore, contenuto, liste dei like/dislike e dei commenti, tutto il necessario per implementare i Rewin e strutture di supporto per il calcolo delle ricompense dello stesso post.
    • Comment.java: Un commento e' una semplice coppia utente/contenuto del commento.
    • Users.java: Oggetto che rappresenta per intero un singolo utente di Winsome. Contiene al suo interno tutte le strutture che lo riguardano come le liste di seguiti e di followers, e il proprio portafoglio.
    • Wallet.java: Contiene semplicemente il valore corrente del portafoglio e la lista di transazioni effettuate, oltre ai metodi per visualizzarne il valore e la conversione in Bitcoin
    • Transaction.java: Rappresenta una ricompensa in Wincoin, contiene un timestamp del momento in cui e' avvenuta la transazione.
  • CLIENT:
    • Client.java: Il client all'avvio si collega al server Winsome e a tutti i servizi extra (RMI, UDP Multicast, ). Dopo di che si occupa cicliclamente di prendere i comandi dal terminale, inviarli al server e visualizzarne l'esito.
    • FollowersClient.java: Implementazione dell'omonima interfaccia di RMI Callback esposta al server durante la registrazione al servizio di notifiche followers. Notifica l'utente loggato di qualsiasi cambiamento nella propria lista di followers.
    • RewardListener.java: Thread che si occupa di ascoltare sull'indirizzo multicast e ricevere dal server una notifica che indica che le ricompense in Wincoin sono arrivate.
    • Config.java: Classe che si occupa della lettura del file di configurazione del client e ne ospita tutti i parametri letti. E' molto simile all'omonima classe Config del server.
  • SHARED:
    • RegisterInterface.java
    • FollowersServer.java
    • FollowersClient.java

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Server:

  • 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.

Client:

  • 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.

Servizi:

  • La registrazione di un nuovo utente avviene sfruttando un interfaccia RMI esposta dal server e pubblicata come servizio (di default chiamato 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.
  • In maniera simile funziona il servizio di notifica dei nuovi followers: il server espone al client un interfaccia RMI (sul registry di default 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 followers

Istruzioni compilazione/esecuzione:

I 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:

User:   Pw:  Tags:
kirk    k    [enterprise, startrek, earth]
spock   s    [enterprise, startrek, logic, vulcano]
picard  p    [enterprise, earlgray, wine, earth]
worf    w    [enterprise, battle, klingons]
scotty  s    [enterprise, startrek, teleports, earth]

Commenti finali:

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.