di Piergiuseppe Spinelli 
 
JDK: java.net
 
 
 

Di una cosa possiamo essere sicuri: qualsiasi piroletta pretenderemo di fare in Java, non sarà mai senza rete.

Il package di comunicazione della piattaforma Java, java.net, non è solo una potente collezione di strumenti di comunicazione. Esso è parte integrante del nucleo del linguaggio dal momento che le politiche di security ed il caricamento dinamico delle classi sono ad esso strettamente collegati.

Package java.net

Tutte le classi utili per implementare applicazioni di networking sono comprese in questo package oppure, come nel caso di rmi, sono baste su di esso.

A partire dal JDK 1.1, e ancor più nella versione 1.2, il package è stato ampliato con l'intenzione di diventare, oltre che una libreria di metodi ci comunicazione, una base per successive evoluzioni, non necessariamente a livello applicativo; tale intento si rivela nel fatto che alcune classi hanno perso il modificatore final e sono state modificate per poter sostenere l'apliamento delle funzionalità attraverso il sub-classing.

Quella seguente è una veloce panoramica che, spero, dovrebbe dare l'idea della potenza offerta da Java in questo campo.

Socket, ServerSocket, SocketImpl, SocketImplFactory, InetAddress, SocketOptions

La comunicazione, in Java, vuol dire IP e famiglia (TCP, UDP etc...). Le classi a più basso livello (si fa per dire) implementano la comunicazione tramite Socket, ovvero un canale bidirezionale full-duplex punto-punto tra due task genericamente su due diverse macchine (ma, volendo, anche sulla stessa utilizzando l'indirizzo di loop 127.0.0.1).

Parlando di Socket TCP, il concetto di Client/Server riguarda più che altro il protocollo di connessione: il server (classe ServerSocket) resta continuamente in attesa di una richiesta di connessione su una certa porta, identificata da un numero univoco. Il client richiede la connessione aprendo un Socket su un determinato indirizzo IP (quello della macchina su cui gira il server) specificando il numero di porta stabilito. Accettando la connessione il ServerSocket genera un nuovo Socket già connesso con quello del richiedente. Vediamo qualche esempio:

Apertura di un socket lato client:

import java.io.*;
import java.net.*;
public class MioClient {
	public static void main(String[] av) throws IOException {
		Socket cs = null;
		PrintWriter pw = null;
		BufferedReader br = null;
		if(av.length!=2){
			System.err.println("USO: java MioCLient <host> <porta>");
			System.exit(1);
		}
		try {
			cs  = new Socket(av[0], Integer.parseInt(av[1]));
			pwt = new PrintWriter(cs.getOutputStream(), true);
			br = new BufferedReader(new InputStreamReader(cs.getInputStream()));
		} catch (UnknownHostException e) {
			System.err.println("L'indirizzo " + av[0] + " è sconosciuto.");
			System.exit(1);
		} catch (IOException e) {
			System.err.println("Connessione fallita con " + av[0] + ":" + av[1]);
			System.exit(1);
		}
		BufferedReader cbr = new BufferedReader(new InputStreamReader(System.in));
		String but;
		while ((buf = cbr.readLine()) != null) {
			pw.println(userInput);
			System.out.println("echo: " + br.readLine());
		}
		pwd.close();
		br.close();
		cbr.close();
		cs.close();
	}
}

Il Server, onde poter servire più client contemporaneamente (un tipico esempio può essere un Web Server), deve generare un thread per ogni richiesta accettata, passargli il Socket aperto, e rimettersi immediatamente in attesa di eventuali altre richieste.

Apertura di un socket lato server:

import java.io.*;
import java.net.*;
public class MioServer {
	private boolean flagApertura=true;
	public static void main(String[] av) throws IOException {
		ServerSocket ss=null;
		if(av.length!=1){
			System.err.println("USO: java MioServer <porta>");
			System.exit(1);
		}
		try {
			ss = new ServerSocket( Integer.parseInt(av[0]));
		} catch (IOException e) {
			System.out.println("Ascolto fallito sulla porta " + av[0]);
			System.exit(-1);
		}
		while(flagApertura){
			Socket cs = null;
			try {
				cs = ss.accept();
				new MioServerTask(cs);
			} catch (IOException e) {
				System.out.println("Accept fallito sulla porta " + av[0]);
				System.exit(-1);
			}
		}
		ss.close();
	}
	protected String mioParser(String s){
		...
	}
	private class MioServerTask extends Thread{
		Socket cs=null;
		PrintWriter pw = null;
		BufferedReader br = null;
		MioServerTask(Socket cs){
			this.cs=cs;
			PrintWriter pw = new PrintWriter(cs.getOutputStream(), true);
			BufferedReader br = new BufferedReader(new BufferedOutputStream(cs.getInputStream()));
			start();
		}
		public void run(){
			String inBuf, outBuf;
			while ((inBuf = br.readLine()) != null) {
				outBuf  = mioParser(inBuf);
				if outBuf.equals("Fine")){
					br.println("Ciao!");
					break;
				}else if outBuf.equals("Chiudi Server")){
					flagApertura=false;
					br.println("Chiusura server avviata!");
					break;
				}else{
					br.println(outBuf);
				}
			}
		}
	}
}

