UDP
e Datagrammi
Alcune applicazioni non necessitano di un protocollo
affidabile come il TCP: ordine e garanzia di consegna
non sono requisiti indispensabili in ogni circostanza.
A volte l'unico requisito che conta veramente è
la velocità: il tipico esempio solo le applicazioni
di streaming video o audio, in cui un fotogramma perso
è accettabile, mentre un fotogramma giunto in
ritardo non solo è inutile, ma addirittura dannoso
per le prestazioni dell'applicazione. In situazioni
come questa è possibile ricorrere al protocollo
UDP, che fornisce un meccanismo di comunicazione basato
su datagrammi, pacchetti di dati inviati in modo indipendente
l'uno dall'altro, per i quali non sono garantiti né
l'arrivo né l'ordine di arrivo: l'unica garanzia
è la correttezza del contenuto, nel caso il pacchetto
arrivi a destinazione. Il package java.net contiene
le classi DatagramPacket e DatagramSocket che implementano
un meccanismo di comunicazione basato su UDP indipendente
dalla piattaforma. La programmazione UDP non è
necessariamente basata sul paradigma client-server,
quanto piuttosto su una dicotomia trasmettitore-ricevitore.
Nei prossimi paragrafi verranno illustrate alcune linee
guida per riprodurre il paradigma client-server anche
con trasmissioni UDP.
La
classe Datagram
La classe Datagram è l'implementazione Java di
un pacchetto UDP. Un pacchetto UDP è caratterizzato
da un indirizzo IP, un numero di porta e un vettore
di byte. La classe dispone di molti costruttori: di
questi ne verranno illustrati una coppia. Sul lato ricevitore
è sufficiente specificare un vettore di byte
e la sua dimensione:
DatagramPacket(byte[]
buf, int length)
Sul
lato trasmettitore, oltre al buffer è necessario
specificare un indirizzo internet e una porta:
DatagramPacket(byte[]
buf, int length, InetAddress address, int port)
In
questo senso il datagramma ricorda una lettera, con
l'indirizzo stampato sopra la busta e il contenuto al
suo interno.
Nota:
Le porte UDP sono completamente indipendenti da quelle
TCP. E' dunque possibile lanciare sulla stessa macchina
un'applicazione TCP e una UDP che sfruttano lo stesso
numero di porta.
Tra
i metodi della classe vanno anzitutto segnalati i metodi
get() che permettono di accedere ai vari elementi del
datagramma. Sul lato ricevitore sono essenziali i metodi
che forniscono l'inirizzo Internet e la porta del trasmettitore,
dato che questi elementi permettono eventualmente di
creare un pacchetto di risposta:
InetAddress
getAddress()
int getPort()
Dal
lato ricevitore risultano utili i seguenti metodi, che
permettono di ottenere tutte le informazioni utili a
leggere il contenuto del datagramma, vale a dire il
vettore di byte, la posizione da cui iniziare la lettura
(offset) e la lunghezza del messaggio (lenght):
byte[]
getData()
int getOffset()
int getLength()
Per
ognuno di questi metodi get() esiste un metodo set()
corrispondente, utili se si desidera riutilizzare lo
stesso datagramma per inviare più messaggi: negli
esempi che seguiranno, per semplicità, verrà
creato un nuovo datagramma per ogni messaggio.
La
classe DatagramSocket
La classe DatagramSocket implementa le socket UDP: a
differenza di quanto avveniva con il protocollo TCP,
in questo caso esiste un solo tipo di socket sia per
il trasmettitore che per il ricevitore. Per costruire
una socket trasmittente non è necessario fornire
alcun argomento: la socket viene creata sulla prima
porta disponibile, e il ricevitore che desideri inviare
una risposta può trovare l'indirizzo completo
sul messaggio stesso:
DatagramSocket()
throws SocketException
Per
costruire una socket ricevente è necessario invece
specificare un numero di porta:
DatagramSocket(int
port) throws SocketException
Qualora
la macchina disponesse di più di una scheda di
rete, è possibile specificare quale utilizzare
attraverso un opportuno oggetto InetAddress:
DatagramSocket(int
port, InetAddress laddr) throws SocketException
Tutti
i costruttori possono generare una SocketException se
qualcosa va storto durante la fase di creazione. I metodi
più importanti sono quelli che permettono di
inviare o ricevere un datagramma, oltre al caratteristico
metodo close() che chiude la socket e rilascia le risorse
di sistema occupate:
void
send(DatagramPacket p) throws IOException
void receive(DatagramPacket p) throws IOException
void close()
Il
metodo receive() è bloccante: in pratica il ricevitore
resta in attesa per un tempo indefinito fino a quando
non viene ricevuto un pacchetto, a meno che non venga
attivato l'apposto meccanismo di timeout, come verrà
illustrato nel prossimo paragrafo. I metodi send() e
re-ceive() sono soggetti ad IOException, qualora la
comunicazione non andasse a buon fine.
Tra
gli altri metodi della classe vale la pena di segnalarne
solo uno:
void
setSoTimeout(int timeout) throws SocketException
Questo
metodo abilita o disabilita un meccanismo di timeout
valido sul lato ricevente, attraverso la specifica di
un numero intero di millisecondi. Un tempo pari a 0
disabilita il meccanismo di timeout, con un numero superiore
si imposta un tempo massimo di attesa in ricezione,
scaduto il quale viene generata una SocketTimeoutException,
ma la socket può essere ancora utilizzata. Come
verrà illustrato più avanti, questo metodo
è necessario se si desidera creare un semplice
protocollo a due vie tra trasmettitore e ricevitore.
Programmazione
UDP di base
Come già specificato più volte, il protocollo
UDP non crea alcuna connessione tra trasmettitore e
ricevitore. Il primo esempio che verrà illustrato
è proprio una coppia di programmi, un ricevitore
e un trasmettitore, ciascuno dei quali è specializzato
in un'unica operazione. Di osservi prima il programma
ricevitore:
import
java.net.*;
import java.util.*;
public
class DatagramReceiver {
public static void main(String argv[]) throws
Exception {
DatagramSocket receiver = new
DatagramSocket(5000);
while ( true ) {
byte[] buffer =
new byte[255];
DatagramPacket packet
= new DatagramPacket(buffer, buffer.length);
System.out.println("In
attesa di messaggi...");
receiver.receive(packet);
String message =
new String(packet.getData(), packet.getOffset(), packet.getLength());
System.out.println("Messaggio
ricevuto dall'host " + packet.getAddress() +
"
su porta " + packet.getPort() + ": "
+ message);
}
}
}
Il
programma, per semplicità, delega la gestione
delle eccezioni alla JVM grazie alla direttiva throws
Exception dopo il metodo main(). Nella prima riga viene
creata una DatagramSocket in ascolto sulla porta 5000.
Quindi viene dato il via ad un ciclo infinito all'interno
del quale viene creato un array di byte e un DatagramPacket
basato su tale array. Quindi, dopo aver stampato un
messaggio di attesa su console, il programma si mette
in attesa di un pacchetto. Quando questo arriva, ne
trasferisce il contenuto nella stringa message; infine
stampa a schermo un insieme di informazioni: l'host
trasmittente, al sua porta e il messaggio. Si noti che
questo programma, non essendo vincolato ad una connessione,
può ricevere da qualunque trasmettitore presente
su internet. Si osservi ora il programma trasmettitore:
import
java.net.*;
import java.io.*;
public
class DatagramSender {
public static void main(String argv[]) throws
Exception {
if ( argv.length != 1 )
System.out.println("Usage:
java ReverseClient <hostname>");
else {
DatagramSocket sender
= new DatagramSocket();
BufferedReader lineReader
= new BufferedReader(new InputStreamReader(System.in));
InetAddress IP =
InetAddress.getByName(argv[0]);
while ( true ) {
System.out.println("Scrivi
qualcosa, poi premi invio, oppure scrivi !q per uscire:");
String
line = lineReader.readLine();
if (
line.equals("!q") )
break;
else
{
byte[]
buffer = line.getBytes();
DatagramPacket
packet = new DatagramPacket(buffer, buffer.length, IP,
5000);
sender.send(packet);
System.out.println("---Messaggio
Spedito---");
}
}
sender.close();
}
}
}
Anche
in questo caso la gestione delle eccezione viene delegata
alla JVM. L'utente deve specificare, al momento del
lancio, l'indirizzo intenet del ricevitore: se questo
gira sulla stessa macchina del trasmettitore, si ricorrerà
all'usuale dispositivo di loopback:
java
DatagramSender localhost
Il
programma verifica che il numero di parametri inseriti
a riga di comando sia giusto: in caso affermativo, esso
crea dapprima una DatagramSocket, quindi un BufferedReader
connesso allo standard input e infine risolve l'indirizzo
internet passato come parametro. Inizia quindi un ciclo
while infinito in cui viene letto un messaggio scritto
dall'utente su console: se questo corrisponde alla sequenza
di uscita "!q", il programma esce, altrimenti
crea un vettore di byte a partire dalla stringa, crea
un DatagramPacket a partire dal buffer, dall'IP e dal
numero di porta 5000 e invia il messaggio; quindi riprende
il ciclo while da capo. Si noti che una volta spedito
il pacchetto, il programma non è in grado di
verificare se quest'ultimo sia stato ricevuto o meno:
in questa particolare circostanza la cosa non ha semplicemente
importanza.
Trasmissione
a due vie basata su datagrammi
Nonostante si tratti di un protocollo connectionless,
è possibile creare meccanismi di comunicazione
a due vie, tipo client-server. L'unica cosa a cui bisogna
fare attenzione è che la risposta potrebbe non
arrivare: per questa ragione sul lato client si ricorre
ad un meccanismo di timeout che interrompe la receive()
dopo cinque secondi di attesa. Si osservi il codice
del server:
import
java.net.*;
import java.util.*;
public
class DatagramServer {
public static void main(String argv[]) throws
Exception {
DatagramSocket s = new DatagramSocket(5000);
while(true) {
DatagramPacket packet
= new DatagramPacket(new byte[0],0);
s.receive(packet);
String message =
new Date().toString();
byte[] buffer =
message.getBytes();
InetAddress address
= packet.getAddress();
int port = packet.getPort();
s.send(new DatagramPacket(buffer,buffer.length,address,port));
}
}
}
Questo
programma implementa un time server, un programma che
ad una richiesta del client risponde con una stringa
che rappresenta la data e l'ora corrente:
Sat
Jan 08 17:50:46 CET 2005
Il
server crea una socket sulla porta 5000, quindi da il
via ad un ciclo while infinito in cui crea un pacchetto
vuoto, il cui unico scopo è raccogliere la richiesta
del client. Quindi si mette in attesa sulla receive()
fino a quando non arriva una richiesta. Giunta la richiesta,
il server ricava una stringa da un oggetto Date creato
per l'occasione, converte la stringa in un vettore di
byte, quindi preleva dal pacchetto l'indirizzo e la
porta del mittente. Infine invia un datagramma con i
dati appena elencati. Il programma client deve ricorrere
al timeout per impostare un protocollo di comunicazione
a due vie, dato che non esiste nessuna garanzia che
la richiesta raggiunga il server o che la risposta raggiunga
il client:
import
java.net.*;
import java.util.*;
public
class DatagramClient {
public static void main(String argv[]) throws
Exception {
if ( argv.length != 1 )
System.out.println("Usage:
java DatagramClient <hostname>");
else {
DatagramSocket s
= new DatagramSocket();
s.setSoTimeout(5000);
DatagramPacket packet
= new DatagramPacket(new byte[256], 255,
InetAddress.getByName(argv[0]), 5000);
s.send(packet);
s.receive(packet);
String message =
new String(packet.getData(), packet.getOffset(), packet.getLength());
System.out.println(message);
}
}
}
Anche
in questo caso l'utente deve specificare il nome dell'host
verso il quale effettuare la connessione. Il client
crea una DatagramSocket ed imposta un tempo di timeout
di 5 secondi, quindi invia un pacchetto di richiesta
al server e resta in attesa di una risposta. Se la risposta
arriva, questa viene stampata a schermo; nel caso invece
scada il timeout viene generata una SocketTimeoutException
che interrompe il programma:
Exception
in thread "main" java.net.SocketTimeoutException:
Receive timed out
at java.net.PlainDatagramSocketImpl.receive0(Native
Method)
at java.net.PlainDatagramSocketImpl.receive(Unknown
Source)
at java.net.DatagramSocket.receive(Unknown Source)
at DatagramClient.main(DatagramClient.java:14)
E'
ovviamente possibile creare protocolli più complessi,
che in caso di timeout provano a rispedire la richiesta
per un certo numero di volte; tuttavia, nel caso si
desideri un protocollo più affidabile, è
più opportuno utilizzare TCP, che esegue tutte
queste operazioni in modo trasparente rispetto all'utente.
Conclusioni
Questo mese è stato analizzato il protocollo
UDP che, a differenza del TCP studiato il mese scorso,
è un protocollo inaffidabile che non garantisce
né la consegna né l'ordine di arrivo dei
pacchetti ma che offre, di contro, un livello molto
elevato di prestazioni. Per prima cosa sono state introdotte
le classi DatagramPacket e DatagramSocket che implementano
la comunicazione UDP in Java. Quindi, per chiarire i
concetti esposti, sono stati sviluppati due esempi,
il primo, basato su una semplice coppia trasmettitore-ricevitore,
ha fornito l'esempio di un programma di trasmissione
ad una via, il secondo invece ha suggerito le linee
guida per creare protocolli di trasmissione a due vie
basati su UDP. Termina qui la panoramica sulla programmazione
di rete in Java: il mese prossimo avrà inizio
una serie sulla reflection, una funzionalità
di Java che permette di ispezionare a runtime la composizione
di una classe.
Risorse
Scarica
il file con gli
esempi
|