MokaByte Numero 31  - Giugno 1999 
Un piccolo JWebServer
di 
Antonio Cisternino
Come realizzare un server http in Java


In questo articolo descriverò come si scrive un server usando la libreria standard di Java. L'esempio riportato sarà un Web Server che supporta una parte del protocollo HTTP 1.0.

Introduzione

Il paradigma dominante di comunicazione in Internet è quello client-server, nel quale si prevede che uno o più programmi clienti si colleghino via rete a un programma servente per scambiare informazioni.
Se dapprima sembrava la panacea di tutti i mali col tempo si è capito che questo paradigma di interazione non è sempre il più adeguato. È accaduto infatti che nuovi paradigmi di interazione si affacciassero per offrire nuovi sistemi e canali di interazione.
Il Web, nato ormai quasi dieci anni fa, è basato sul paradigma client-server: un server accetta le richieste dei clienti (i browser) e invia i file richiesti. Come molti oggigiorno sanno un server Web ormai fa molto altro, così non era per i primi server Web scritti.
La struttura del dialogo che consente il dialogo tra il cliente e il server e le regole che vanno rispettate perché questa comunicazione possa avvenire sono comunemente identificate col generico nome di protocollo. Ora posso finalmente esordire con una banalità: HTTP è il protocollo del Web.
In questo articolo scriveremo insieme un server Web che è in grado di servire pagine ad un normale browser come Microsoft IE oppure Netscape. Il lettore più attento però dovrebbe essere in grado di applicare lo stesso schema per scrivere i propri server, magari per accedere ad un database remoto da applet. Le tecniche sviluppate sono prevalentemente orientate alla scrittura di server TCP.
Il ciclo del server
La struttura più semplice per un server è considerata quella sequenziale in cui il server gestisce un cliente alla volta. I clienti che si collegano quando il server è impegnato restano sospesi in attesa di iniziare la sessione col server.
Per scrivere un server innanzitutto dobbiamo essere capaci di aspettare connessioni dalla rete. Per fare questo utilizziamo la classe java.net.ServerSocket che è in grado di attendere connessioni su una certa porta.
Il ciclo di base del server sarà quindi:
 
for (;;) { // Per sempre
  waitClient();
  serveClient();
}


Dove waitClient e serveClient rappresentano la fase di attesa di un cliente e di esecuzione della connessione. La coda di attesa dei clienti non è rappresentata esplicitamente poiché è mantenuta dal sistema operativo.
Il metodo accept della classe java.net.ServerSocket consente di attendere un cliente (non termina finché non si rende disponibile una connessione) e di ottenere un'istanza della classe java.net.Socket che potrà essere usata per ottenere un canale di input e uno di output necessari per la comunicazione.
Il ciclo precedente diviene quindi:
 

for (;;) // Per sempre
  try { 
    Socket s = listen.accept();
    OutputStream outf = s.getOutputStream();
    BufferedReader in = 
      new BufferedReader(new InputStreamReader(s.getInputStream()));
    OutputStreamWriter out = 
      new OutputStreamWriter(outf); 

    handleConnection();
  } catch(Exception e) {
    System.err.println("WebServer: "+e);
  } 

Il metodo handleConnection è solo una marca per indicare il resto del codice che gestisce la connessione. Gli stream riportati sono quelli necessari alla comunicazione; sarà utile in seguito disporre sia di un canale di output binario che di un canale di output testuale, per questo motivo vengono inizializzati due stream di output. La variabile listen contiene il riferimento ad un oggetto di tipo java.net.ServerSoccer inizializzato come segue:
 
ServerSocket listen = null;

try {
  listen = new ServerSocket(PORT);
} catch(IOException e) {
  System.err.println("Cannot bind the port "+PORT);
  System.exit(1);
}

