Introduzione
La piattaforma java è molto apprezzato anche a causa della sua idoneità per la scrittura di programmi che utilizzano e interagiscono con le risorse su Internet. In particolare ciò che rende java un linguaggio adatto per il networking è il package java.net
che sarà analizzato in questo articolo. In esso sono contenute classi ed interfacce che implementano le funzionalità di comunicazione di basso livello. In particolare forniscono supporto a due protocolli base:
- TCP (Transmission Control Protocol): un protocollo di comunicazione affidabile tra applicazioni, tipicamente utilizzato dal protocollo IP (Internet Protocol) per realizzare lo stack TCP/IP.
- UDP (User Datagram Protocol): un protocollo di comunicazione connection-less quindi non affidabile per la trasmissione di pacchetti di dati tra applicazione.
L’astrazione utilizzata per la rappresentazione del canale di comunicazione è quello delle socket. Considerando il classico paradigma client-server, un’entità client crea una socket tentando di connettersi verso un’altra entità server, la quale, rilevato il tentativo di connessione, apre una socket dal suo lato del canale e, stabilita in questo modo la connessione, le due entità possono comunicare. Generalmente tali entità sono dei programmi che possono risiedere su computer differenti ma collegati in rete ed univocamente identificati tramite un indirizzo IP.
Un client, quindi, per comunicare con un server tramite il protocollo TCP/IP, dovrà per prima cosa creare una socket verso tale server, specificando:
- l’indirizzo IP della macchina su cui il server è in esecuzione;
- il numero di porta sulla quale il server è in ascolto.
Il concetto di porta consente di esporre più servizi contemporaneamente sullo stesso computer, permettendo sostanzialmente di avere in esecuzione programmi server diversi, in ascolto su porte diverse. Il numero di porta è un intero compreso fra 1 e 65535. Il protocollo TCP/IP riserva le porte minori di 1024 ad un set di servizi standard come, ad esempio, la porta 21 per l’FTP, la 23 per il Telnet, la 25 per la posta elettronica, la 80 per l’HTTP.
Risoluzione dei Nomi
Un indirizzo IP è costituito da 32 bit (nella specifica IPv4) o 128 bit (nella specifica IPv6). Per evidenti ragioni di semplicità agli indirizzi IP è spesso associato un nome, la cui risoluzione (conversione in IP) è realizzata da un servizio DNS (Domani Name Service). L’accesso a tale servizio in java si realizza utilizzando la classe InetAddress
, che non ha costruttori, ma che fornisce metodi statici per la creazione di un oggetto di tale tipo.
Nell’esempio seguente sono recuperate le informazioni relative al sito www.javaboss.it:
1 2 3 4 5 6 7 8 9 10 11 12 |
public static void main(String[] args ) { try { InetAddress address = InetAddress.getByName( "www.javaboss.it" ); System.out.println( "CanonicalHostName: " + address.getCanonicalHostName() ); System.out.println( "HostAddress: " + address.getHostAddress() ); System.out.println( "HostName: " + address.getHostName() ); } catch (UnknownHostException e) { e.printStackTrace(); } } |
Manipolazione di URL
Java fornisce una classe specifica per la manipolazione delle URL (Uniform Resource Locator). Esistono diversi costruttori di tale classe perché molti sono i modi in cui una URL può essere specificata.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public static void main(String[] args ) throws IOException { try { URL url1 = new URL("http://www.javaboss.it"); System.out.println( "Protocollo: " + url1.getProtocol() ); System.out.println( "Host: " + url1.getHost() ); System.out.println( "Porta: " + url1.getPort() ); //-1 indica porta non specificata URL url2 = new URL( url1, "networking" ); System.out.println( "URL: " + url2 ); URL url3 = new URL( "http", "www.javaboss.it", 80, "/networking" ); System.out.println( "URL: " + url3 ); } catch (MalformedURLException e) { e.printStackTrace(); } } |
1 2 3 4 5 |
Protocollo: http Host: www.javaboss.it Porta: -1 URL: http://www.javaboss.it/networking URL: http://www.javaboss.it:80/networking |
Lettura e Scrittura da una URL
La classe URL offre inoltre metodi specifici per aprire la connessione verso l’url indicato e leggere lo stream di quanto restituito dal server. In particolare getContent()
converte direttamente lo stream in un oggetto il cui tipo dipende dal contenuto dell’url, ad esempio sarà Image
se è una immagine o String
se è un file di testo. Vediamo invece un esempio di utilizzo del metodo openStream()
, che restituisce un InputStream
dal quale è possibile leggere i contenuti del file richiesto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class URLReader { public static void main(String args[]) throws IOException { URL url = new URL( "http://www.javaboss.it" ); // Lettura dei dati dell’URL InputStream is = url.openStream(); DataInputStream ds = new DataInputStream( is ); String line ; while( ( line = ds.readLine() ) != null ) { System.out.println(line); } ds.close(); } } |
Se si intende però utilizzare la connessione verso l’url anche per l’invio di informazioni al server è necessario utilizzare un oggetto URLConnection,
che può essere ottenuto o invocando il metodo openConnection()
sull’oggetto URL
, o utilizzando il costruttore di una delle classi derivate da URLConnection. Tale classe infatti è astratta ed il suo costruttore è definito protected
.
La connessione al server avviene invocando il metodo connect()
e non alla istanziazione della classe, mentre i metodo getInputStream()
e getOutputStream()
restituiscono rispettivamente uno stream per leggere dell’url ed uno per scrivere.
Vediamo un esempio che utilizza il servizio ospitato su http://httpbin.org/ per il test dell’invio di parametri con una request di tipo POST. Nell’esempio i parametri inviati sono param1
e param2
con valori value1
e value2
. Il servizio restituisce un JSON con gli stessi parametri ricevuti.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public static void main(String args[]) throws IOException { URL url = new URL( "http://httpbin.org/post" ); HttpURLConnection con = (HttpURLConnection) url.openConnection(); // Reuqest Header con.setRequestMethod("POST"); con.setRequestProperty("User-Agent", "Mozilla/5.0"); con.setRequestProperty("Accept-Language", "en-US,en;q=0.5"); // Parametri inviati in POST String urlParameters = "param1=value1&param2=value2"; // Invio in POST della request con.setDoOutput(true); DataOutputStream wr = new DataOutputStream(con.getOutputStream()); wr.writeBytes(urlParameters); wr.flush(); wr.close(); // Recupero dell'HTTP Response Code int responseCode = con.getResponseCode(); System.out.println("Sending 'POST' request to URL : " + url); System.out.println("Post parameters : " + urlParameters); System.out.println("Response Code : " + responseCode); // Recupero dello stream di risposta BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); StringBuffer response = new StringBuffer(); String inputLine; while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); //print result System.out.println(response.toString()); } |
1 2 3 4 5 6 7 8 |
{ ... "form":{ "param1":"value1", "param2":"value2" }, ... } |
Le Socket
Vediamo infine come un client può aprire una connessione verso un server utilizzo in modo esplicito un oggetto di tipo Socket
. Per farlo è sufficiente utilizzare uno dei suoi costruttori che accettano come parametro l’host ed il numero di porta, ed invocare i metodo getInputStream()
e getOutputStream(),
tramite i quali è possibile ottenere gli stream con cui comunicare attraverso la connessione TCP instaurata alla creazione della socket. In realtà gli oggetti restituiti sono rispettivamente di tipo SocketInputStream e SocketOutputStream
, classi che in non sono pubbliche. Quando si comunica attraverso connessioni TCP, infatti, i dati vengono suddivisi in pacchetti, quindi è consigliabile non utilizzare tali stream direttamente, ma è meglio costruire stream bufferizzati, evitando così di avere pacchetti contenenti poche informazioni.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public static void main(String[] args) throws UnknownHostException, IOException { URL url = new URL( "http://httpbin.org/html"); Socket socket = new Socket( url.getHost(), ( url.getPort() == -1 ? 80 : url.getPort() ) ); System.out.println( "Porta locale: " + socket.getLocalPort() ); // Creazione degli stream di input ed output DataOutputStream out = new DataOutputStream( new BufferedOutputStream(socket.getOutputStream()) ); DataInputStream in = new DataInputStream( socket.getInputStream() ); try { // Richiesta HTTP per recuperare GET un file remoto out.writeBytes("GET " + url.getFile() + " HTTP/1.0\r\n\r\n"); out.flush(); // Lettura stream di ritorno String input ; while((input = in.readLine()) != null) System.out.println(input); } finally { socket.close(); } } |
Dall’esempio si nota anche che l’oggetto Socket
ha un metodo getLocalPort()
che restituisce il numero della porta locale. Quando ci si connette ad un server infatti, anche sulla macchina locale, sulla quale viene creata la socket, si userà per tale socket una determinata porta, assegnata dal sistema operativo, scegliendo il primo numero di porta non occupato. Questo perché ogni connessione TCP consiste sempre di un indirizzo locale e di uno remoto, e di un numero di porta locale e un numero di porta remoto.
Infine la richiesta inviata al server tramite lo stream di output è una istruzione GET che fa parte del protocollo HTTP. In questo caso quindi, il server remoto è un web server in grado di interpretare la richiesta ed eseguirla. Nel caso specifico il client chiede al server di restituire il file associato all’url http://httpbin.org/html, comportandosi di fatto come un comune browser web.
Fino ad ora abbiamo visto metodi per aprire una connessione verso un server in esecuzione su un host remoto. Vediamo ora come, utilizzando la classe SocketServer
, un programma server possa mettersi in ascolto in attesa di una richiesta di connessione da parte di un programma client. Il programma server dovrà eseguire le seguenti attività:
- Creare un oggetto di tipo ServerSocket specificando un numero di porta locale;
- Attendere, tramite il metodo
accept()
una richiesta di connessione da un client; - Utilizzare la socket ottenuta ad ogni connessione, per comunicare con il client.
Nel fare questo però, il server deve essere in grado di soddisfare connessioni multiple provenienti da client differenti. A tale scopo utilizzeremo il supporto al multithreading di java. Il flusso descritto sopra dovrà quindi essere adattato in modo che il un thread principale resti in attesa di connessione, bloccato sul metodo accept()
e, non appena arrivi una richiesta da un client, creai un nuovo thread per gestire la richiesta. Il thread principale torna quindi ad attendere nuove connessioni.
Vediamo un esempio di server implementando un ParrotServer
, ovvero un server che ripete a pappagallo quanto ricevuto dal client. Innanzitutto avremo bisogno de thread principale che attende le connessioni:
1 2 3 4 5 6 7 8 9 10 11 12 |
public static void main(String args[]) throws IOException { ServerSocket server = new ServerSocket( 60011 ); System.out.println("ParrotServer partito sulla porta " + server.getLocalPort()); Socket client; while (true) { System.out.println("In attesa di connessioni..."); client = server.accept(); System.out.println("Richiesta di connessione da " + client.getInetAddress()); (new ParrotServer(client)).start(); } } |
Mentre il thread di gestione della connessione è il seguente. Si noti che, dopo l’invio del messaggio di benvenuto, il parrot server ripete quanto trasmessogli dal client fino alla ricezione del carattere CR (Carriage return), a seguito del quale si interrompe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class ParrotServer extends Thread { protected Socket client; public ParrotServer(Socket socket) { System.out.println("Nuova connessione da " + socket.getInetAddress()); client = socket; } public void run() { try { InputStream is = client.getInputStream(); OutputStream os = client.getOutputStream(); PrintStream p = new PrintStream( os ); p.println("Benvenuto,"); p.println("io sono un pappagallo e ripeto quello che dici."); // Ciclo ripetizione int x; while ( (x = is.read()) > -1 ) { // Ripeto il carattere os.write(x); // Se CR esco if(x == 13) { break; } } } catch (IOException e) { e.printStackTrace(); } finally { System.out.println("Connessione chiusa con " + client.getInetAddress()); try { client.close(); } catch (IOException e) { e.printStackTrace(); } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class ParrtoClient { public static void main(String[] args) throws UnknownHostException, IOException { Socket socket = new Socket("localhost", 60011); // Creazione degli stream di input ed output DataOutputStream out = new DataOutputStream(new BufferedOutputStream( socket.getOutputStream()) ); DataInputStream in = new DataInputStream( socket.getInputStream() ); try { out.writeBytes("Ciao Parrot Server ... come va?" + (char)13 ); out.flush(); // Lettura stream di ritorno String input; while ( (input = in.readLine()) != null ) { System.out.println(input); } } finally { socket.close(); } } } |
User Datagram Protocol (UDP)
Finora abbiamo sempre parlato del protocollo TCP (Transfer Control Protocol), che nello stack di comunicazione è collocato immediatamente sopra l’IP (Internet Protocol). Un altro protocollo basato sempre sull’IP, è l’UDP (User Datagram Protocol) di qui daremo solamente qualche cenno.
Innanzitutto si tratta di un protocollo non basato sulla connessione (connectionless) e che non garantisce né l’arrivo né l’ordine dei pacchetti. La sola cosa garantita è la loro integrità. Nel protocollo TCP, si deve prima di tutto stabilire una connessione, dopo di che tale connessione può essere utilizzata sia per spedire che per ricevere informazioni. Quando la comunicazione è terminata, la connessione deve essere chiusa. Nell’UDP, invece, ogni messaggio sarà spedito come un pacchetto indipendente, che seguirà un percorso indipendente. Oltre a non garantire l’arrivo di tali pacchetti il protocollo non garantisce nemmeno che i pacchetti arrivino nell’ordine in cui sono stati spediti, e che non ci siano duplicati.
Il protocollo UDP si utilizza sostanzialmente quando si necessita di comunicazioni con poco overhead ed in cui non è importante se i pacchetti arrivino in ordine o meno, anzi molto spesso l’arrivo di un pacchetto “vecchio” comporta la sua eliminazione. Un’applicazione di esempio è lo streaming di video.
Il supporto a tale protocollo è offerto da java attraverso le classi DatagramPacket
e DatagramSocket
.