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
|