Soket
e TCP
Le classi URL e URLConnection offrono un meccanismo
di alto livello per l'accesso a risorse Internet, perlopiù
orientato al protocollo applicativo HTTP. Nella programmazione
di rete capita abbastanza spesso di voler realizzare
applicazioni proprietarie basate su protocolli applicativi
differenti. In questo caso è necessario appoggiarsi
direttamente sul protocollo TCP, già descritto
per sommi capi il mese scorso, attraverso le socket,
uno strumento di comunicazione molto versatile presente
oramai su qualunque piattaforma. Prima di descrivere
le basi della programmazione basata su soket è
necessario introdurre il paradigma client-server, su
cui le applicazioni basate su socket sono fondate.
Il paradigma client-server
Il paradigma client-server è oramai diventato
sinonimo di programmazione di rete. In realtà
si tratta di un'architettura applicativa più
generica in cui è possibile riscontrare un fornitore
di servizi, il server appunto, e un fruitore di tali
servizi, che prende il nome di client.
Nella
programmazione di rete, client e server comunicano attraverso
la rete grazie alle socket e al protocollo TCP, che
fornisce un meccanismo di comunicazione punto a punto
affidabile basato sugli stream. L'applicazione client-server
più familiare è il web, in cui esistono
server HTTP che offrono l'accesso remoto ad una serie
di documenti HTML. In questo caso il client è
un browser web, che si connette al server per scaricare
una pagina. Altri esempi sono FTP, in cui il client
può accedere ad un file system remoto presente
su un apposito server ed effettuare le consuete operazioni
di lettura e scrittura.
Socket
Una socket è il punto di contatto di una comunicazione
a due vie tra due programmi che girano su computer di
rete. Sul lato server, la socket è legata (bound)
ad una porta predefinita, in modo da permettere al client
di identificare l'applicazione con cui connettersi.
In inglese il termine socket denota le prese di corrente:
la metafora di riferimento è esattamente il servizio
di erogazione della corrente elettrica, a cui ci si
allaccia attraverso un'apposita spina.
In
un'applicazione client-server basata su socket, il server
resta in attesa di una connessione su una determinate
porta. Quando un client si connette, il server accetta
la connessione mediante un'apposita direttiva di accept(),
che connette la socket del client ad una porta libera,
differente da quella di ascolto che rimane disponibile
per altre connessioni. Solitamente infatti, i programmi
server sono concorrenti, in modo da poter servire più
di un client per volta. Sul lato client , quando la
connessione viene accettata, viene creata una socket
che il client può usare per comunicare con il
server.
Dal
momento che il protocollo TCP è un protocollo
standard di Internet, non è necessario che entrambi
i programmi siano scritti nello stesso linguaggio o
girino sulla stessa piattaforma: spesso è anzi
vero il contrario
La
comunicazione tra client e server avviene mediante scambio
di messaggi basati su un protocollo, binario o testuale,
definito dall'utente. Lo scambio di messaggi avviene
attraverso una coppia di stream del tutto equivalenti
a quelli di I/O già studiati nei capitoli precedenti.
Le
classi URL e URLConnection sono ovviamente basate su
socket, anche se la cosa non è visibile al programmatore.
La
classe InetAddress
La classe InetAddress incapsula un indirizzo internet,
sia esso un indirizzo IP standard o un nuovo indirizzo
IPv6. Questa classe non dispone di costruttori, ma unicamente
di metodi factory statici che permettono di creare un
InetAddress a partire da un nome simbolico o da altri
parametri. Per quanto concerne gli scopi del presente
trattato, l'unico metodo interessante è il seguente:
static
InetAddress getByName(String host) throws UnknownHostException
Questo
metodo effettua il lookup del nome simbolico su DNS
e restituisce un opportuno InetAddress; se l'host non
viene trovato, il metodo genera una UnknownHostException.
La classe dispone anche di numerosi metodi di interrogazione,
che possono essere trovati nella documentazione.
La
classe ServerSocket
Il package java.net dispone di una coppia di classi,
Socket e ServerSocket, che implementano i due lati della
connessione tra un programma Java e un altro programma
presente in rete in modo indipendente dalla piattaforma.
La classe ServerSocket dispone dei seguenti costruttori:
ServerSocket(int
port) throws IOException
ServerSocket(int port, int backlog) throws IOException
Il
primo di questi costruttori richiede come argomento
unicamente il numero di porta su cui ascoltare la connessione,
mentre il secondo permette di specificare anche la lunghezza
massima della coda in cui vengono messi in attesa i
client che tentano di connettersi nel frattempo. Come
di consueto nelle classi di I/O, se qualcosa non va
viene lanciata una IOException. Un altro costruttore
permette di specificare un indirizzo IP locale, nel
caso la macchina server disponga di più schede
di rete:
ServerSocket(int
port, int backlog, InetAddress bindAddr) throws IOException
Tra
i vari metodi della classe è sufficiente conoscerne
due:
Socket
accept() throws IOException
void close()throws IOException
Il
primo è un metodo bloccante, che mette la socket
in attesa sulla porta di rendez-vous fino a quando un
client prova a connettersi; quando questo avviene, viene
restituita una socket collegata al client; il secondo
è il classico metodo close(), che permette di
chiudere l'attività di una socket. Entrambi i
metodi possono generare una IOException.
La
classe Socket
La classe Socket implementa una socket lato client.
I costruttori richiedono la specifica di un host, in
forma simbolica o come oggetto InetAddress, e un numero
di porta:
Socket(InetAddress
address, int port) throws IOException
Socket(String host, int port) throws IOException
Come
di consueto, se la connessione non andasse a buon fine
viene generata una IOException. Altri costruttori permettono
di gestire le situazioni in cui la macchina dispone
di più di una scheda di rete, e quindi di indirizzi
differenti:
Socket(InetAddress
address, int port, InetAddress localAddr, int localPort)
throws IOException
Socket(String host, int port, InetAddress localAddr,
int localPort) throws IOException
I
metodi più importanti di questa classe sono quelli
che permettono di aprire gli stream di input e output
con l'host remoto:
InputStream
getInputStream() throws IOException
OutputStream getOutputStream() throws IOException
Infine
il classico metodo close(), da invocare al termine della
sessione, dopo aver chiuso gli stream di I/O:
void
close() throws IOException
Tutti
i metodi presentatiu possono generare una IOException
in caso di problemi.
Programma
Client-Server
Un aspetto sorprendente di Java è la possibilità
di realizzare con poche righe un completo programma
client-server basato su socket. La classe ReverseServer
implementa un servizio non molto utile sul piano pratico,
ma molto interessante sul piano didattico:
import
java.io.*;
import java.net.*;
public
class ReverseServer {
public static void main(String argv[]) throws IOException
{
ServerSocket server = new ServerSocket(5000);
while ( true )
try {
System.out.println("In attesa di connessione...");
Socket client = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream()));
while ( true ) {
String s = in.readLine();
if ( s == null )
break;
else {
System.out.println(client + ". Messaggio: "
+ s);
StringBuffer reverse = new StringBuffer(s).reverse();
out.println(reverse);
out.flush();
}
}
in.close();
out.close();
client.close();
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
Come
prima cosa viene creata una socket server legata alla
porta 5000: se a questo livello venisse generata una
IOException, la sua gestione viene delegata alla JVM.
A questo punto viene avviato un ciclo while infinito:
tipicamente un programma server gira a ciclo continuo,
come in questo caso. All'interno del ciclo while viene
aperto un blocco try-catch al cui interno vengono svolte
tutte le operazioni relative alla gestione di un client:
per prima cosa viene stampato un messaggio a console,
quindi viene chiamata la accept(), che mette in attesa
il server fino a quando non avviene un tentativo di
connessione; se la connessione va a buon fine, la accept()
genera la socket client. Seguono l'estrazione degli
stream di Input/Output, che per comodità vengono
concatenati ad appositi Reader e Writer, quindi inizia
il ciclo while che costituisce il dialogo vero e proprio
con il client. Il server legge una riga di testo dallo
stream di input, verifica se è uguale a null
(segno che la comunicazione è stata interrotta),
e in caso contrario stampa su console le informazioni
della socket client seguite dal messaggio ricevuto;
quindi inverte la stringa ricevuta attraverso una chiamata
all'apposito metodo di StringBuffer rispedisce al client
questa versione rielaborata dell'input. La chiamata
alla flush() è necessaria, dal momento che il
sistema operativo, e in generale gli stream, tendono
a trattenere i dati fino a quando non raggiungono un
volume sufficiente a giustificarne la spedizione.
Quando
il client chiude la connessione, l'istruzione break
provoca l'uscita dal ciclo while, e l'esecuzione delle
istruzioni che chiudono gli stream di I/O e la socket
client. Se durante questa interazione qualcosa andasse
storto, il blocco catch raccoglie la IOException, stampa
il contenuto dello stack e comincia una nuova iterazione.
Il programma server va lanciato prima del client con
il seguente comando da console:
java
ReverseServer
Il
programma client è lievemente più semplice
del programma server:
import
java.io.*;
import java.net.*;
public
class ReverseClient {
public static void main(String argv[]) {
if ( argv.length != 1 )
System.out.println("Usage: java ReverseClient <hostname>");
else
try {
Socket client = new Socket(argv[0], 5000);
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream()));
BufferedReader lineReader = new BufferedReader(new InputStreamReader(System.in));
while ( true ) {
String line = lineReader.readLine();
if ( line.equals("!q") )
break;
else {
out.println(line);
out.flush();
System.out.println(in.readLine());
}
}
in.close();
out.close();
client.close();
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
In
primo luogo viene effettuato un controllo sul numero
dei parametri: esso infatti richiede, al momento dell'esecuzione,
la specifica del nome dell'host su cui gira il programma
server. Se il client viene eseguito sulla stessa macchina
su cui è in esecuzione il programma server, sarà
necessario specificare la stringa "localhost",
che denota il dispositivo di loopback, ossia la connessione
di una macchina con se stessa:
java
ReverseClient localhost
Da
notare che anche se client e server girano sulla stessa
macchina, utilizzano due console differenti e di conseguenza
due spazi di indirizzamento diversi. In breve, si tratta
di una valida simulazione di una comunicazione client-server
reale.
Dopo
il controllo sul numero dei parametri viene aperto un
blocco try-catch al cui interno vengono svolte le operazioni
di connessione: dapprima viene creata la socket client,
quindi vengono estratti gli stream di I/O; a questo
punto viene aperto un BufferedReader connesso allo stream
di input di sistema, ossia la console. In questo modo
è possibile leggere ciò che l'utente scrive
a console. A questo punto viene avviato un ciclo while
infinito, al cui interno viene eseguita l'interazione
con il server: dapprima viene letto quanto scritto dall'utente
su console, quindi si verifica se questo corrisponde
alla stringa di uscita "!q", in caso contrario
la stringa viene inviata al server e la risposta viene
stampata a console. Si noti anche in questo caso il
ricorso alla flush(), per gli stessi motivi già
spiegati per il server. Ecco una tipica interazione
tra client e server:
C:\>java
ReverseClient localhost
Scrivi qualcosa, poi premi invio:
ciao
oaic
Scrivi qualcosa, poi premi invio, oppure scrivi !q per
uscire:
Tanto va la gatta al lardo che ci lascia lo zampino
onipmaz ol aicsal ic ehc odral la attag al av otnaT
Scrivi qualcosa, poi premi invio, oppure scrivi !q per
uscire:
Rosso di sera, bel tempo si spera
areps is opmet leb ,ares id ossoR
Scrivi qualcosa, poi premi invio, oppure scrivi !q per
uscire:
!q
C:\>
La
console del server mostrerà un output simile
al seguente:
C:\java
-ea ReverseServer
In attesa di connessione...
Socket[addr=/127.0.0.1,port=1030,localport=5000]. Messaggio:
ciao
Socket[addr=/127.0.0.1,port=1030,localport=5000]. Messaggio:
Tanto va la gatta al lardo che ci lascia lo zampino
Socket[addr=/127.0.0.1,port=1030,localport=5000]. Messaggio:
Rosso di sera, bel tempo si spera
In attesa di connessione...
Si
tenti di lanciare due client contemporaneamente: il
secondo resterà in attesa fino a quando il primo
non ha terminato la sua interazione. Per servire più
di un client per volta è necessario ricorrere
alla programmazione concorrente, come verrà mostrato
nel prossimo paragrafo.
Programma
Client-Server concorrente
La versione concorrente del server si compone di due
classi: la prima svolge il ruolo di server di rendez-vous:
import
java.io.*;
import java.net.*;
public
class MultithreadReverseServer {
public static void main(String argv[]) throws IOException
{
System.out.println("In attesa di connessione...");
ServerSocket server = new ServerSocket(5000);
while ( true ) {
try {
Socket client = server.accept();
Thread t = new Thread(new ThreadServer(client));
t.start();
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
}
In
questa prima classe viene creata la socket server, sulla
porta 5000. Quindi, all'interno del solito ciclo while
infinito, il server attende una connessione client attraverso
la accept(), quindi passa la socket appena ottenuta
ad un'apposita classe ThreadServer, che lavora su un
thread separato: dopo l'avvio, mediante l'apposito metodo
start(), il server di rendez-vous si rimette in attesa
per un'altra connessione. La classe ThreadServer, realizzazione
dell'interfaccia Run-nable, gestisce l'interazione con
un client in modo indipendente dal server principale:
import
java.io.*;
import java.net.*;
public
class ThreadServer implements Runnable {
private Socket client;
public ThreadServer(Socket client) {
this.client = client;
}
public void run() {
while ( true ) {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream()));
while ( true ) {
String line = in.readLine();
if ( line == null )
break;
else {
System.out.println(client + ". Messaggio: "
+ line);
StringBuffer reverse = new StringBuffer(line).reverse();
out.println(reverse);
out.flush();
}
}
in.close();
out.close();
}
catch (IOException ioe) {
ioe.printStackTrace();
break;
}
}
try {
client.close();
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
La
classe ThreadServer contiene codice equivalente a quello
del ciclo più interno della classe ReverseServer,
ossia il ciclo che gestisce l'interazione con un particolare
client. Quando la connessione cade o viene chiusa, il
metodo run() chiude gli stream e la socket e termina
la sua esecuzione. Questo server accetta connessioni
dallo stesso client visto in precedenza, tuttavia, a
differenza di quanto avveniva con il server precedente,
MultithreadReverseServer è in grado di gestire
un numero indefinito di connessioni client contemporaneamente,
ciascuna su un thread separato. La struttura di questo
server non è molto diversa da quella di un server
concorrente che svolge compiti più complessi:
si invita il lettore a modificare il presente codice
al fine di creare programmi client-server più
potenti e più utili. Chi volesse approfondire
l'argomento, può trovare utili spunti in [1].
Conclusioni
Questo mese è stato introdotto il paradigma client-server,
l'architettura su cui si fonda qualunque programma di
rete. Quindi sono state introdotte le classi più
importanti per la programmazione TCP: InetAddress, ServerSocket
e Socket. Infine è stato dimostrato come sia
semplice, in Java, realizzare programmi client-server
basati su socket, sia a thread singolo che a thread
multiplo.
Il
mese prossimo verrà illustrata l'uso del protocollo
UDP e dei datagrammi.
Bibliografia
[1] Elliotte Rusty Harold Java Network Programming O'Reilly
|