make
./server -?
./client -?
Questo progetto fa parte dell'esame Reti di Calcolatori della facoltà di Sicurezza Informatica della Statale di Milano.
Il progetto consiste nella implementazione di un modello client server sviluppato interamente in C per scambi di messaggi testuali tra più utenti in diverse stanze.
Client e server utilizzano socket TCP per comunicare tra loro.
Il server gestisce più client utilizzando un modello multi-thread chiamando la subroutine handleClient in un thread separato.
Dopodiché si mette in ascolto per connessioni in entrata all'indirizzo e porta specificati nelle opzioni, o su quelle di default.
Usage: server \[-?\] \[-h IP\] \[-p PORT\] \[\--host=IP\]\[\--port=PORT\]
Il client in multithread gestisce l'invio e la ricezione dei messaggi.
All'avvio del client l'utente tenta la connessione al server specificato
nelle opzioni.
Se la connessione va a buon fine il server crea un utente con uid
univoco e username a scelta e lo inserisce nella stanza principale
"General".
Usage: client \[-?\] \[-d DOMAIN\] \[-h IP\] \[-p PORT\]\[\--domain=DOMAIN\] \[\--host=IP\] \[\--port=PORT\]
Il server crea una socket TCP e ci assegna l'interfaccia e la porta su
cui ascolterà.
Di default indirizzo e porta vengono assegnati tramite delle define
SERVER_HOST e SERVER_PORT presenti in "server.h" altrimenti
vengono assegnati ai valori specificati come opzioni ---host e
---port gestite dal parser tramite la funzione parserArgv definita
in "parser.h".
Tramite setsockopt vengono settate le opzioni della socket per la dimensione del buffer di invio SO_SNDBUF e di ricezione SO_RCVBUF.
Eseguo la bind della socket all'indirizzo e porta specificati e la preparo ad accettare connessioni tramite listen.
accept aspetta una connessione su server_sd e quando la riceve apre una nuova socket, client_sd e assegna a client_addr l'indirizzo del client che si sta connettendo.
Il server crea un nuovo utente assegnandogli l'indirizzo della connessione ricevuta e la socket corrispondente, dopodiché crea un nuovo thread chiamando la funzione handleClient che si occupa della gestione dei messaggi, interpreta i comandi e inoltra i messaggi agli utenti interessati.
Come nel server il client crea una socket TCP e ci assegna l'indirizzo e la porta a cui dovrà connettersi. Di default indirizzo e porta vengono assegnati tramite delle define CONNECTION_HOST e CONNECTION_PORT presenti in "client.h" altrimenti vengono assegnati ai valori specificati come opzioni ---host ( o ---domain che verrà tradotto in un host tramite resolveHostname ) e ---port gestite dal parser tramite la funzione parserArgv definita in "parser.h".
Tramite setsockopt vengono settate le opzioni della socket per la dimensione del buffer di invio SO_SNDBUF e di ricezione SO_RCVBUF
Il client apre una connessione verso il server.
Il server si aspetta di ricevere un username per creare l'utente. Tramite la funzione getCurrentStdin e con gli opportuni controlli di lunghezza dell'input preleviamo l'username da stdin e con send lo mandiamo al server
il client lancia due thread diversi per l'invio e la ricezione dei messaggi in modo da poter gestire messaggi in arrivo durante l'invio di un messaggio. Il thread di ricezione termina solo se il client riceve 0 byte da una lettura, evento che succede solo se il server crasha o chiude la socket del client.
Tramite una pthread_join si attende la terminazione del thread di ricezione, dopodiché si interrompe l'esecuzione del programma, l'utente chiude la socket ed esce dal server.
All'avvio il server inizializza la lista delle stanze inserendo una stanza "General" e un utente "Admin" come amministratore della stanza di default.
roomList è un nodo sentinella che contiene il numero di stanze presenti nel server, due puntatori alla testa e alla coda della lista e un mutex per consentire l'accesso alla lista delle stanze in mutua esclusione tra i vari thread, per evitare race conditions.
Dopo l'inizializzazione la testa della lista punterà alla stanza "General" che verrà usata come stanza di default, inserita nella lista con la funzione addRoom.
Ogni stanza ha un uid univoco, un nome, un proprietario (l'utente che crea la stanza), una lista di utenti, il nome del file di log da cui recuperare la cronologia dei messaggi (verrà inizializzato più avanti come "(room->name).log" e puntatori alla stanza precedente e successiva nella lista.
Alla creazione di una stanza la sua lista utenti viene inizializzata, allocando memoria, tramite la funzione initializeUserList
user_list è un nodo sentinella che contiene il numero di utenti presenti nella stanza, due puntatori alla testa e alla coda della lista e un mutex per consentire l'accesso alla lista degli utenti della stanza in mutua esclusione tra i vari thread, per evitare race conditions.
Ogni utente un uid univoco, un username, un colore con cui verrà visualizzato dagli altri utenti (inizializzato random tra una lista di colori durante la creazione dell'utente), un socket file descriptor, una struttura sockaddr_in contenente l'indirizzo del client corrispondente, un puntatore alla stanza a cui appartiene e puntatori all'utente precedente e successivo nella lista.
Il server permette un certo numero di utenti, definito in MAX_CLIENT_COUNT.
Alla connessione di un nuovo client, se il numero di utenti collegati o
che stanno tentando l'accesso supera MAX_CLIENT_COUNT, nega
l'accesso e chiude la socket.
Il client, non ricevendo la conferma di connessione, saprà che il server
è pieno e terminerà l'esecuzione stampando un messaggio di errore.
All'accesso l'utente viene inserito nella stanza "General".
La funzione sendMessage prende in input una stringa e un utente e tramite la funzione send presente in "socket.h" prova ad inviarlo al socket file descriptor dell'utente.
La funzione sendBroadcastMessage chiama sendMessage su tutti gli utenti della stanza dell'utente che invia il messaggio, tranne che a sé stesso, e tramite keepLog salva il messaggio nel file di log della stanza.
Con il comando /restore l'utente può ricevere dal server l'elenco di tutti i messaggi mandati in quella stanza prima che vi entrasse.
Con il comando /rooms l'utente può stampare la lista di stanza disponibili con i vari utenti connessi. La propria stanza e il proprio username verranno evidenziati nel messaggio.
Con il comando /newroom l'utente può creare una nuova stanza di cui sarà proprietario, e che quindi solo lui potrà eliminare.
Il server accetta un massimo numero di stanze definito in MAX_ROOM_COUNT.
Alla creazione di una nuova stanza da parte di un utente il
cleanEmptyRooms controlla il numero di stanze già presenti e se è
superiore cerca una stanza vuota da eliminare.
Se il server ha già raggiunto il numero di stanze massimo tutte
contengono almeno un utente non sarà possibile creare una nuova stanza.
L'utente inoltre può scegliere una delle stanze esistenti create da altri utenti con il comando /changeroom tramite l'uid univoco della stanza. Al client verrà chiesto di inserire il numero uid della stanza in cui vuole entrare, dopo avergli mostrato la lista completa delle stanze disponibili. Il server interpreta il messaggio ricevuto controllando che sia un numero valido e che corrisponda ad una stanza esistente. L'utente, quindi, verrà rimosso dalla stanza a cui appartiene, notificando gli altri utenti della cosa, e verrà inserito nella stanza da lui scelta.
Il proprietario di una stanza può decidere di eliminarla con il comando /deleteroom e di spostare automaticamente tutti gli utenti connessi nella stanza generale.
L'admin può eliminare ogni stanza. Nessuno, compreso l'admin, può eliminare la stanza di default.
Tramite il comando /exit (o se il client viene interrotto mediante segnali di SIGINT, SIGTSTP o SIGQUIT), l'utente viene disconnesso dal server. Durante la fase di logOut viene rimosso dalla stanza in cui è attualmente e la proprietà delle stanze che possiede verrà trasferita all'utente Admin