MokaByte Numero 36  - Gennaio 2000   

Introduzione alle Socket

di
Lorenzo Bettini

Parliamo delle Socket, il meccanismo standard per la comunicazione in rete. 


 


Le socket sono il meccanismo e l'astrazione principale per comunicare in rete; Java, nella sua libreria, mette a disposizione diverse classi per l'utilizzo delle Socket. Ma vediamo prima che cosa sono le socket, e quali sono i concetti di base principali che permettono di utilizzarle. Vedremo anche le operazioni tipiche dei programmi client e di quelli server, e come questi utilizzano le socket.


Introduzione

La volta scorsa avevamo visto una panoramica di alcuni servizi per le applicazioni distribuite. Questa Introdurremo le Socket, un meccanismo per comunicare in rete, indipendentemente dal dispositivo fisico tramite il quale effettuiamo la connessione.

Ricordiamo brevemente il paradigma Client-Server: un programma chiamato Client richiede dei servizi ad un altro programma chiamato Server. Quest’ultimo è in ascolto di richieste da parte dei client, esegue tali richieste con le risorse che ha a disposizione, e rispedisce, se richiesto i risultati al client. Tali programmi possono risiedere anche su computer diversi: tutto questo è trasparente ad entrambi.

Esistono tuttora molte applicazioni, soprattutto in Internet, che fanno uso di questo paradigma:

  • Telnet: se sulla nostra macchina si ha disposizione il programma telnet (programma client), è possibile operare su un computer remoto come si opera su un computer locale. Questo è possibile se sulla macchina remota è presente un programma server in grado di esaudire le richieste del client telnet.
  • Ftp: tramite un client ftp si possono copiare e cancellare file su un computer remoto, purché qui sia presente un server ftp.
  • Web: il browser è un cliente web, che richiede pagine web ai vari computer su cui è installato un web server, che esaudirà le richieste spedendo la pagina desiderata.

Il meccanismo di astrazione per la comunicazione in rete è rappresentato dal paradigma socket presentato per la prima volta nella Berkeley Software Distribution (BSD) della University of California a Berkeley.

Una socket è come una porta di comunicazione e non è molto diversa da una presa elettrica: tutto ciò che è in grado di comunicare tramite il protocollo standard TCP/IP, può collegarsi ad una socket e comunicare tramite questa porta di comunicazione, allo stesso modo in cui un qualsiasi apparecchio che funziona a corrente può collegarsi ad una presa elettrica e sfruttare la tensione messa a disposizione. Nel caso delle socket, invece dell’elettricità, nella rete viaggiano pacchetti TCP/IP. Tale protocollo e le socket forniscono quindi un’astrazione, che permette di far comunicare dispositivi diversi che utilizzano lo stesso protocollo.

Ovviamente Java mette a disposizione alcune classi per l’utilizzo delle socket, tra cui la classe Socket. Usando questa classe un client può stabilire un canale di comunicazione con un host remoto. Si potrà comunicare attraverso questo canale utilizzando stream particolari, specializzati per le socket.

Vediamo adesso quali sono le operazioni che un client ed un server devono effettuare con le socket per stabilire una connessione (nel caso del client) e per accettare connessioni (nel caso del server).

Il client

Quindi un client, per comunicare con un host remoto usando il protocollo TCP/IP, dovrà creare per prima cosa un oggetto Socket con tale host. Si dovrà specificare l’indirizzo IP dell’host, e il numero di porta. Sull’host remoto dovrà esserci un server che è in "ascolto" su tale porta. La classe Socket mette a disposizione due costruttori:

Socket( String host, int port ) throws IOException
Socket( InetAddress address, int port ) throws IOException

InetAddress è una classe di utilità per la gestione di indirizzi Internet (ad esempio ottenere l’indirizzo IP numerico, dato un indirizzo sotto forma di stringa).

Una volta creato un oggetto Socket, si possono ottenere gli stream ad esso associati tramite i metodi della classe Socket:

InputStream getInputStream() throws IOException
OutputStream getOutputStream() throws IOException

A questo punto la comunicazione può avere inizio: il client può scrivere sull’OutputStream, come si fa con un normale stream, e ricevere dati dal server leggendo dall’InputStream.

Di seguito viene mostrato un estratto di codice (quasi pseudo codice) che vorrebbe dare un'idea delle operazioni da compiere per aprire una socket con un server che si trova ad un certo indirizzo (host) ed è in ascolto su una certa porta (port).

protected String host;
protected int port;

protected DataInputStream in;
protected DataOutputStream out;

protected Socket connect () throws IOException {
  System.err.println ("Connessione a " + host + ":" + port + "...");
  Socket socket = new Socket (host, port);
  System.err.println ("Connessione avvenuta.");

  out = new DataOutputStream (socket.getOutputStream ());
  in = new DataInputStream (socket.getInputStream ());

  return socket;
}

