148 NETWORKING Storicamente la programmazione di rete è considerata complessa e soggetta ad errori. I programmatori devono conoscere numerosi dettagli sulle reti e qualche volta anche sull’hardware. È necessario conoscere i vari “livelli” delle reti, i diversi protocolli e numerose funzioni in ogni diversa libreria di rete riguardanti la connessione, il packing dei dati, l’handshaking della comunicazione etc… Tuttavia il concetto di programmazione in rete non è difficile. È necessario conoscere da quale computer proviene l’informazione e dove deve giungere. È simile alla lettura e scrittura di file, con la differenza che tali file anziché risiedere sulla stessa macchina locale, possono risiedere su una macchina remota. Java permette di programmare in rete in questo modo. È infatti possibile scambiare informazioni in rete nello stesso modo in cui si opera sui file. Identificazione di una macchina La prima informazione necessaria per scambio di informazioni in rete lo è 149 l’identificazione delle due macchine devono essere messe in comunicazione. che Java nella gestione della comunicazione si basa sulle tecniche di comunicazione usate da Internet e quindi sui protocolli TCP/IP. In tale ambiente l’identificazione di un calcolatore avviene tramite due modi: 1. Il D.N.S. che identifica un qualunque calcolatore collegato in rete con un nome di dominio (es. java.sun.com) 2. L’indirizzo IP costituito da quattro cifre (byte) separate da punti (es. 1.2.3.4). In entrambi i casi Java usa una rappresentazione interna sotto forma di un numero a 32 bit incapsulato in un oggetto InetAddress presente nella lbreria java.net. Esiste un metodo statico static InetAddress.getByName() che permette di ottenere a partire dal nome di dominio di un calcolatore il corrispondente indirizzo IP, interrogando l’ISP (Internet Service Provider), quando si è connessi ad Internet. Esempio: 150 Stampare l’indirizzo IP di un calcolatore connesso in rete TCP/IP, a partire dal suo nome di dominio. import java.net.*; import java.io.*; public class WhoAmI { static public void main(String[] args) { if (args.length != 1) { System.err.println("Usage: WhoAmI MachineName"); System.exit(1); } try { InetAddress a= InetAddress.getByName(args[0]); System.out.println(a); } catch (IOException e) { System.out.println("Error " + e); } } } COMUNICAZIONE CLIENT/SERVER I servizi Internet sono organizzati secondo una architettura client/server. I programmi client creano collegamenti con i server facendo opportune richieste e i server rispondono fornendo al cliente il servizio richiesto. 151 Esempio: Browser WEB richiede al server WEB un file HTML ed il server risponde fornendo lo specifico file. Client e Server comunicano tramite socket stabilendo un collegamento. I client creano socket client ed i server socket server. Collegamento: sessione di comunicazione in Internet che usa il TCP/IP. Socket: punto terminale di una comunicazione in Internet. Socke t Client Socke t Server COMUNICAZIONE TRA PROGRAMMI Per attivare la comunicazione tra computer è necessario gestire la comunicazione tra programmi che risiedono su computer diversi. Un indirizzo IP, infatti, non è abbastanza per identificare un unico server, poiché molti server possono esistere su un’unica macchina. Ogni macchina IP, contiene diverse Porte di comunicazione. Una porta è un indirizzo (a 16 152 bit) all’interno di un computer associato ad un particolare servizio (protocollo) offerto da una specifica applicazione. Alcune applicazioni tipiche di Internet hanno indirizzi di protocollo prefissati. PORTA 21 23 25 80 PROTOCOLLO FTP TELNET Simple Mail Transfer Protocol Hypertext transfer protocol Un socket è associato ad un indirizzo IP e ad un indirizzo di porta. JAVA.NET È una libreria Java che fornisce le varie classi che supportano la comunicazione client/server basata su socket. InetAddress Rappresenta gli indirizzi Internet sia nella forma decimale puntata che nella forma di nomi di dominio. Fornisce 8 metodi che operano sugli indirizzi: getLocalHost() (statico) restituisce un oggetto InetAddress che 153 rappresenta l’indirizzo computer locale. del getByName() (statico) restituisce un oggetto InetAddress per uno specifico computer (dato il nome di dominio). getAllByName() (statico) restituisce un array di tutti gli indirizzi Internet associati ad un particolare computer. byte[] getAddress() restituisce l’indirizzo IP numerico del computer identificato da InetAddress (array di 4 elementi) String getHostName() restituisce il nome di dominio del computer identificato da InetAddress. I metodi di cui sopra possono l’eccezione UnknownHostException. generare 154 Esercizio: Il programa stampa tutti gli indirizzi IP associati ad un nome di dominio utilizzando i metodi di cui sopra. import java.net.*; import java.io.*; public class WhoAreYou { static public void main(String[] args) { try { if (args.length !=1) { System.out.println("Usage: java WhoAreYou HostName"); return; } InetAddress host = InetAddress.getByName(args[0]); String hostName= host.getHostName(); byte ipAddress[] = host.getAddress(); System.out.println("Host name:"+ hostName); System.out.print("IP address: "); for (int i=0;i<ipAddress.length;++i) System.out.print((ipAddress[i] +256)%256+"."); System.out.println(); } catch (UnknownHostException ex) { System.out.println("Unknown host"); return; } }} 155 Socket La classe socket implementa socket client basati su collegamento (circuiti virtuali). Questi socket sono utilizzati per sviluppare applicazioni che utilizzano servizi forniti da applicazioni server orientate al collegamento. In Java vengono utilizzati dei socket per creare la connessione con un’altra macchina e scambiarsi dati utilizzando le classi InputStream ed OutputStream come se la connessione di rete così stabilita fosse un semplice flusso di I/O. La classe Socket possiede diversi costruttori e metodi: Socket(String host, int port) costruttore che crea un socket collegandolo ad uno specifico indirizzo IP (nome di host) e ad una porta. void close() metodo che chiude il socket. InputStream getInputStream() prende dal socket il flusso di input da leggere. OutputStream getOutputStream() prende un flusso di output da scrivere sul socket. 156 getInetAddress(), getPort() restituiscono l’indirizzo IP e l’indirizzo di porta del computer di destinazione al quale il socket è collegato. getLocalPort() restituisce l’indirizzo di porta locale del computer sorgente associato con il socket. 157 Esempio (Socket Client): Il programma si connette ad un server e semplicemente invia e legge linee di testo. import java.net.*; import java.io.*; public class ClientSok { static public void main(String[] args) throws IOException { //Passando null a getByName() si genera l'indirizzo locale // valido anche in assenza di collegamento in rete InetAddress addr= InetAddress.getByName(null); // Alternativamente si sarebbe potuto assegnare direttamente: //InetAddress addr= new InetAddress.getByName("127.0.0.1"); // oppure: //InetAddress addr= new InetAddress.getByName("localhost"); System.out.println("addr: "+addr); Socket socket= new Socket(addr,8080); // si usa un try..finally per essere sicuri che qualunque cosa // accada alla fine il socket vega chiuso try { System.out.println("socket = "+ socket); InputStreamReader ir = new InputStreamReader(socket.getInputStream()); 158 BufferedReader in=new BufferedReader(ir); // Il secondo parametro al costruttore di Printstream fa in // modo che il flush venga eseguito automaticamente PrintStream out = new PrintStream(socket.getOutputStream(),tru e); for (int i=0; i<10; i++) { out.println("Packet "+i); String str= in.readLine(); System.out.println(str); // Non è necessario out.flush(); per il motivo di cui sopra } out.println("END"); } finally { System.out.println("closing..."); socket.close(); } } } 159 SocketServer La classe ServerSocket implementa un socket server TCP. Fornisce due costruttori che specificano la porta sulla quale il server resta in attesa delle richieste del client (è chiaro che non serve specificare l’indirizzo IP, dato che il server agisce sulla macchina locale). Un parametro opzionale (count) può essere specificato, per fornire il tempo di attesa del collegamento in arrivo da parte del socket. I metodi utilizzati sono i seguenti: Socket accept() serve per fare in modo che il server attenda finchè il collegamento non sia stabilito, restituisce un oggetto di tipo Socket. InetAddress getInetAddress() restiruisce l’indirizzo del computer al quale il socket è collegato. int getLocalPort() restituisce il numero di porta locale alla quale il server resta in attesa del collegamento in arrivo. void close() metodo che chiude il socket. Esempio (Socket Server): 160 Il programma crea un Server che semplicemente effettua l’eco di ciò che il client gli invia. import java.net.*; import java.io.*; // Scegliere una porta al di fuori dell'intervallo 1..1024 public class ServerSok { public static final int PORT = 8080; static public void main(String[] args) throws IOException { ServerSocket s= new ServerSocket(PORT); System.out.println("Started: " + s); try { Socket socket= s.accept(); try { System.out.println("Connection accepted: "+ socket); InputStreamReader i = new InputStreamReader (socket.getInputStream()); BufferedReader in=new BufferedReader(i); PrintStream out= new PrintStream (socket.getOutputStream()); while (true) { String str= in.readLine(); if (str.equals("END")) break; 161 System.out.println("Echoing: "+ str); out.println(str); out.flush(); } } finally { //chiude comunque i due socket... System.out.println("closing..."); socket.close(); } } finally { s.close(); } } } 162 Esempio: Realizza un server in grado di servire le richieste di più client in contemporanea. import java.net.*; import java.io.*; public class MultiServerSok extends Thread { private Socket socket; private InputStreamReader in; private PrintStream out; public MultiServerSok(Socket s) throws IOException { socket = s; in = new InputStreamReader(socket.getInputStream()); // Abilita autoflush out= new PrintStream(socket.getOutputStream(), true); // se qualcuna delle ulteriori chiamate genera un'eccezione, // il chiamante è responsabile per la chiusura del socket, // altrimenti lo stesso thread lo chiuderà. start(); //Chiama run() } 163 public void run() { try{ while (true) { String str= in.readLine(); if (str.equals("END")) break; System.out.println("Echoing: "+ str); out.println(str); } System.out.println("closing..."); } catch (IOException ex) {} finally { try { socket.close(); } catch (IOException ex) {} } } } public class MultiSock { public static final int PORT = 8080; static public void main(String[] args) throws IOException { ServerSocket s= new ServerSocket(PORT); System.out.println("Server Started"); try { while (true) { Socket socket= s.accept(); try { 164 new MultiServerSok(socket); } catch (IOException ex) { // Se fallisce chiude il socket, altrimenti lo chiuderà il thread. socket.close(); } } } finally {s.close(); } } } 165 DATAGRAM Le classi Socket e ServerSoket sono state utilizzate per realizzare una comunicazione che si basa sul protocollo TCP. Questo protocollo è stato progettato per garantire l’affidabilità della comunicazione. Esso infatti si preoccupa di effettuare la ritrasmissione (automaticamente) dei dati eventualmente perduti, prevede diversi percosi per i dati nel caso in cui uno di essi non funzionasse più, garantisce che i byte spediti arrivino nello stesso ordine in cui sono stati mandati. Tutti questi controlli però hanno un costo in termini di rallentamento delle prestazioni dei calcolatori predisposti ad effettuarli. Esiste un secondo protocollo chiamato UDP (User Datagram Protocol) che non offre l’affidabilità del TCP, ma che risulta molto efficiente soprattutto quando per i dati che si vogliono spedire è richiesta più la velocità nella spedizione che la garanzia che tutti i pacchetti spediti arrivino correttamente. Java prevede per implementare la comunicazione tramite UDP, due classi: DatagramSocket e DatagramPachet. 166 DatagramSocket La classe in oggetto prevede due costruttori che servono per creare stabilire la connessione sia dal lato del server che da quello del client. In questo caso non si tratta di connessioni TCP, infatti ad un server sulla stessa porta possono arrivare più pacchetti mescolati insieme relativi a diverse comunicazioni con client diversi. Per questo motivo i dati inviati dovranno contenere le informazioni relative al mittente e destinatario della comunicazione oltre che all’oggetto della comunicazione stessa. Il costruttore predefinito per il server prevede di specificare una porta di comunicazione sulla quale il server stesso rimane in attesa di pacchetti. Il costruttore predefinito per il client non prevede di specificare alcuna porta (la porta a cui è destinato insieme all’indirizzo IP del destinatario devono essere specificati all’interno del pacchetto). DatagramPacket Il pacchetto o datagramma spedito dovrà contenere le informazioni relative al destinatario, cioè indirizzo IP e porta di comunicazione. Queste informazioni devono 167 essere esplicitamente inserite all’interno del pacchetto nel modo seguente: String dati = ”prova dati da inviare…”; byte pck[] = new byte[256]; dati.getBytes(0,dati.length(),pck,0);1 DatagramPacket dgram= new DatagramPacket(pck,256,destAddress,destP ort); L’indirizzo IP e il numero di porta del mittente verranno automaticamente inseriti all’interno del pacchetto da JAVA. Il destinatario del datagramma lo riceverà in un buffer di byte così definito: byte pck[] = new byte[bufLen]; DatagramPacket dgram= new DatagramPacket(pck,bufLen); Da questo buffer potrà estrarre le informazioni relative all’indirizzo IP e alla porta del mittente tramite i metodi getAddress() e getPort(). 1 Il metodo getBytes applicato ad una stringa prevede 4 parametri che indicano rispettivamente la posizione iniziale e finale della stringa da copiare in un array di byte, il nome dell’array di byte in cui copiare, la posizione iniziale dell’array di byte a partire dalla quale copiare la stringa. 168 Per inviare e ricevere datagrammi si utilizzeranno i metodi send() e receive() della classe DatagramSocket che prevede come unico parametro un pacchetto dati appartenente alla classe DatagramPacket. DatagramSocket DatagramSocket(); socket= new DatagramPacket dp= new DatagramPacket(buffer,buffer.legt h()); socket.send(dp); socket.receive(dp); 169 Esempio: Un programma server effettua il servizio di inviare l’ora e la data corrente a tutti i client che ne facciano richiesta. import java.net.*; import java.io.*; import java.util.*; public class TimeServer { public static final int PORT = 1711; private byte[] buf= new byte[1000]; private DatagramPacket dgram = new DatagramPacket(buf, buf.length); private DatagramSocket sok; public TimeServer(){ try { sok = new DatagramSocket(PORT); System.out.println("Server started"); while(true) { sok.receive(dgram); String s = new String(dgram.getData(),0,0,dgram.getLength()).tri m(); s+= " from address "+dgram.getAddress()+ ", port: "+ dgram.getPort(); System.out.println(s); 170 String time_s = new Date().toString(); time_s.getBytes(0,time_s.length(),buf,0); DatagramPacket echo = new DatagramPacket(buf,buf.length,dgram.getAddres s(), dgram.getPort()); sok.send(echo); } } catch (SocketException e){ System.out.println("Non riesco ad aprire il Socket"); System.exit(1); } catch (IOException e) { System.out.println("Errore di comunicazione"); System.exit(1); } } static public void main(String[] args) { new TimeServer(); } } 171 Esempio Il programma seguente realizza 5 client che effettuano richieste al server dell’esempio precedente. import java.net.*; import java.io.*; public class TimeClient { private DatagramSocket sok; private DatagramPacket dp; private byte packetBuf[]; public TimeClient() { try{ sok = new DatagramSocket(); InetAddress addr=InetAddress.getByName(null); String localHost=addr.getHostName(); for (int i=0;i<=5;i++) { packetBuf=new byte[256]; "time".getBytes(0,4,packetBuf,0); dp= new DatagramPacket(packetBuf,256,addr,TimeServer.PO RT); sok.send(dp); 172 System.out.println("Ho inviato una richiesta di 'time' a "+ localHost+ " alla porta "+ TimeServer.PORT); sok.receive(dp); InetAddress rc_addr=dp.getAddress(); int destPort= dp.getPort(); String destHost=rc_addr.getHostName().trim(); String data=new String(dp.getData(),0).trim(); System.out.println("Ho ricevuto un datagram da "+ destHost+ " alla porta "+ destPort); System.out.println("Esso conteneva i seguenti dati:“ +data); System.out.println("******"); } } catch (IOException e) { System.out.println("Errore di IO"); System.exit(1); } } static public void main(String[] args) { TimeClient timeClient = new TimeClient(); } }