MokaByte Numero 37  - Febbraio 2000   

Introduzione alle Socket
(II parte)

di
Lorenzo Bettini

Continuiamo a parlare 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. Vediamo adesso una breve panoramica delle classi utilizzate per comunicare in rete in Java tramite le socket, in particolare la classe InetAddress e, ovviamente, la classe Socket.


Introduzione

La volta scorsa avevamo introdotto le Socket, un meccanismo di astrazione per la comunicazione in rete, indipendentemente dal dispositivo fisico tramite il quale effettuiamo la connessione.

Questa volta daremo una breve panoramica delle classi e dei metodi utilizzati per comunicare in rete tramite le socket, in particolare descriveremo molto brevemente la classe InetAddress e, ovviamente, la classe Socket; con Andrea invece vedrete un esempio vero e proprio del loro utilizzo: l'implementazione di un piccolo sistema di chat (client-server), in cui il server è in grado di gestire più connessioni contemporaneamente.

Ricordiamo che 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.

Come si è detto, nel paradigma Client-Server, sia il server che il client sono dei programmi che possono girare su macchine diverse collegate in rete. Il client deve conoscere l’indirizzo del server, ed il particolare protocollo di comunicazione utilizzato dal server. L’indirizzo in questione è il classico indirizzo IP.

Un client, quindi, per comunicare con un server usando il protocollo TCP/IP dovrà per prima cosa creare una socket con tale server, specificando l’indirizzo IP della macchina su cui il server è in esecuzione, ed il numero di porta sulla quale il server è in ascolto. Il concetto di porta permette ad un singolo computer di servire numerosi client contemporaneamente: su uno stesso computer possono essere in esecuzione server diversi, in ascolto su porte diverse. Un server "rimarrà in ascolto" su una determinata porta, finché un client non creerà una socket con la macchina del server, specificando quella porta. Se si vuole un’analogia si può pensare al fatto che più persone abitano allo stesso indirizzo, ma a numeri civici diversi. In questo caso i numeri civici rappresenterebbero le porte.

Una volta che il collegamento col server, tramite la socket è avvenuto, il client può iniziare a comunicare col server, sfruttando la socket creata. A questo punto, come già accennato, a collegamento avvenuto, si instaura un protocollo di livello superiore, che dipende da quel particolare server: il client deve conoscere il protocollo di comunicazione del server, per richiedergli dei servizi.

Il numero di porta è un intero compreso fra 1 e 65535. Il TCP/IP riserva le porte minori di 1024 a servizi standard. Gli esempi più noti ed utilizzati sono: la porta 21 è riservata all’FTP, la 23 al Telnet, la 25 alla posta elettronica, la 80 all’HTTP (il protocollo delle pagine web), la 119 alle news di rete. Si deve tenere a mente che una porta in questo contesto non ha niente a che vedere con le porte di una macchina (porte seriali, parallele, ...), ma è un’astrazione utile per smistare informazioni a più server.

Vediamo adesso le principali classi messe a disposizione da Java, nel pacchetto java.net per la gestione di comunicazioni in rete.

La classe InetAddress

Come si sa un indirizzo Internet è costituito da 4 numeri (da 0 a 255) separati ciascuno da un punto. Spesso però, quando si deve accedere ad un particolare host, invece di specificare dei numeri, si utilizza un nome, che corrisponde a tale indirizzo (es www.myhost.it). La traduzione dal nome all’indirizzo numerico vero e proprio è compito del servizio Domain Name Service, spesso abbreviato con DNS.

Senza entrare nei dettagli di questo servizio, basti sapere che la classe InetAddress mette a disposizione diversi metodi per astrarre dal particolare tipo di indirizzo specificato (a numeri o a lettere), occupandosi lei di effettuare le dovute traduzioni.

La classe ha la seguente descrizione:

public final class InetAddress
extends Object
implements Serializable

La classe non mette a disposizione nessun costruttore: l’unico modo per creare un oggetto InetAddress è tramite l’utilizzo di metodi statici, descritti di seguito.

public static InetAddress getByName(String host) throws UnknownHostException

Restituisce un oggetto InetAddress rappresentante l'host specificato nel parametro host. L'host può essere specificato sia come nome, che come indirizzo numerico. Se si specifica null come parametro, ci si riferisce all'indirizzo di default della macchina locale.

public static InetAddress[] getAllByName(String host) throws UnknownHostException

Tale metodo è simile al precedente, ma restituisce un array di oggetti InetAddress: spesso dei siti web molto trafficati registrano lo stesso nome con indirizzi IP diversi. Con questo metodo si otterranno tanti InetAddress quanti sono questi indirizzi registrati.

public static InetAddress getLocalHost() throws UnknownHostException

Viene restituito un InetAddress corrispondente alla macchina locale. Se tale macchina non è registrata, oppure è protetta da un firewall, l'indirizzo è quello di loopback: 127.0.0.1

In tutti questi metodi, viene sollevata l'eccezione UnknownHostException se l'host specificato non è stato risolto (tramite il DNS).

public String getHostName()