Accade il più delle volte che la macchina su cui gira il ServerSocket abbia il proprio indirizzo IP registrato su un server DNS con un indirizzo simbolico. Tale indirizzo può essere utilizzato dal client invece dell'IP del server.

I Socket sono classi wrapper attorno alla classe SocketImpl che svolge tutto il lavoro e viene attribuita al Socket dalla sua SocketFactory. E' possibile assegnare ad un Socket una factory diversa per utilizzare diverse implementazioni di SocketImpl per scopi specialistici come il supporto di firewall, diverse politiche di connessione, funzionalità addizionali di logging etc...

SocketImpl implementa l'interfaccia SocketOptions che consente di impostare e leggere opzioni di funzionamento per i Socket tramite i metodi getOption e setOption. Alcuni opzioni sono valide solo per alcuni tipi di Socket:

 

DatagramSocket, DatagramPacket, DatagramSocketImpl, MulticastSocket

Esistono altri tipi di socket basati, piuttosto che sullo scambio di flussi continui di dati (stream), sull'invio di pachetti UDP, ovvero di array di byte implementati dalla classe DatagramPacket. Oltre ai dati un DatagramPocket contiene l'indirizzo e la porta di destinazione.

Quando un DatagramSocket invia un pacchetto con il metodo send, esso viene inviato all'indirizzo in esso impostato dove deve esserci un altro DatagramSocket in attesa eseguendo un metodo receive.

La comunicazione tramite datagrammi è davvero a basso livello e richiede un completo controllo da parte delle applicazioni che ne fanno uso: non esiste sessione, non è garantito l'ordine di ricezione dei paccetti, se un pacchetto è più grande del buffer di ricezione esso viene semplicemente troncato, e lo stesso pacchetto potrebbe arrivare più volte.

Un server utilizzante Datagrammi (Esempio tratto dal Java Tutorial della SUN)

