MokaByte 100 - 8bre 2005
 
MokaByte 100 - 8bre 2005 Prima pagina Cerca Home Page

 

 

 

Il Networking in Java
III parte: socket UDP

Dopo aver studiato due forme di comunicazione basati sulla connessione, attraverso le classi URL, URLConnection e le socket TCP, è giunto il momento di illustrare l'UDP, un protocollo inaffidabile, privo di connessione che non garantisce né l'arrivo né l'ordine dei pacchetti, ma che d'altra parte fornisce una prestazione di gran lunga superiore a TCP. Come di consueto verranno illustrate le classi che implementano questo protocollo e alcuni esempi utili a chiarire il funzionamento del protocollo stesso

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