MokaByte 59 - Gennaio 2002 
Applicazioni web
III parte: Il secondo tier, il server
di
Antonio Cisternino
In questo terzo appuntamento sulle architetture n-tier ci concentreremo sul secondo livello di una struttura 3-tier: il server a cui si collegano i clienti.

Introduzione
In questa puntata vedremo come realizzare un altro strato della nostra architettura 3-tier: lo strato intermedio che rende accssibili i dati ai clienti. Questo livello aiuta a separare le tecnologie impiegate per la realizzazione del cliente e lo storage in cui vengono memorizzati i dati.
Noi realizzeremo un server multithread in Java come livello intermedio per la nostra architettura. Questo servirà ad esemplificare le problematiche legate a questo tipo di programmi. Nelle applicazioni reali oramai si evita di scrivere questo codice in favore di una soluzione piùl modulare: un server, condiviso da più programmi, carica dinamicamente componenti che si occupano di gestire una connessione.
I server utilizzati sono vari; quelli più diffusi sono quelli derivati dai server Web e dai framework come MTS di Microsoft e Enterprise Java Beans per la piattaforma Java (basata su RMI). Nell'ambito di Java EJB e Java Servlet rappresentano lo scheletro del livello intermedio.

 

 

Lo scheletro del server
Un server ha una struttura decisamente semplice: attende l'arrivo delle richieste dei clienti e le gestisce. Le richieste possono essere gestite in modo sequenziale (chi prima arriva prima viene servito) oppure con un certo grado di parallelismo; in questo caso la richiesta viene girata ad un thread responsabile per la sua gestione e il server procede immediatamente a raccogliere la prossima richiesta. Il grado di parallelismo è rappresentato dal massimo numero di richieste che possono essere gestite contemporaneamente dal server.
Innanzitutto scriviamo il ciclo base del server che gestisce le richieste in modo sequenziale, poi vediamo come cambiarlo per gestire più richieste contemporaneamente.
Il ciclo di base è il seguente:

// ...
ServerSocket ss = new ServerSocket(PORT);
boolean exit = false;
while (!exit) {
   Socket s = ss.accept();
   InputStream in = s.getInputStream();
   OutputStream out = s.getOutputStream();
   // Serve la connessione
}
//...

Il server è incentrato sulla classe ServerSocket che rappresenta il punto di connessione per i clienti. Sono disponibili 65536 porte per ascoltare ed ovviamente solo un processo può essere collegato ad una certa porta. Per fare i primi esperimenti è sufficiente utilizzare un numero di porta superiore a 1024 (sotto si rischiano conflitti con i serivizi standard).
L'invocazione del metodo accept blocca l'esecuzione del programma finché un cliente non si collega. Il risultato della chiamata è un oggetto della classe Socket che rappresenta la connessione. Utilizzando questo oggetto è possibile ottenere un InputStream ed un OutputStream che rappresentano i canali utilizzati per comunicare col cliente.

 

 

Il primo server
Diamo ora un'occhiata allo schema di un semplice server capace di gestire un cliente per volta. La struttura del programma è essenzialmente quella appena introdotta. Ogni volta che il cliente invia una linea di testo il server la rimanda indietro a meno che non si tratti della stringa "exit".
La classe PrimoServer implementa l'interfaccia Runnable in modo da poter creare un thread che esegua il methodo run(). Il metodo main infatti non fa altro che creare un thread che avvii il server.
L'intero server è contenuto nel metodo run. Contiene il ciclo che accetta i clienti e costruisce un BufferedReader e un PrintWriter per comunicare col cliente. Dal primo si legge una stringa e nel secondo si invia la risposta.
Il server non accetta ulteriori clienti finché non riceve la stringa "exit" dal cliente connesso. La struttura del server è quindi sequenziale.


import java.net.ServerSocket;
import java.net.Socket;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class PrimoServer implements Runnable {
   public void run() {
      try {
         ServerSocket ss = new ServerSocket(1030);
         while (true) {
            Socket s = ss.accept();
            BufferedReader in = new BufferedReader(new             InputStreamReader(s.getInputStream()), 1);
            PrintWriter out = new PrintWriter(new             OutputStreamWriter(s.getOutputStream()));
            // Gestisce la connessione
            String line;
            while ((line = in.readLine()) != null &&
                                          ! "exit".equals(line)) {
               out.print(line + "\r\n");
               out.flush();
            }
            s.close();
         }
      }
      catch (Exception e) {
         System.out.println(e);
      }
}

   public static void main(String[] args) {
      Thread t = new Thread(new PrimoServer());
      t.start();
   }
}

Per provare il server dopo aver compilato il programma si esegue:

> java PrimoServer

Si usa poi un'altra shell (in un'altra finestra) per eseguire il comando:

> telnet 127.0.0.1 1030