import java.io.*;
import java.net.*;
import java.util.*;
public class QuoteServerThread extends Thread {
	protected DatagramSocket socket = null;
	protected BufferedReader in = null;
	protected boolean moreQuotes = true;
	public QuoteServerThread() throws IOException {
		this("QuoteServerThread");
	}
	public QuoteServerThread(String name) throws IOException {
		super(name);
		socket = new DatagramSocket(4445);
		try {
			in = new BufferedReader(new FileReader("one-liners.txt"));
		} catch (FileNotFoundException e) {
			System.err.println("Could not open quote file. Serving time instead.");
		}
	}
	public void run() {
		while (moreQuotes) {
			try {
				byte[] buf = new byte[256];
				// receive request
				DatagramPacket packet = new DatagramPacket(buf, buf.length);
				socket.receive(packet);
				// figure out response
				String dString = null;
				if (in == null)
					dString = new Date().toString();
				else
					dString = getNextQuote();
				buf = dString.getBytes();
				// send the response to the client at "address" and "port"
				InetAddress address = packet.getAddress();
				int port = packet.getPort();
				packet = new DatagramPacket(buf, buf.length, address, port);
				socket.send(packet);
			} catch (IOException e) {
				e.printStackTrace();
				moreQuotes = false;
			}
		}
		socket.close();
	}
	protected String getNextQuote() {
		String returnValue = null;
		try {
			if ((returnValue = in.readLine()) == null) {
				in.close();
				moreQuotes = false;
				returnValue = "No more quotes. Goodbye.";
			}
		} catch (IOException e) {
			returnValue = "IOException occurred in server.";
		}
		return returnValue;
	}
}

Un client utilizzante Datagrammi (Esempio tratto dal Java Tutorial della SUN)

import java.io.*;
import java.net.*;
import java.util.*;
public class QuoteClient {
	public static void main(String[] args) throws IOException {
		if (args.length != 1) {
			System.out.println("Usage: java QuoteClient <hostname>");
			return;
		}
		// get a datagram socket
		DatagramSocket socket = new DatagramSocket();
		// send request
		byte[] buf = new byte[256];
		InetAddress address = InetAddress.getByName(args[0]);
		DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 4445);
		socket.send(packet);
		// get response
		packet = new DatagramPacket(buf, buf.length);
		socket.receive(packet);
		// display response
		String received = new String(packet.getData(), 0);
		System.out.println("Quote of the Moment: " + received);
		socket.close();
	}
}

Il protocollo UDP consente ad un DatagramSocket di spedire e ricevere packetti multicast, ovvero datagrammi il cui indirizzo designa un gruppo piuttosto che un singolo indirizzo. Un gruppo di indirizzi multicast è caratterizzato da un indirizzo IP di class D, compreso in un intervallo convenzionale da 224.0.0.1 a 239.255.255.255, su una porta convenzionale standard.

Questo tipo di comunicazione è supportata dalla classe MulticastSocket (il cui utilizzo non è attualmente consentito agli applet). MulticastSocket deriva da DatagramSocket, richiede una quaslsiasi porta libera al momento della creazione e consente di unirsi ad un gruppo di altri multicast host mediante il metodo joinGroup.

Per ricevere un multicast packet è necessario aver raggiunto un gruppo, mentre qualsiasi MulticastSocket può inviare messaggi ad qualsiasi gruppo. Il tempo di persistenza di un pacchetto inviato ad un gruppo viene stabilita dal mittente con setTimeToLive; all'interno di tale intervallo, qualsiasi membro connesso al gruppo, anche in momento successivo alla spedizione, riceverà il datagramma.

L'esempio che segue è tratto direttamente dalla documentazione della API del JDK1.2:

 // join a Multicast group and send the group salutations
 ...
 byte[] msg = {'H', 'e', 'l', 'l', 'o'};
 InetAddress group = InetAddress.getByName("228.5.6.7");
 MulticastSocket s = new MulticastSocket(6789);
 s.joinGroup(group);
 DatagramPacket hi = new DatagramPacket(msg, msg.length,group, 6789);
 s.send(hi);
 // get their responses!
 byte[] buf = new byte[1000];
 DatagramPacket recv = new DatagramPacket(buf, buf.length);
 s.receive(recv);
 ...
 // OK, I'm done talking - leave the group...
 s.leaveGroup(group);
 

URLStreamHandlerFactory, ContentHandlerFactory, ContentHandler, URL, HttpURLConnection, JarURLConnection, URLClassLoader, URLConnection, URLDecoder, URLEncoder, URLStreamHandler