Effettuata la connessione possiamo ottenere gli stream associati con i metodi della classe Socket getOutputStream e getInputStream. Creiamo poi un DataOutputStream ed un DataInputStream su tali stream ottenuti (effettivamente per ottimizzare la comunicazione in rete si dovrebbe prima creare uno stream bufferizzato sullo stream di output, ma questi dettagli al momento non ci interessano).

A questo punto, se ad esempio abbiamo stabilito una connessione con un web server, si deve richiedere il file al server e quindi si spedisce tale richiesta tramite lo stream di output:

out.writeBytes ("GET " + file + " HTTP/1.0\r\n\r\n");

A questo punto non resta che mettersi in attesa, sullo stream di input, dell’invio dei dati da parte del server:

while ((input = in.readLine ()) != null)

Il server

Vediamo adesso quali sono le operazioni tipiche di un programma Server.

Un server rimane in attesa di connessioni su una certa porta, ed ogni volta che un client si connette a tale porta, il server ottiene una socket, tramite la quale può comunicare col client. Il meccanismo messo a disposizione da Java per questo è la classe ServerSocket, tramite la quale il server può appunto accettare connessioni dai client attraverso la rete. I passi tipici di un server saranno quindi:

  • creare un oggetto di classe ServerSocket specificando un numero di porta locale, cioè la porta in cui il server rimarrà in ascolto di richieste di connessioni.
  • attendere (tramite il metodo accept di suddetta classe) connessioni dai client
  • usare la socket ottenuta ad ogni connessione, per comunicare col client.

Infatti il metodo accept della classe ServerSocket crea un oggetto Socket per ogni connessione. Il server potrà poi comunicare, come fa un client: estraendo gli stream di input ed output dalla socket.

I costruttore della classe ServerSocket sono:

ServerSocket(int port) throws IOException
ServerSocket(int port, int count) throws IOException

A parte il parametro che specifica la porta (già spiegato), il parametro count permette di specificare il numero di richieste di connessioni messe in coda dal sistema operativo (il default per questo parametro è 50). Infatti dopo che il ServerSocket è stato creato il sistema operativo si mette subito in attesa di connessioni; queste richieste saranno messe in una coda e saranno rimosse una per volta, ad ogni chiamata, da parte del server, del metodo

Socket accept() throws IOException

Quindi count non limita il numero di connessioni che un server è in grado di mantenere, ma il numero di richieste di connessioni che verranno messe in coda, se il server ci mette molto per accettare nuove connessioni. Infine se come numero di porta viene specificato 0 il sistema operativo seleziona un numero di porta valido e non occupato. E’ comunque possibile ottenere il numero di porta sul quale il ServerSocket è in ascolto di connessioni utilizzando il metodo

int getLocalPort()

La socket ottenuta dal metodo accept è uguale a quella ottenuta dal client alla connessione, e quindi potrà essere utilizzata allo stesso modo per recuperare gli stream ad essa associata e per comunicare col client.

E' bene far notare che se il server accetta più connessioni (di solito è quello che succede: spesso il server è multithreaded), il server otterrà una socket differente per ogni client.

L’ultimo metodo della classe ServerSocket è

void close() throws IOException

Che chiude il ServerSocket, ma non le connessioni che sono stare stabilite (queste dovranno essere chiuse chiamando il metodo close della classe Socket). Tipicamente quando una socket viene chiusa da un lato, sull’altro lato si avrà una IOException.

Anche l'esempio di codice che segue vuole semplicemente mostrare un'idea del codice Java che un server esegue per mettersi in attesa di connessioni da parte di client. Una volta ottenuta una connessione il server stampa semplicemente un messaggio di benvenuto.

System.out.println ("Server in partenza sulla porta " + port);
ServerSocket server = new ServerSocket (port);
System.out.println ("Server partito sulla porta " + 
	server.getLocalPort() ) ;

System.out.println ("In attesa di connessioni...");
Socket client = server.accept ();
System.out.println ("Richiesta di connessione da " + 
	client.getInetAddress () ) ;

InputStream i = client.getInputStream ();
OutputStream o = client.getOutputStream ();
PrintStream p = new PrintStream (o) ;
p.println("BENVENUTI.");

Conclusioni

Andrea vi mostrerà adesso qualche semplice esempio funzionante di utilizzo di Socket; vedrete come in Java è molto semplice instaurare una comunicazione in rete fra un client ed un server. Provate a fare la stessa cosa in C, e vedrete che le operazioni da compiere sono molte di più...

a presto :-)

Lorenzo

P.S. I listati Java sono stati formattati col programma java2html scaricabile al sito http://www.gnu.org/software/java2html/java2html.html.