MokaByte Numero 32  -  Luglio Agosto 99
 
Un piccolo JWebServer 
II parte
di 
Antonio Cisternino
Partendo dall'esempio preso in considerazione il mese scorso, proseguiamo e rendiamo il web server concorrente


 

In questo articolo, prosecuzione del precedente in cui ho descritto un Web Server scritto in Java, mostrerò come sia possibile estendere il server descritto in modo che divenga concorrente.

Una piccola riflessione

Perchè scrivere un web server in Java? Ormai i Web Server spuntano come funghi e uno in più non è certo una novità. Lo scopo del precedente articolo era quello di mostrare come sia possibile scrivere un server che consentisse lo scambio di dati e fosse in grado di interagire con uno o più clienti connessi via Internet.
Lo scopo principale dell'articolo era quello di mostrare il ciclo base di un server qualsiasi che accetti connessioni (via TCP) e, implementando un opportuno protocollo, offra servizi via rete.
Lo schema concettuale presentato è il seguente: 
<Aspetta un cliente>

<Servi il cliente>

<Attendi altri clienti>
Ill codice che eseguiva il ciclo principale del server era il seguente: 
public void run() {

  ServerSocket listen = null;
 
 

  try {

    listen = new ServerSocket(PORT);

  } catch(IOException e) {

    System.err.println("Cannot bind the port "+PORT);

    System.exit(1);

  }
 
 

  for (;;)

    try {

      Socket s = listen.accept();

      OutputStream outf = s.getOutputStream();

      BufferedReader in = 

        new BufferedReader(new InputStreamReader(s.getInputStream()));

      OutputStreamWriter out = 

        new OutputStreamWriter(outf);
 
 

      Hashtable request = getRequest(in);
 
 

      if (request != null) {

        File f = new File(root + request.get(":Get"));
 
 

        if (f.exists() && f.isFile())

          if (f.canRead())

            sendFile(request, f, out, outf);

          else

            permissionDenied(request, out);

        else 

          fileNotFound(request, out);

      }
 
 

      s.close();

    } catch (Exception e) {

      System.err.println("WebServer: "+e);

    }

}

Adesso qualcuno si potrebbe domandare: ma non è possibile fare meglio sfruttando i thread di java? La risposta è ovviamente affermativa: se osserviamo il codice del metodo run() la parte del codice che effettua il ciclo fondamentale del server si limita alle seguenti linee: 
public void run() {

  ServerSocket listen = null;
 
 

  try {

    listen = new ServerSocket(PORT);

  } catch(IOException e) {

    System.err.println("Cannot bind the port "+PORT);

    System.exit(1);

  }
 
 

  for (;;)

    try {

      Socket s = listen.accept();
 
 

      ...
 
 

      s.close();

    } catch() { ... }

}

Il resto del codice è necessario solo per gestire la connessione e il protocollo.
Potremmo pensare quindi di migliorare il servizio offerto dal nostro server offrendo la possibilità di servire più clienti in contemporanea. Ma è realmente conveniente questa scelta? Se è innegabile che servire un cliente alla volta porti alla massima efficienza su una macchina con un solo processore è anche vero che ridurre di poco questa efficienza consente di suddividere il lavoro e servire in contemporanea più clienti. Bisogna inoltre fare un'altra osservazione: quando un'applicazione cerca di accedere al disco viene sospesa in attesa che le informazioni passino dal disco alla memoria; il sistema operativo appoggia l'applicazione durante questa interazione che dura un tempo enorme rispetto alla velocità di un normale elaboratore. Quando si legge che un disco ha un tempo di accesso nell'ordine dei millisecondi si deve pensare che i tempi del processore sono mille volte più piccoli e quindi per un tempo enorme il processo che sta servendo il cliente in realtà è sospeso in attesa dei dati. Ecco quindi che servire più clienti in contemporanea può essere addirittura quasi gratis: l'accesso al disco li sospende tutti e uno alla volta poi termineranno il loro servizio.
Quanto detto è vero solo per quei servizi che, come il Web, richiedono per il loro fuzionamento l'accesso a dispositivi. Se al contrario il cliente richiede al server un calcolo pesante (come ad esempio provare un certo numero di chiavi per forzare un algoritmo di crittografia) senza accessi al disco, può rivelarsi inopportuno accettare più clienti in contemporanea poiché tutti concorreranno ad usare il processore aumentando sensibilmente i tempi di risposta.
In generale però un server tipicamente si occupa di accedere al filesystem o ad un database e quindi l'esecuzione del servizio per un cliente userà i dispositivi e risulterà complessivamente conveniente permettere l'accesso contemporaneo a più clienti.
Nel resto dell'articolo vedremo innanzitutto come sia possibile con poco sforzo rendere concorrente il web server che abbiamo presentato cercando di capire le gioie e i dolori di una tale soluzione.
 
 

