Dopo una piccola interruzione ricomincia il mini-corso sulla multiprogrammazione in Java

L'ultima volta avevamo visto come si poteva creare un'applicazione Client/Server che faceva dei semplici calcoli lanciati da remoto (per assurdo anche dall'altro capo del mondo).

Si era però detto che quell'applicazione non era completa. Il Server, infatti, poteva servire solo un Client per volta e, se un altro Client faceva richiesta, questo veniva messo in attesa. Ciò era evidentemente incompleto.

Pensiamo a cosa potrebbe succedere se un server HTTP servisse solo un utente alla volta. Internet cadrebbe come un castello di carte.

Cosa si può dunque fare? Semplice: basta attendere la chiamata di un Client, creare un processo che lo serva e continuare il ciclo.

Per gli amanti degli schemi (e per quelli che non hanno ancora le idee ben chiare) faccio questo diagramma: 1. Apri il Socket 2. Poniti in attesa 3. Quando arriva un Client crea il processo che lo serve e lancialo 4. Vai al punto 2 Notare che il punto n° 3 viene eseguito senza bloccare il ciclo.

Oramai siamo tutti abituati ai sistemi multitasking, quindi non ci stupiamo che la nostra macchina possa eseguire più programmi contemporaneamente.

Bene, qui abbiamo solo un'estensione della cosa. Infatti eseguiamo non solo più programmi contemporaneamente, ma addirittura più "frammenti" di programmi assieme. (per una trattazione dei concetti di programmazione multithread e multitasking si veda l'articolo di Giovanni Puliti apparso in [](../../01/index.htm) - n.d.r.).

Questi "frammenti" si chiamano processi sotto Unix, Thread sotto Windows o altri sistemi.

Per coloro che, come me, vengono dal C posso dire che, mentre gestire i processi in C (sotto Unix o sotto Win32) non è banale e richiede un codice abbastanza lungo, in Java la cosa è quasi puerile e richiede un codice minimo.

Vediamo dunque come si può creare un processo in Java.

Come ho detto è quasi banale, infatti basta creare una classe che implementi il Runnable in questa maniera: public class Server implements Runnable nella quale si scrive il server vero e proprio (ossia quel codice che gestisce le richieste del Client e risponde). Il codice va scritto (o almeno richiamato) nel membro run() che è quello che viene chiamato all'atto dell'attivazione del Thread.

