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