Applicazioni Multi-User

 

Un'applicazione multi-user è molto simile ad una tipica applicazione client/server, con la sola differenza che le informazioni fluiscono da un client a tutti gli altri passando attraverso il server. Al contrario, in una applicazione client/server, le informazioni vanno dal client al server e da qui di nuovo al client.

Tipici esempi di applicazioni multi-user sono i giochi in rete, in cui più client (giocatori) si collegano ad un server che si occupa di creare il mondo virtuale in cui far interagire i partecipanti.

E' chiaro che in un modello come questo, tutti gli utenti hanno la necessità di vedere (sotto forma di testo o di grafica) l'evolversi dello 'stato' degli altri client.

Altri esempi molto simili sono gli ambienti di realta virtuale, in cui ogni client si 'mostra' agli altri tramite un 'avatar', rappresentazione virtuale di se stessi. Il modello non cambia, anche qui vi è la necessità di segnalare ad ogni client l'evoluzione dello stato degli altri.

Infine ci sono i programmi di chat. In questo caso non è tanto lo stato di un client ad essere trasmesso agli altri client, ma dei messaggi. Le uniche informazioni di stato che vengono segnalate sono quelle di ingresso e di abbandono dell'ambiente virtuale.

 

Una chat multi-protocollo

 

L'idea è quella di realizzare una chat nella quale i client comunicano attraverso protocolli differenti con un unico server.

Per raggiungere questo obiettivo è necessario che i metodi del client invocati dal server siano dichiarati in una unica interfaccia. Ogni qualvolta si vorrà realizzare un client diverso, sarà sufficiente implementare questa interfaccia definendo quindi i metodi in essa dichiarati.

 

Socket

 

Nella realizzazione tramite socket è necessario prevedere delle operazioni di trasformazione per inviare i messaggi. E’ necessario che client e server sappiano in che modo è costruito il messaggio per avere la possibilità di inviare messaggi di lunghezza variabile. La tecnica usata è di inviare inizialmente la lunghezza di tutto il messaggio con un messaggio di lunghezza fissa, e poi il messaggio vero e proprio la cui lunghezza è questo punto nota.

Una volta creata l'applicazione chat, cioè il generico server e l'interfaccia dei client, è necessario creare un server che accetta connessioni dagli utenti che vogliono comunicare tramite socket.

Il ruolo di questo server è di rimanere in attesa di una connessione su socket; una volta giunta una richiesta, crea un apposito client, passandogli come argomenti il server principale e l'handle della socket, e lo avvia in un thread.

Questo client ha due ruoli. Il primo è di porsi in attesa di messaggi di input sulla socket, provenienti  dall'utente, per reindirizzarli, dopo un'opportuna conversione, al server principale.

L'altro compito è quello di definire i metodi dell'interfaccia client in modo tale che ad ogni chiamata del server, venga fatta l'opportuna traslazione del messaggio ricevuto per poterlo convogliare nella socket. A questo punto interviene un 'socket reader' che si occupa di individuare il tipo di messaggio e di chiamare l'opportuno metodo nel modulo che interagisce direttamente con l'utente.

 

RMI

 

Sfruttando l'RMI si eliminano alla radice molti degli ostacoli incontrati con le Socket, dato che è l'RMI stesso che si occupa di effettuare le trasformazioni del messaggi nei due sensi per poterle convogliare su Socket.

Per realizzare l'implementazione tramite RMI viene usato il pattern 'factory'. Infatti ci sarà, lato server, un oggetto 'factory' (l'unico a necessitare della registrazione nel registry RMI) che ad ogni invocazione del suo metodo enrol da parte del programma che gestisce l'interazione con l'utente, crea un oggetto che si occuperà di interfacciare il client con il server principale.

 

Implementazione e codifica

 

Classe ChatServer

 

Questa classe realizza il server principale della chat.

Viene tenuta traccia di tutti i client collegati, di qualunque tipo essi siano, memorizzandoli in una HashTable.

 

clients = new Hashtable();

 

Inoltre espone i metodi necessari per effettuare le seguenti operazioni:

 

q       aggiungere un client:

 

public synchronized void addClient(String name, InterfChatClient client)

 

Se il client sceglie un <name> già usato, lo disconnette ed esce:

 

      if (clients.get(name) != null) {

            client.disconnect();

            return;

      }

 

Altrimenti aggiorna la HashTable ed avvisa tutti gli altri client del nuovo arrivato:

 

      clients.put(name, client);

      sendEnterMessage(name);

 