Nella classe principale (quella dove c'è il main, tanto per intenderci) dobbiamo scrivere questo codice: p = new Server(); new Thread(p).start(); in questa maniera viene creato un processo e viene lanciato senza dover fermare il ciclo principale. Sempre per chiarire le idee a quelli che vengono dal C, dirò che queste tre righe che ho scritto corrispondono a tutto il casino ops confusione (ndr) che bisogna fare tra fork() e signal() per gestire i processi e cancellare i processi fantasmi.

Non mi dilungo ulteriormente perché qui stiamo parlando di Java e non di C, ma per i profani basti sapere che per fare la stessa cosa avrei dovuto scrivere mezza paginetta di codice.

Vediamo dunque come si può fare (con una breve introduzione teorica) una piccola applicazione che, quando un Client si collega, gli manda la stringa "Scrivi qualcosa", accetta righe di testo e le rimanda al Client.

È molto semplice a livello concettuale, ma già introduce quanto serve per fare cose più complesse. Innanzi tutto una precisazione, in questo articolo ci limiteremo a considerare il server.

Per mandare dati ad esso (e vedere cosa ritorna) è possibile utilizzare l'applicazione Telnet che tutti noi abbiamo (anche senza saperlo) sulle nostre macchine. Basterà infatti lanciare telnet.exe e dare come nome del server a cui collegarsi l'indirizzo: 127.0.0.1 che corrisponde alla macchina locale e indicando la porta che si va a utilizzare (nel nostro esempio la 12000). Bene, veniamo dunque al punto.

Il server dovrà fare queste operazioni:

  • Iniziare la comunicazione Socket, definendosi come Server, ascoltando su una porta, etc.
  • Attendere un client.
  • Appena qualcuno si fa vivo sulla porta specificata, creare una nuova istanza della classe Server che provvedere ad eseguire le operazioni con il Client.
  • Creare un nuovo Thread con la classe appena istanziata
  • Riprendere il ciclo dal punto 2

La classe Server, per conto suo, dovrà semplicemente attendere una riga di testo e, se è valida (se il Client non si è disconnesso) rimandarla indietro. Scriviamo dunque il codice della classe Server che è il più semplice.

import
java.io.*;

import
java.lang.*;

import
java.net.*;

 

 

public
class Server implements Runnable{

       
Socket Channel;

       
public DataInputStream in;

       
public PrintStream out;

 

 

       
public void run(){

               
String c="";

               
boolean Exit=false;

 

 

               
try {

                       
/* Crea gli Stream di Input/Output */

                       
out = new PrintStream(Channel.getOutputStream());

                       
in = new DataInputStream(Channel.getInputStream());

 

 

                       
out.println("Scrivi qualcosa");

                       
while(!Exit) {

                               
/* Vero ciclo del server.

                               
Prende una stringa e la rimanda indietro */

                               
try{

                                       
c=in.readLine();

                                       
if(c==null)

                                               
Exit=true;

                                       
else

                                               
out.println(c);

                               
}

                               
catch(IOException e){

                                       
Exit=true;

                               
}

                       
}

                       
in.close();

                       
out.close();

               
}

               
catch(IOException e){}

       
}

}

Come si può notare, dopo aver definito gli Stream di Input/Output derivati dal Socket passato dalla procedura principale il ciclo è banale. Legge una stringa, controlla se è valida, se lo è la stampa, altrimenti esce dal ciclo e poi dalla classe. A questo punto arriviamo al cuore del nostro programma: la classe principale. Come si è detto in precedenza, è necessario iniziare la comunicazione Socket, mettersi in attesa e, al momento buono chiamare un Thread con la classe Server. Tutto ciò si riduce a questo codice:

import
java.awt.*;

import
java.io.*;

import
java.lang.*;

import
java.net.*;

 

 

public
class Prova{

       
public static void main(String args[]){

               
Server p;

               
Socket channel;

               
ServerSocket server;

 

 

               
try{

               
/* Inizializza il Socket. Porta 12000 con TimeOut di 15 secondi */

                       
server = new ServerSocket(12000, 15);

                       
while(true){

                               
/* Si pone in attesa */

                               
channel = server.accept();

                               
p = new Server();

                               
p.Channel=channel;

                               
new Thread(p).start();

                       
}

               
}

               
catch (IOException e){}

       
}

}

Un codice veramente semplice. La parte "incriminata" è:

p = new Server();

p.Channel=channel;

new Thread(p).start();

dove creo una nuova istanza, gli mando il canale usato e ne creo un Thread. Tutto qua! Ora basta creare due files, chiamati Server.java e Prova.java e compilarli. Lanciando la classe Prova (scrivendo da Prompt di DOS: java Prova) avremo lanciato il nostro Server e potremo provarlo lanciando telnet.exe con le impostazioni citate prima. In questo articolo non abbiamo citato l'altra possibilità di creare Thread. In effetti chiunque abbia oramai imparato un po' di teoria sulla programmazione a oggetti l'avrà notato. Si può anche creare una classe derivata dalla Thread sovrascrivendo il membro run().

A questo punto per lanciare il nuovo Thread devo semplicemente scrivere queste righe di codice:

Server p;

p=new Server();

p.run();

e tutto funziona allo stesso modo. In effetti cambia molto poco (a livello pratico) tra le due implementazioni. Cambia molto a livello concettuale perché nel primo caso (quello che ho usato per l'applicazione) ho una classe che può essere usata come Thread, mentre nel secondo caso ho una classe che viene solo usata come Thread. Bene, come prima applicazione di prova è sufficiente.