Scrivendo qualsiasi cosa una volta stabilita la connessione con telnet si ottiene come risposta quello che si è scritto. Quando si scrive exit la connessione viene terminata.
Verso una soluzione più modulare
Cerchiamo ora di strutturare meglio il nostro piccolo server in modo che sia riutilizzabile. Vorremmo evitare di cambiare il codice del server sfruttando i meccanismi offerti dal linguaggio per fornire al server un oggetto in grado di comunicare col cliente. Per ora assumiamo che la porta del server sia sempre la stessa.
In effetti la parte che decide cosa fa il server per ogni cliente è quella che segue la chiamata al metodo accept. Possiamo quindi generalizzare il nostro schema assumendo che la comunicazione con il cliente sia gestita da una classe che implementa un'interfaccia ben definita.
Assumiamo quindi di aver definito la seguente interfaccia:

public interface Protocol {
void speak(Socket s) throws Exception;
}

Possiamo quindi riscrivere il nostro server come segue:
import java.net.ServerSocket;
import java.net.Socket;

public class SecondoServer implements Runnable {
private Protocol proto;

   public SecondoServer(Protocol p) {
      proto = p;
   }

   public void run() {
      try {
         ServerSocket ss = new ServerSocket(1030);
         while (true) {
            Socket s = ss.accept();
            proto.speak(s);
         }
      }
      catch (Exception e) {
         System.out.println(e);
      }
   }

   public static void main(String[] args) {
      Thread t = new Thread(new SecondoServer(new EchoProtocol()));
      t.start();
   }
}

Abbiamo aggiunto un costruttore che accetta come parametro un oggetto di una classe che implementa l'interfaccia Protocol. Il server semplicemente delega la gestione della connessione all'oggetto invocando il metodo speak e passando il Socket rappresentante la connessione.
Abbiamo finalmente incapsulato la comunicazione tra il cliente e il server in una classe, questo ci consente di implementare diversi protocolli mantenendo lo stesso server. L'esempio precedente si ottiene definendo la seguente classe che è poi specificata nel metodo main come protocollo da essere usato:

import java.net.Socket;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class EchoProtocol implements Protocol {
   public void speak(Socket s) throws Exception {
      BufferedReader in = new BufferedReader(new       InputStreamReader(s.getInputStream()), 1);
      PrintWriter out = new PrintWriter(new       OutputStreamWriter(s.getOutputStream()));
      // Gestisce la connessione
      String line;
      while ((line = in.readLine()) != null &&
                                    !"exit".equals(line)) {
         out.print(line + "\r\n");
         out.flush();
      }   
      s.close();
   }
}

 

 

Server multi-thread
Adesso facciamo una magia: senza cambiare una riga nel nostro server (tranne che nel metodo main che però non è realmente legato al server) facciamo in modo che sia in grado di accettare più clienti contemporaneamente. Consideriamo la seguente classe:
import java.net.Socket;

public class MultiThreadProtocol implements Protocol, Runnable {
   private Protocol proto;
   private Socket s = null;
   public MultiThreadProtocol(Protocol p) {
      proto = p;
   }
   public void run() {
      try {
         proto.speak(s);
      }
      catch (Exception e) {
         System.out.println(e);
      }
   }

    public void speak(Socket s) throws Exception {
      MultiThreadProtocol p = new MultiThreadProtocol(proto);
      p.s = s;
      Thread t = new Thread(p);
      t.start();
    }
}

Questa classe prende un protocollo pensato per servire una connessione e lo esegue in un thread separato. In questo modo il metodo speak termina subito, prima che la richiesta sia completata e il server può accettare un'altra connessione.
Se si vuole un server concorrente basta creare il server specificando come protocollo un oggetto della classe MultiThreadProtocol e nella sua costruzione specificare un oggetto della classe che implementa il protocollo vero e proprio.
Per provare la versione multithread del server è sufficiente modificare la seguente linea nel metodo main:

Thread t = new Thread(new SecondoServer(new EchoProtocol()));

con

Thread t = new Thread(new SecondoServer(new MultiThreadProtocol(
   
                   new EchoProtocol())));

 

 

Conclusioni
In questo articolo abbiamo visto come realizzare un server capace di fare da tramite tra il livello dei clienti e quello dei dati. Lo scopo del server era quello di esemplificare le problematiche tipiche del livello intermedio la cui responsabilità è sostanzialmente quella di fare da tramite tra i il primo e il terzo livello.
Questa è sostanzialmente la struttura di un server per Servlet oppure quello fornito con EJB. Nel caso di questi server il numero di servizi è decisamente superiore, ma l'idea è la stessa.
Nella prossima puntata ci concentreremo sul terzo livello di un'architettura 3-tier: il cliente. Infine cercheremo di mettere insieme i tre livelli come esempio di architettura n-tier.

 

 

Esempi
Scarica gli esempi descritti in questo articolo


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati 
E' vietata la riproduzione anche parziale 
Per comunicazioni inviare una mail a info@mokabyte.it