Per poter creare un socket che ascolta la rete è necessario conoscere la porta su cui il server attenderà i suoi clienti. Nel caso del protocollo HTTP utilizzato dai server Web si usa per default la porta 80. Per provare il server presentato in questo articolo però potrebbe essere necessario utilizzare un'altra porta: sotto Unix le porte sotto la 1024 possono essere richieste da processi eseguiti come root. Inoltre se un web server è già in esecuzione sulla macchina la porta 80 sarà occupata e quindi il nostro codice non potrà ottenerla. Il valore della costante PORT consente di cambiare questa porta.
Il protocollo HTTP: un breve riassunto
Per proseguire l'implementazione del nostro server è necessario fare una piccola digressione sul protocollo HTTP e sul suo funzionamento. Come ho già detto in precedenza un protocollo è l'insieme di regole necessarie a definire una comunicazione. Nel caso del Web un browser solitamente deve richiedere al server uno o più file (tipicamente la pagina HTML e le varie immagini e file accessori necessari alla sua visualizzazione).
Il protocollo HTTP offre quindi fondamentalmente una funzionalità: la possibilità di richiedere un documento mediante una richiesta di tipo GET. Una richiesta può essere accompagnata da una serie di opzioni che, in modo simile agli attributi dei tag HTML, sono tante coppie (nome dell'attributo, valore).
Ovviamente si può obiettare che dover effettuare una connessione per ogni file scaricato è poco furbo poiché sarebbe più intelligente sfruttare lo stesso canale per scaricare i file relativi ad una singola pagina. Daltronde quando HTTP fu pensato la semplicità era importante e i web server reali (non quello che scriviamo noi...) effettivamente fanno inizializzazioni di questo genere.
La prima fase del protocollo prevede una fase di scambio di informazione testuale, per l'esattezza di una serie di linee di testo. Le linee sono separate dalla coppia CRLF (ovvero dagli 'a capo' in formato DOS).
Il protocollo prevede che, una volta stabilita la connessione, sia il cliente a prendere l'iniziativa dichiarando cosa ha intenzione di fare. Per quanto ci interessa assumiamo che il cliente richieda sempre un documento inviando al server una linea terminata da CRLF come la seguente (assumendo che sia stato richiesto il documento /index.html):
GET /index.html HTTP/1.0
Il documento normalmente è riferito alla radice dei documenti del web server. Si ottiene da una URL eliminando la parte dell'host (ad esempio http://medialab.di.unipi.it/index.html richiede /index.html al server web.
Come si può osservare l'operatore GET specifica che si vuole un documento, si trova poi il nome del documento e infine la versione di HTTP che il cliente supporta.
Successivamente alla riga della richiesta il cliente invia una serie di linee ciascuna nel formato
 
Nome dell'attributo: valore


che consentono di aggiungere informazioni alla richiesta, come ad esempio il linguaggio preferito dal browser. Un esempio di header di richiesta di un browser è il seguente:
 

GET /test.html HTTP/1.0
Connection: Keep-Alive
User-Agent: Mozilla/4.04 [en] (Win95; I)
Host: 127.0.0.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*
Accept-Language: it
Accept-Charset: iso-8859-1,*,utf-8


La richiesta del cliente termina con una linea vuota. Ovviamente un Web server è libero di ignorare tutti i parametri passati così può cercare di sfruttarli per offrire una migliore qualità del servizio.
Al termine della richiesta il Web server invia un file in risposta, preceduto da un header che descrive il formato dei dati che seguono. Il formato della risposta è assolutamente analogo alla richiesta per quanto riguarda l'header, successivamente i dati vengono inviati in formato binario. Un esempio di risposta può essere:
 

HTTP/1.1 200 OK
Date: Mon, 05 Jan 1998 15:50:33 GMT
Server: Apache/1.2.0
Last-Modified: Thu, 01 Jan 1998 22:34:44 GMT
ETag: "2105e8e9-8ea-34ac1a04"
Content-Length: 2282
Accept-Ranges: bytes
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html


Anche nel caso della risposta una linea vuota delimita la fine dell'header e l'inizio dei dati.
Ovviamente se il Web Server non trova il documento risponde inviando sulla prima riga un codice di errore (quante volte avrete visto il mitico 404 File not found durante un po' di surfing?) e tipicamente invia una pagina HTML generata automaticamente che segnala l'errore in modo che il browser possa visualizzare il problema all'utente.
Qui termina il nostro piccolo viaggio in HTTP anche se ovviamente si potrebbero dire molte altre cose. D'altronde il nostro Web Server ci sta attendendo...
La gestione della connessione HTTP
Torniamo ora al nostro problema originale: la scrittura del Web server. Cerchiamo di capire con cosa vada rimpiazzata la funzione handleConnection() utilizzata prima come segnaposto. Secondo quanto visto nel paragrafo precedente bisogna leggere la richiesta del cliente e successivamente inviare la risposta. Al termine dell'invio dei dati la connessione viene chiusa ed il server è pronto ad accettare una nuova connessione.
La gestione di una connessione può essere effettuata nel seguente modo:

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();

Il metodo getRequest() si occupa di leggere la richiesta dal canale. Successivamente, se la richiesta è quantomeno nel formato HTTP, viene individuato il file richiesto relativamente alla root del server e se questo esiste viene restituito; altrimenti viene inviato un messaggio di errore.
Una tabella hash viene utilizzata per memorizzare la richiesta. Il documento richiesto viene associato all'attributo ":Get" che non può essere inviato dal cliente. Il metodo getRequest() è il seguente:
 
private Hashtable getRequest(BufferedReader in) throws IOException {
 

 String line;

  line = in.readLine();
  int firstSpace = line.indexOf(" ");
  int secondSpace = (firstSpace == -1)?-1:line.indexOf(" ", firstSpace + 1);

  if ((line == null) || !"GET ".equalsIgnoreCase(line.substring(0, 4)) ||
      (firstSpace == -1) || (secondSpace == -1))
    return null; // Invalid request

  Hashtable ret = new Hashtable();
  ret.put(":Get", line.substring(firstSpace + 1, secondSpace));
  ret.put(":Version", line.substring(secondSpace + 1));

  while (((line = in.readLine()) != null) && !line.equals("")) {
    int idx = line.indexOf(":");

    if (idx != -1)
      ret.put(line.substring(0, idx).trim(), line.substring(idx + 1).trim());
  }

  return ret;
}

Questo metodo semplicemente effettua il parsing delle linee inviate dal client e le memorizza in una tabella hash che rappresenta la richiesta inviata.
Una volta interpretata la richiesta siamo in grado di mandare la risposta. Innanzitutto si manda l'intestazione dei dati, per questo c'è il metodo sendMIMEHeader():
 
private void sendMIMEHeader(int code, 
                            String msg,
                            String type,
                            long size,
                            Writer out) 
  throws IOException {
  DateFormat df = DateFormat.getDateInstance();
  df.setTimeZone(TimeZone.getTimeZone("GMT"));

  out.write("HTTP/1.0 " + code + " " + msg + CRLF +
            "Date: " + df.format(new Date()) + CRLF +
            "Server: " + SERVER_STRING + CRLF +
            "Content-type: " + type + CRLF);
  if (size > 0)
    out.write("Content-length: " + size + CRLF);

  out.write(CRLF);
  out.flush();
}

Come si vede dal codice viene semplicemente inviato l'header della risposta. Per inviare questo header è utile la classe PrintWriter che permette il familiare println. Per l'invio dei dati in formato binario abbiamo bisogno però di un OutputStream, ecco perchè al momento dell'inizio della connessione vengono preparati due stream di output. La gestione del tipo MIME del documento è interessante: una tabella hash contiene l'associazione estensione del file/tipo MIME e viene utilizzata quando necessario.
I restanti metodi possono essere analizzati direttamente nel codice della classe server dove sono anche documentati. La loro funzione è comunque marginale e sono semplici da comprendere.
Conclusioni
Sperando di non avervi annoiato troppo con le mie chiacchere concludo questo mio articolo. Il codice mi sembra abbastanza leggibile ed è divertente collegarsi ad un server scritto in 130 righe di codice Java circa.
Pensavo di scrivere un secondo articolo a partire da questo che spieghi come può essere esteso il server qui presentato per supportare CGI (o almeno una sua apparenza), qualcosa di simile a una Servlet e come poter gestire più connessioni contemporaneamente.
Ovviamente questo tipo di software, presentato in un articolo, è incompleto e molte cose si potrebbero fare meglio. Se lo pensate basta che prendiate il codice e lo miglioriate!

  
 

MokaByte rivista web su Java

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