Un colpo di genio

Abbiamo appena scoperto che non ci piace il web server sequenziale e lo vogliamo concorrente... Come fare? Roba da suicidio: riscrivere le 245 righe di codice con questo caldo? Giammai! Effettivamente ce lo possiamo risparmiare sfruttando la programmazione OOP e introducendo, udite udite, una sola classe minimale.
Nel codice che abbiamo studiato nel precedente articolo abbiamo unito la gestione del protocollo con la gestione dei clienti. Nel paragrafo precedente abbiamo visto quale in realtà sia il codice che accetta i clienti e quale quello che gestisce la connessione. Oltre a quel codice avevamo il metodo main che si occupava di avviare il server e quindi di iniziare il ciclo di ricezione dei clienti avviando un Thread che eseguiva il metodo run() della classe WebServer.
Proviamo quindi a fare la seguente cosa: creiamo una nuova classe che abbia un metodo main equivalente al precedente, un metodo run() contenente solo la parte del codice che abbiamo detto essere quella necessaria per accettare i clienti. E poi? Poi riutilizziamo la classe che abbiamo già scritto con molta fatica per gestire i clienti.
Per non perdere il filo del discorso però facciamo una cosa alla volta! E iniziamo con il definire una nuova classe chiamata JWebServer che a sua volta userà la classe WebServer Già definita nel precedente articolo. Secondo quanto detto la classe JWebServer dovrebbe essere (commenti a parte) come segue: 
public class JWebServer implements Runnable {

  public static int PORT = 80;

  private static int OUTBUFSZ = 65536;

  private static final String CRLF = "\r\n";

  private static final String SERVER_STRING = "JWebServer/1.0";

  private String root = ".";
 
 

  public static Hashtable types;
 
 

  {   /** Codice di inizializzazione della classe */

    types = new Hashtable();

    types.put(".html", "text/html");

    types.put(".htm", "text/html");

    types.put(".gif", "image/gif");

    types.put(".jpg", "image/jpg");

    types.put(".class", "application/octet-stream");

  }
 
 

  public void run() {

    ServerSocket listen = null;
 
 

    try {

      listen = new ServerSocket(PORT);

    } catch(IOException e) {

      System.err.println("Cannot bind the port "+PORT);

      System.exit(1);

    }
 
 

    for (;;)

      try {

        Socket s = listen.accept();
 
 

        ?????????
 
 

      } catch (Exception e) {

        System.err.println("WebServer: "+e);

      }

  }
 
 

  public static void main(String[] args) {

    Thread t = new Thread(new JWebServer());

    t.start();

  }

} // JWebServer

Come si può osservare la classe mantiene le costanti che usava: queste infatti definiscono parametri comuni a tutto il server e quindi è giusto che appartengono alla classe del server. La classe WebServer che è stata declassata da web server a gestore di un singolo cliente, utilizzerà queste costanti al suo interno per poter decidere ad esempio dove si trovi la root del web server.
Dobbiamo ora chiederci cosa mettiamo al posto dei punti interrogativi? Poiché abbiamo deciso di accettare più connessioni in contemporanea useremo un Thread per ogni cliente che sarà realizzato con piccole modifiche alla classe WebServer.
Ecco quindi che al posto dei punti interrogativi troviamo: 
try {

  Socket s = listen.accept();
 
 

  Thread t = new Thread(new WebServer(s));

  t.start();
 
 

} catch (Exception e) {

  System.err.println("WebServer: "+e);

}