Dal momento che Java nasce in piena era INTERNET, il modo più potente di identificare una risorsa in rete è utilizzare un URL (Universal Resource Locator), facilmente manipolabile dagli esseri umani e capace di descrivere totalmente i dati necessari alla connessione. L'ormai familiare struttura di un URL è la seguente:

	<PROTOCOLLO>://[[<USER>][:<PASSWORD>]@][<SERVER>][:<porta>][/<PATH>][#ANCHOR]

Esempi di URL sono:

	HTTP://www.mokabyte.it
	HTTP://www.sun.com/java/index.html
	FTP://pippo:xyz@mioserver/
	FILE://c:/temp/file.doc
	HTTPS://INTRANET:8181/JWB/default.htm
	MAILTO://piero@120.145.0.3/

A livello di Socket vengono utilizzate solo alcune delle informazioni contenute in una URL:

Altre classi di java.net, maggiormente specializzate, utilizzano i dati inerenti il protocollo, il path ed il login. La Classe URL rappresenta questo tipo di puntatore a risorse remote con una serie di metodi per costruire una URL o separare i suoi componenti.

Una URL può anche essere relativa al contesto che la specifica. Tipicamente un link in una pagina WEB utilizza come default le componenti della URL della pagina corrente. Se, ad esempio, carichiamo la pagina all'indirizzo

	http://www.mokabyte.it/index.html

ed in essa troviamo i link:

	http://images/moka.jpg
	/0398/index.html

essi saranno interpretati, rispettivamente, come:

	http://www.mokabyte.it/images/moka.jpg
	http://www.mokabyte.it/0398/index.html

Una URL relativa si ottiene con un particolre costruttore:

	set u = new URL("http://www.miosito.com/miopath",indirizzoRelativo);

