MokaByte Numero  38  - Febbraio  2000
Networking in Java 
III puntata: la pratica
MultiThreaded server
di 
Andrea Trentini
Come gestire più connessioni contemporaneamente

La volta scorsa abbiamo visto come si crea un server con le socket in Java, però gestiva una sola connessione alla volta. Questa volta cerchiamo di servire tutti i tentativi di connessione.

Un gestore multiplo
Cominciamo subito col provare il software. Una volta scaricati questi sorgenti, lanciate "java MultiEchoServer" per avere il server attivo:

In questa immagine mi sono già connesso con un telnet, provate anche voi. Dovete eseguire un telnet e connettervi all'indirizzo 127.0.0.1 (Ricordate? Questo è l'indirizzo della vostra macchina, ovunque vi troviate) sulla porta 2001 (di default, poi nel sorgente potete modificarla).
Se ora lanciate un secondo telnet, e un terzo, e un quarto e vi connettete allo stesso modo, vedrete che il server vi accetta (contrariamente alla versione della volta scorsa). Avrete cioè una situazione simile a questa:

Stavolta ogni client che si connette ottiene "udienza", non viene rifiutato.

Questa funzione viene realizzata creando un thread per ogni client che si connette. Tale thread gestisce una singola connessione e niente altro: è bloccato quasi sempre sulla read() dallo stream della socket.
Se guardate il sorgente troverete una classe MultiEchoServer e una classe Echoer (che estende Thread). MultiEchoServer è il gestore delle connessioni in ingresso, è lui a creare un'istanza di Echoer per ogni connessione che arriva:

while(true)
{
        try
        {
                System.out.print("accepting... on port:"+port);
                s = server.accept();
                System.out.print("... accepted:"+s);
                new Echoer(s).start();
                System.out.println("... Echoer started.");
        }
        catch(Exception e)
        {
                e.printStackTrace();
        } 
}


Come vedete da questo frammento di codice, il server resta in perenne attesa di connessioni, ogni client che si connette (cioè quando la accept() esce fornendo una Socket) causa la creazione di un Echoer a cui viene passata la Socket in questione. E' l'Echoer ad occuparsi in toto della comunicazione. Il gestore torna ad accettare nuove connessioni.
Il brutto di questo meccanismo è che viene creato un thread per ogni connessione accettata. Ciò può essere pesante su alcuni sistemi operativi. Purtroppo però, che io sappia, in Java non è ancora stata implementata la funzionalità di select(), presente in altri ambienti (ad es. C sotto Unix). Per curiosità potete andare a consultare le RFE (Request for FEatures) sul sito developer.javasoft.com, Questa funzioanlità era fino a poco tempo fa nelle prime 25 in ordine di importanza. La select() permette di assegnare una RRI (Routine di Risposta a Interrupt) a d ogni socket aperta, in modo da ricevere una notifica quando ci sono dati da leggere, evitando così la creazione di molti thread.
Un chat server
Ora facciamo una piccola aggiunta che ci servirà come spunto per una delle prossime puntate.
Il server che vi ho mostrato poco fa è sì un server multi-client, però ogni connessione viene gestita da un oggetto che non ha la minima idea del fatto che esistano altre connessioni... Sarebbe divertente poter gestire un piccolo chat server, in modo da permettere che molti utenti si parlino tra loro...
Facciamo le opportune modifiche: ci serve una struttura che "tenga in contatto" i vari Echoer, diciamo uno Stack. Bisogna modificare ogni Echoer (ora chiamato Chatter) per fargli mandare i caratteri ricevuti al gestore invece che direttamente alla connessione. Bisogna modificare il gestore in modo che "forwardi" ogni carattere ricevuto da un Chatter a tutti i Chatter collegati in un dato momento.

Il meccanismo è semplice: ogni Chatter, appena riceve un carattere, lo passa al gestore chiamando broadcast():

while(true)
{
        int c=i.read(); // bloccante !!!
        m.broadcast(c); // lo passa al gestore per la trasmissione a tutti

        // PRIMA ERA COSI'
        //o.write(c); // echo
}


Il gestore, avendo traccia di ogni Chatter istanziato, può ciclare su tutti i Chatter e spedire il carattere ricevuto a tutte le connessioni (realizzando l'echo anche per il client trasmittente):

public void broadcast(int c){
        Enumeration e=chatters.elements();
        while(e.hasMoreElements())
        {
                ((Chatter)e.nextElement()).send(c);
        }
}

 
 

Trasmissione di dati complessi
Dov'è il difetto di questo chat server? Beh, il difetto è che ogni singolo carattere viene spedito appena arriva, non c'è bufferizzazione. Per cui vedete il testo di ogni partecipante mescolato a quello di tutti gli altri... Cioè il quanto di trasmissione è il singolo carattere, non la linea o un messaggio più complesso. Bisognerebbe trovare un sistema (un protocollo?) di trasmettere blocchi di dati, magari etichettati col nome del partecipante, in modo da avere ad esempio un riga intera per ogni partecipante. Esattamente come accade usando IRC.

In una delle prossime puntate vedremo come si può, senza scrivere tanto codice, realizzare un protocollo per trasmettere righe intere di lunghezza variabile. In seguito trasmetteremo addirittura oggetti interi, creeremo cioè degli oggetti messaggio a nostro piacimento e li manderemo al gruppo in chat.
 

Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it