q       rimuovere un client:

 

public synchronized void removeClient(InterfChatClient client)

 

Ricerca il client passato come parametro nella HashTable clients; una volta trovato, lo rimuove ed informa gli altri client di questa rimozione:

 

Enumeration e = clients.keys();

      while (e.hasMoreElements()) {

            String key = (String) e.nextElement();

            if (clients.get(key) == client) {

                  clients.remove(key);

                  sendLeaveMessage(key);

      }

 

q       rimuovere tutti i client:

 

public synchronized void removeAllClient()

 

Chiama il metodo disconnect di tutti client presenti nella HashTable:

 

Enumeration e = clients.elements();

while (e.hasMoreElements()) {

            InterfChatClient client=(InterfChatClient) e.nextElement();

            client.disconnect();

}

 

q       notificare un messaggio inviato da un client:

 

public synchronized void sendChat(String name, String message)

 

In questo caso il metodo chiamato è incomingChat:

 

      Enumeration e = clients.elements();

while (e.hasMoreElements()) {

            InterfChatClient client=(InterfChatClient) e.nextElement();

            if (clients.get(name) != client)

                  client.incomingChat(name, message);

}

 

q       notificare la presenza di un nuovo client:

 

public synchronized void sendEnterMessage(String name)

 

Viene invocato il metodo userHasEntered di tutti i client meno uno, quello appena entrato in chat:

 

Enumeration e = clients.elements();

      while (e.hasMoreElements()) {

            InterfChatClient client=(InterfChatClient) e.nextElement();

            if (clients.get(name) != client)

                  client.userHasEntered(name);

      }

 

q       notificare l'uscita di un client dalla chat:

 

public synchronized void sendLeaveMessage(String name)

 

In questo caso il metodo invocato è userHasLeft:

 

      Enumeration e = clients.elements();

      while (e.hasMoreElements()) {

            InterfChatClient client=(InterfChatClient) e.nextElement();

            client.userHasLeft(name);

      }

 

Interfaccia InterfChatClient

 

Questa è l'interfaccia che deve essere implementata dai diversi client che si vogliono realizzare.

I metodi dichiarati sono per la notifica di un messaggio, di un nuovo client, di un client che è uscito dalla chat e per disconnettere il client.

 

Classe SocketChatServer

 

Questa classe implementa l'interfaccia Runnable per poter essere eseguita in un thread separato.:

 

public class SocketChatServer implements Runnable

 

L'unica funzione di questa classe è di rimanere in attesa di connessioni sulla socket.

Nel costruttore viene creata una serversocket sulla porta passata come parametro:

 

serverSocket = new ServerSocket(port);

 

Nel metodo Run si pone in attesa di connessioni sulla socket; non appena ne arriva una, crea il SocketChatClient, lo avvia (come thread) e si ripone in attesa:

 

Socket newConn = serverSocket.accept();

SocketChatClient newClient = new SocketChatClient(server, newConn);

newClient.start();

 

Classe SocketChatClient

 

Questa classe implementa le interfacce InterfChatClient e Runnable.

Agisce come un client di ChatServer. Essa transle messaggi da una socket in richieste per il server chat, e transla le chiamate ai metodi effettuate dal server in messaggi su socket.

Nel costruttore vengono creati gli stream di I/O e, dopo aver ricevuto il nome dell’utente, viene aggiunto il binding (nome utente, classe) al ChatServer:

 

inStream = new DataInputStream(clientSock.getInputStream());

outStream = new DataOutputStream(clientSock.getOutputStream());

clientName = inStream.readUTF();

server.addClient(clientName, this);

 

Quando il server deve notificare la presenza di un nuovo client, invoca userHasEntered. In questo metodo viene inviato su socket il tipo del messaggio, la lunghezza del messaggio ed il nome dell’utente appena entrato in chat:

 

outStream.writeInt(SocketChatMessageTypes.ENTER);

outStream.writeInt(who.length());

outStream.writeBytes(who);

 

La stessa operazione viene effettuata anche nel metodo userHasLeft; l’unica differenza è il tipo di messaggio:

 

outStream.writeInt(SocketChatMessageTypes.LEAVE);

outStream.writeInt(who.length());

outStream.writeBytes(who);

 

Il metodo incomingChat viene invocato dal server quando qualcuno manda un messaggio. La parte dati del messaggio è composta dalla lunghezza del nome della persona che invia il messaggio, il nome della persona ed il messaggio:

 

outStream.writeInt(SocketChatMessageTypes.CHAT);

outStream.writeInt(who.length() + chat.length() + 4);

outStream.writeInt(who.length());

outStream.writeBytes(who);

outStream.writeBytes(chat);

 

Nel metodo disconnect viene semplicemente chiusa la socket ed interrotto il thread:

 

clientSock.close();

stop();

 

Fin qui i metodi che vengono invocati dal server.

In handleChatMessage viene letto un messaggio in arrivo dall’utente, decodificandolo ed inviandolo al server sotto forma di stringa:

 

int length = inStream.readInt();

byte[] chatChars = new byte[length];

inStream.readFully(chatChars);

String message = new String(chatChars);

server.sendChat(clientName, message);

 

Se arriva dall’utente un messaggio di tipo non conosciuto, viene tralasciato usando il metodo skipMessage:

 

int length = inStream.readInt();

inStream.skipBytes(length);

 

Nel metodo run si pone in attesa di dati dall'utente. Se viene richiesto di abbandonare la chat, viene chiamato il metodo opportuno in ChatServer. Altrimenti legge il resto del messaggio sulla socket:

 

int messageType = inStream.readInt();

switch (messageType) {

case SocketChatMessageTypes.CHAT:

handleChatMessage();

break;

                             

case SocketChatMessageTypes.EXIT:

server.removeClient(this);

return;

 

default:

skipMessage();

return;

 

Classe RunSocketClient

 

Implementa l'interfaccia InterfChatClient. E’ l’applicazione che viene avviata dall'utente. Consente di passare il numero della porta (-Dport=value) ed il nome del server (-Dhost=value) da linea di comando, altrimenti vengono usati quelli di default:

 

String portStr = System.getProperty("port");

String hostName = System.getProperty("host");

 

Crea la socket ed inizializza i canali di I/O sulla socket e di input sul system.in:

 

Socket clientSocket = new Socket(hostName, port);

DataOutputStream chatOutputStream = new DataOutputStream(clientSocket.getOutputStream());                          

DataInputStream chatInputStream = new DataInputStream(clientSocket.getInputStream());

BufferedReader userInputStream = new BufferedReader(new InputStreamReader(System.in));

 

Viene richiesto il nome (nickname) ed inviato su socket:

 

String myName = userInputStream.readLine();

chatOutputStream.writeUTF(myName);

 

Inoltre avvia un thread di lettura che legge messaggi dal server tramite la classe SocketChatReader alla quale viene passata una nuova istanza di RunSocketClient:

 

RunSocketClient thisClient = new RunSocketClient();

SocketChatReader reader = new SocketChatReader(thisClient, chatInputStream);

reader.start();

 

Rimane in attesa di input dall’utente e lo invia su socket:

 

while (true) {

String chatLine = userInputStream.readLine();

      if (!chatLine.equals("-exit"))

      {

            chatOutputStream.writeInt(SocketChatMessageTypes.CHAT);

            chatOutputStream.writeInt(chatLine.length());

            chatOutputStream.writeBytes(chatLine);

      }

      else

      {

            chatOutputStream.writeInt(SocketChatMessageTypes.EXIT);

            System.exit(0);

      }

}

 

Ci sono poi i metodi che scrivono sul system.out: userHasEntered, userHasLeft, incomingChat e disconnect, metodi invocati da SocketChatReader.

 

Classe SocketChatReader

 

Questa classe implementa l’interfaccia Runnable per poter essere eseguita in un thread.

Nel  metodo run legge il tipo di messaggio e chiama il metodo appropriato per elaborarlo:

 

while (myThread == thisThread) {         

try {

            int messageType = inStream.readInt();

            switch (messageType) {

                  case SocketChatMessageTypes.CHAT:

                       readChat();

                       break;

 

                  case SocketChatMessageTypes.ENTER:

                       readEnter();

                       break;

 

                  case SocketChatMessageTypes.LEAVE:

                       readLeave();

                       break;

 

                  default:

                       skipMessage();

                       break;

}

}

}

 

In readChat, legge un messaggio interpretando i byte provenienti dalla socket ed invocando l'opportuno metodo nella classe RunSocketClient che restituisce l’output all’utente:

 

int length = inStream.readInt();

int whoLength = inStream.readInt();

int chatLength = length - whoLength - 4;

 

byte[] whoBytes = new byte[whoLength];

inStream.readFully(whoBytes);

 

String whoString = new String(whoBytes);

 

byte[] chatBytes = new byte[chatLength];

inStream.readFully(chatBytes);

 

String chatString = new String(chatBytes);

 

client.incomingChat(whoString, chatString);

 

Anche in readEnter, readLeave e skipMessage si legge su socket chiamando poi il metodo relativo in RunSocketClient.

 

Classe RunServer

 

Questa classe crea un nuovo ChatServer, inizializza un SecurityManager, il registry RMI ed i server Socket e RMI.

 

ChatServer server = new ChatServer();

System.setSecurityManager(new RMISecurityManager());       

Registry r = LocateRegistry.createRegistry(1099);           

SocketChatServer socketServer = new SocketChatServer(server, port);

socketServer.start();

RMIChatEnrol rmiEnrol = new RMIChatEnrolImpl(server);

 

Rimane in attesa del comando exit per rimuovere tutti i client ed uscire:

 

String cmd = userInputStream.readLine();

if (cmd.equals("exit")) {

      server.removeAllClient();

      System.exit(0);

}

 

Interfaccia SocketChatMessageTypes

 

Vengono dichiarati i tipi di messaggi che ‘viaggiano’ su socket: CHAT, ENTER, LEAVE, EXIT.

 

Interfaccia RMIChatEnrol

 

E' l'interfaccia della 'fabbrica di oggetti RMIChatClientImpl'. L'unico metodo è enrol.

 

Classe RMIChatEnrolImpl

 

Implementa l'interfaccia RMIChatEnrol.

Nel costruttore registra se stessa nel registry RMI:

 

java.rmi.Naming.rebind("chat", this);

 

Tramite il metodo enrol, crea nuovi oggetti RMIChatClientImpl che restituisce al RunRMIClientImpl:

 

public RMIChatClient enrol(String name, InterfChatClient client)

      throws java.rmi.RemoteException

{

return new RMIChatClientImpl(server, name, client);

}

 

Classe RunRMIClientImpl

 

E' la classe che viene avviata dal client e che gestisce l'interazione con l'utente che desidera comunicare tramite RMI.

Implementa l'interfaccia InterfChatClient.

Interroga il registry RMI per avere il riferimento alla 'fabbrica di oggetti RMIChatClientImpl':

 

RMIChatEnrol enrol = (RMIChatEnrol) java.rmi.Naming.lookup("chat");

 

Invoca il metodo enrol della 'fabbrica' passandogli una nuova istanza di se stessa ed il nome scelto dall’utente:

 

InterfChatClient thisClient = new RunRMIClientImpl();

RMIChatClient server = enrol.enrol(myName, thisClient);

 

Poi rimane in attesa di input dall’utente:

 

while (true) {

String chatLine = userInputStream.readLine();

      if (chatLine.equals("-exit"))

server.disconnect();

      server.sendChat(chatLine);

}

 

Ci sono poi i metodi che scrivono sul system.out: userHasEntered, userHasLeft, incomingChat e disconnect.

 

Interfaccia RMIChatClient

 

Estende l'interfaccia InterfChatClient ed aggiunge il metodo sendChat invocato dal client per comunicare con il ChatServer.

 

Classe RMIChatClientImpl

 

Le istanze di questa classe vengono create dalla 'fabbrica' RMIChatEnrolImpl.

Estende UnicastRemoteObject ed implementa RMIChatClient:

 

public class RMIChatClientImpl extends UnicastRemoteObject implements RMIChatClient

 

Nel costruttore notifica al server la presenza di se stesso:

 

server.addClient(name, this);

 

Il client invoca il metodo sendChat che viene reindirizzato al ChatServer. In questo caso non è necessaria alcuna conversione per inviare i messaggi su socket dato che è l'RMI che si occupa di tutto.

Inoltre ChatServer invoca i metodi di questa classe che rigirano la chiamata al metodo opportuno nel client, anche in questo caso senza alcuna conversione.

 

Come eseguire la chat

 

Nell’eseguire il server ed il client RMI è necessario indicare un file di policy per gestire la sicurezza:

 

java.exe -Djava.security.policy=policy RunServer

java.exe RunSocketClient

java.exe -Djava.security.policy=policy RunRMIClientImpl

 

E’ possibile eseguire il server su una porta diversa da quella predefinita, usando la seguente sintassi:

 

java.exe -Djava.security.policy=policy –Dport=1234 RunServer