di Piergiuseppe Spinelli |
|
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:
IP_MULTICAST_IF
:
Valida per i MulticastSocket, imposta l'InetAddress a cui
spedire pacchetti multicast.SO_BINDADDR:
Valida per SocketImpl, DatagramSocketImpl. Ritorna
l'indirizzo a cui il socket è connesso.SO_LINGER:
Valida per SocketImpl. Specify l'eventuale timeout in
chiusura e la modalità (gentile o forzata) di chiusura.SO_RCVBUF:
Valida per SocketImpl, DatagramSocketImpl. Imposta
l'ampiezza del buffer interno di ricezione.SO_REUSEADDR:
Valida per DatagramSocketImpl. Imposta l'indirizzo di
riutilizzo for un MulticastSocket.SO_SNDBUF:
Valida per SocketImpl, DatagramSocketImpl. Imposta
l'ampiezza del buffer interno di spedizione.SO_TIMEOUT:
Valida per SocketImpl, DatagramSocketImpl. Imposta il
timeout utilizzato dai seguenti metodi:
ServerSocket.accept(); SocketInputStream.read();
DatagramSocket.receive(); Il cambiamento di questa
opzione non influenza l'esecuzione già in corso di uno
di tali metodi.TCP_NODELAY:
Valida per SocketImpl. Disabilita l'algoritmo di Nagle.
tale algoritmo attende, di norma, la conferma di
ricezione di un pacchetto prima di spedirne un
successivo.
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:
openConnection
setAllowUserInteraction o
setDefaultAllowUserInteraction
setDoInput
o setDoOutput
setIfModifiedSince
setUseCaches
o setDefaultUseCaches
setRequestProperty
getContentEncoding
getContentLength
getContentType
getDate
getExpiration
getLastModifed
getContent
getHeaderField
getInputStream
getOutputStream
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:
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.