Ecco quindi che ogni volta che arriva una nuova connessione il nostro nuovo JWebServer avvia un Thread che si mette a gestire la connessione secondo lo schema già visto mentre il Thread principale si mette immediatamente in ascolto di nuove richieste.
Per concludere la nostra migrazione verso il server concorrente non ci resta che da capire come vada modificata la classe WebServer perché si limiti a gestire una singola connessione senza più preoccuparsi di gestire l'arrivo dei clienti. Ovviamente il codice che abbiamo spostato relativo alla gestione dei clienti va eliminato dalla classe WebServer.
Questo però non basta: se si osserva il codice che avvia il dialogo di un cliente si vede subito che viene creata un'istanza della classe WebServer a cui viene passato il riferimento al Socket che rappresenta la connessione da il cliente e il server. Quindi dovremo aggiungere il costruttore alla classe come segue: 
public WebServer(Socket s) {

  conn = s;

}
Ovviamente bisogna anche aggiungere un nuovo attributo alla classe chiamato conn e di tipo Socket. Per quanto riguarda gli altri metodi sono tutti a posto tranne il metodo run() che praticamente si riduce al corpo del ciclo for della versione precedente. Va inoltre cambiato s in conn poiché il socket di connessione al cliente viene passato come abbiamo visto al costruttore.
Infine bisogna ricordarsi di anteporre il nome della classe alle costanti che sono state spostate dalla classe WebServer alla classe JWebServer.
Ecco quindi che con pochissimo sforzo siamo riusciti a rendere concorrente un server sequenziale. Se lo provate dovreste notare un certo aumento delle prestazioni.
Ma sono tutte rose e fiori? E allora perché non lo abbiamo fatto subito concorrente? La prima ragione è chiaramente la semplicità. L'introduzione della concorrenza non porta però solo miglioramenti ma anche qualche complicazione sui dati condivisi.

La condivisione di un dato
Supponiamo di voler realizzare un sistema di log per il nostro Web Server. Come fare? Ovviamente creeremo una classe (e.g. LogManager) che supporti un metodo che, ogni volta che viene invocato, scriva su un file la stringa passata come parametro.
L'oggetto in questione può essere acceduto in concorrenza da più Thread che stanno servendo clienti differenti. La soluzione più banale per implementare il metodo addLog è quella di eseguire una println su un PrintWriter c'è il rischio che se i due clienti cercano di usare più volte addLog queste vengano scritte come segue:

Cliente A:
 
 

... 

addLog("Cliente A connesso");

addLog("Connessione con A chiusa");

...
 
 

Cliente B:
 
 

...

addLog("Cliente B connesso");

addLog("Connessione con B chiusa");

...
 
 

Output errato:
 
 

Cliente A connesso

Cliente B connesso

Connessione con B chiusa

Connessione con A chiusa
 
 

Output atteso:
 
 

Cliente A connesso

Connessione con A chiusa

Cliente B connesso

Connessione con B chiusa
 
 
 

L'esempio in questione è alquanto banale ma dà l'idea del problema: può capitare che i due Thread accedano più volte ad un dato condiviso (il canale di log) e la loro reciproca interazione porti ad operare in modo scorretto sul dato condiviso.
Questo problema può essere affrontato e risolto usando il meccanismo di sincronizzazione di Java: si dichiara un metodo synchronized (nel nostro caso addLog) e in quel metodo si eseguono tutte le operazioni necessarie (quindi entrambe le scritture) in modo che nel caso di conflitto nell'accesso al canale di log solo un Thread alla volta potrà eseguire quel metodo evitando le interferenze indesiderate.
Se questo fatto nel caso del log è irrilevante risulta fondamentale se il server che stiamo scrivendo permette di accedere in modo concorrente ad una base di dati.
 
 

Conclusioni
In questo articolo ho mostrato come rendere concorrente il web server presentato nell'articolo precedente. Abbiamo poi visto che ci possono essere problemi nel passaggio alla concorrenza con un piccolo accenno al problema dei dati condivisi.


  
 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it