La cosa si fa interessante quando, creata una URL, siamo in grado di aprire una URLConnection con il metodo openConnection() o direttamente uno stream di input per leggere il contenuto della URL (più precisamente l'output che ci viene restituito dal server ove la risorsa risiede) mediante openStream().

Una URLConnection rappresenta la connessione tra l'applicazione ed una risorsa identificata da un URL:

	try {
		URL conn = new URL("http://www.mokabyte.it/");
		conn.openConnection();
	} catch (MalformedURLException e) { // Fallita new URL() 
		. . .
	} catch (IOException e) { // openConnection() fallita
		. . .
	}

In base alla specifica URL (in particolare al protocollo ed alle protezioni di accesso) e possibile effettuare operazioni di lettura e/o di scrittura. Utilizzare una URLConnection non è un'operazione elementare: essa richiede diversi passi e controlli. Tipicamente:

  1. Creare un oggetto URL
  2. Creare la connessione con URL.openConnection
  3. Impostare vari parametri (non necessariamente tutti) con i seguenti metodi:
  4. Attivare la URLConnection con il metodo connect
  5. Accedere alle informazioni della risorsa remota, tramite i seguenti metodi:
  6. Effettuare il tipo di I/O richiesto, con:

Lettura di una URL

import java.net.*;
import java.io.*;
public class LettoreDiURL {
	public static void main(String[] av) throws Exception {
		URL u = new URL(av[0]);
		URLConnection uc = u.openConnection();
		BufferedReader br = new BufferedReader(new InputStreamReader(uc.getInputStream()));
		String buf;
		while ((buf = br.readLine()) != null)
			System.out.println(buf);
		br.close();
	}
}

Scrittura su una URLConnection e lettura della risposta

import java.io.*;
import java.net.*;
public class ServerURL {
	public static void main(String[] av) throws Exception {
		if (av.length != 1) {
			System.err.println("USO: java ServerURL comando");
			System.exit(1);
		}
		String cmd = URLEncoder.encode(av[0]);
		URL url = new URL("http://mioServer/cgi/mioServizio");
		URLConnection conn = url.openConnection();
		conn.setDoOutput(true);
		PrintWriter pw = new PrintWriter(conn.getOutputStream());
		pw.println("cmd=" + cmd);
		pw.close();
		BufferedReader ibr = new BufferedReader(new InputStreamReader(conn.getInputStream()));
		String buff;
		while ((buff = br.readLine()) != null)
			System.out.println(buf);
		br.close();
}

Senza scendere nei dettagli, appare evidente che il controllo offerto da URLConnection sulle connessioni INTERNET è, già di base, molto raffinato, consentendo di utilizzare caching, di ottenere ed impostare tutte le carratteristiche di protocollo, header e contenuto e, sopprattutto, di svolgere I/O con i comuni metodi correlati agli stream.

Esistono comunque alcune classi derivate da URL connection che semplificano ulteriormente l'utilizzo in alcuni contesti standard:

Dalla classe astratta URLStreamHandler derivano tutti gli handler per i vari protocolli (http, ftp, jar etc...). Avendo in mente gli strumenti reali per INERNET, come ad esempio i browser, la cosa è stata pensata in questo modo: piuttosto che creare internamente all'applicazione le istanze delle classi derivate da URLStreamHandler, queste possono venire caricate dinamicamente la prima volta che un determinato protocollo viene utilizzato in una URL. L'interfaccia URLStreamHandlerFactory è pensata proprio per supportare tale tipo di comportamento.

Alcune classi di utilità consentono di manipolare le URL nei modi più comuni. URLEncoder e URLDecoder supportano il formato MIME "x-www-form-urlencoded" che scambia gli spazi con "+" e tutti i caratteri non alfanumerici nella forma "%xx", dove xx è il codice ASCII del carattere espresso in esadecimale.

La classe ContentHeader cosente di ricostruire l'oggetto Java contenuto nella risorsa identificata da una URLConnection attiva. Per individuare il COntentHeader in grado di tradurre una risorsa in una specifica istanza di una classe Java, è possibile impostare un'unica volta in un'applicazione (permessi consentendo) una classe che implementi ContentHeaderFactory; tale interfaccia ha proprio lo scopo di mappare, per ogni tipo MIME, una particolare istanza di ContentHeader.

L'identificazione del tipo MIME di un certo file può essere effettuata implementando l'interfaccia FileNameMap.

Infine, la classe URLClassLoader consente il caricamento dinamco di una classe (del resto è ciò che fanno tutti i ClassLoader) quando il relativo file .class è identificato da una URL (sia JAR che directory). Ovviamente sono applicate tutte le restrizioni di sicurezza tipiche di Java ma, in più, le classi caricate in questo modo possono asccedere unicamente alle URL specificate nel costruttore all'atto della creazione dell'URLClassLoader.

Sicurezza

La piattaforma Java è attraversata in ogni sua parte da una costante attenzione alla sicurezza; figuriamoci poi quando si tratta di networking, ovvero il buco nero da cui provengono tutti gli incubi dei gestori di sistemi. Parlare della sicurezza applicata al solo networking potrebbe essere, oltre che limitativo, anche fuorviante. Mi limiterò quindi ad accennare alle classi nel package java.net correlate a tale problema e mi rimprometto, in un prossimo futuro, di dedicare uno o più numeri della presente rubrica alla sicurezza nel JDK 1.2, magari facendomi aiutare da un vero esperto del settore:

 

Conclusioni

Non c'è molto da aggiungere: se programmate in C, magari sotto UNIX, non potrete non apprezzare la semplicità e l'eleganza del modello ad ogetti di Java applicato alle comunicazioni IP.

Risorse

  1. JDK Documentation (JavaSoft website). Il sito ufficiale di documentazione per il JDK 1.2
  2. Changes and Release Notes for the JDK 1.2 Software. Cambiamenti e novità nell'ultima versione del JDK.
  3. Essential Java Classes
  4. Java Platform 1.2 API Specification
  5. The Java Tutorial