Restituisce il nome dell'host che corrisponde all'indirizzo IP dell'InetAddress. Se il nome non è ancora noto (ad esempio se l'oggetto è stato creato specificando un indirizzo IP numerico), verrà cercato tramite il DNS; se tale ricerca fallisce, verrà restituito l'indirizzo IP numerico (sempre sotto forma di stringa).

public String getHostAddress()

Simile al precedente: restituisce però l'indirizzo IP numerico, sotto forma di stringa, corrispondente all'oggetto InetAddress.

public byte[] getAddress()

L'indirizzo IP numerico restituito sarà sotto forma di byte. L'ordinamento dei byte è high byte first (che è proprio l'ordinamento tipico della rete).

La classe Socket

Per creare una socket con un server in esecuzione su un certo host, basta creare un oggetto di classe Socket, specificando nel costruttore l'indirizzo internet dell'host, ed il numero di porta. Dopo che l'oggetto Socket è stato costruito è possibile ottenere (tramite appositi metodi) due stream (uno di input ed uno di output). Tramite questi stream è possibile comunicare con l'host, e ricevere messaggi da questo. Qualsiasi metodo che prende in ingresso un InputStream (o un OutputStream) può comunicare con l'host in rete.

La classe Socket ha la seguente descrizione:

public class Socket 
extends Object

ed i seguenti costruttori

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

Viene creato un oggetto Socket connettendosi con l'host specificato (sotto forma di stringa o di InetAddress) alla porta specificata. Se sull'host e sulla porta specificata non c'è un server in ascolto, verrà generata un'IOException (verrà specificato il messaggio Connection Refused).

I metodi principali della classe Socket sono:

public InetAddress getInetAddress()

Restituisce un oggetto InetAddress corrispondente all'indirizzo dell'host col quale la socket è connessa.

public InetAddress getLocalAddress()

Restituisce un oggetto InetAddress corrispondente all'indirizzo locale, alla quale la socket è collegata.

public int getPort()

Restituisce il numero di porta dell'host remoto col quale la socket è collegata.

public int getLocalPort()

Restituisce il numero di porta locale col quale la socket è collegata. Quando si crea una socket, come si è già detto, ci si collega con un server su una certa macchina, che è in ascolto su una certa porta. Anche sulla macchina locale, sulla quale viene creata la socket, si userà una porta per tale socket. Tale socket sarà assegnata dal sistema operativo, scegliendo il primo numero di porta non occupato. Si deve ricordare infatti che ogni connessione TCP consiste sempre di un indirizzo locale e di uno remoto, e di un numero di porta locale ed un numero di porta remoto. Tale metodo non viene usato molto spesso; può essere utile quando un programma, già collegato con un server remoto, crea lui stesso un server. Per tale nuovo server può non essere specificato un numero di porta (può non essere importante specificare un determinato numero di porta); a tal punto si prende il numero di porta assegnato dal sistema operativo. Con questo metodo si riesce ad ottenere tale numero di porta, che potrà ad esempio essere comunicato ad altri programmi su altri host.

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

Tramite questi metodi si ottengono gli stream, tramite i quali è possibile comunicare attraverso la connessione TCP instaurata con la creazione della socket. Tale comunicazione sarà quindi basata sull'utilizzo degli stream, utilizzati di continuo nella programmazione in Java. Come si può notare vengono restituite un InputStream e un OutputStream, che sono classi astratte. In realtà vengono restituiti dei SocketInputStream e SocketOutputStream, ma tali classi non sono pubbliche. Quando si comunica attraverso connessioni TCP, i dati vengono suddivisi in pacchetti (pacchetti IP appunto), quindi è consigliabile, non utilizzare tali stream direttamente, ma sarebbe meglio costruire stream bufferizzati su tali stream: in questo modo si eviterà di avere pacchetti contenenti poche informazioni (infatti quando si inizia a scrivere i primi byte su tali stream, verranno spediti dei pacchetti con pochi byte, o forse anche un solo byte!).

public synchronized void close() throws IOException

Con questo metodo viene chiusa la socket (e quindi la connessione), e tutte le risorse che erano in uso, verranno rilasciate. Dati bufferizzati verranno comunque spediti, prima della chiusura del socket. La chiusura di uno dei due stream associati alla socket, comporterà automaticamente la chiusura della socket stessa.

Come si può notare nei metodi precedenti può essere lanciata un'IOException, a significare che ci sono stati dei problemi sulla connessione. Questo succede quando uno dei due programmi che utilizza la socket chiude la connessione: l'altro programma potrà ricevere una tale eccezione.

Conclusioni

Come si può vedere, quindi, connettersi e comunicare con un server è molto semplice: basta creare una socket specificando host e porta (queste informazioni devono essere conosciute), ottenere e memorizzare gli stream della socket richiamando gli appositi metodi della socket, ed utilizzarli per comunicare (sia per spedire informazioni, che per ricevere informazioni), magari dopo aver bufferizzato tali stream.

Andrea vi mostrerà adesso qualche esempio più complesso di utilizzo di Socket; vedrete come in Java è molto semplice instaurare una comunicazione in rete fra un client ed un server, e come un server sia in grado di gestire più client contemporaneamente.

a presto :-)

Lorenzo