8. Programmazione TCP-IP
8.1. Informazioni generali
8.1.1. Protocolli Internet
In questa sezione forniamo un'introduzione ai protocolli di comunicazione Internet, noti anche come suite TCP/IP (Transmission Control Protocol / Internet Protocol), dal nome dei due protocolli principali. È consigliabile che il lettore abbia una conoscenza generale del funzionamento delle reti, e in particolare dei protocolli TCP/IP, prima di affrontare lo sviluppo di applicazioni distribuite.
Il testo che segue è una traduzione parziale di un brano tratto da "LAN Workplace for DOS – Administrator’s Guide" di NOVELL, un documento risalente ai primi anni '90.
Il concetto generale di creazione di una rete di computer eterogenei deriva dalla ricerca condotta dalla DARPA (Defense Advanced Research Projects Agency) negli Stati Uniti. La DARPA ha sviluppato la suite di protocolli nota come TCP/IP, che consente a macchine eterogenee di comunicare tra loro. Questi protocolli sono stati testati su una rete chiamata ARPAnet, che in seguito è diventata Internet. I protocolli TCP/IP definiscono formati e regole per la trasmissione e la ricezione che sono indipendenti dall'architettura di rete e dall'hardware utilizzato.
La rete progettata dalla DARPA e gestita dai protocolli TCP/IP è una rete a commutazione di pacchetti. Una rete di questo tipo trasmette le informazioni attraverso la rete in piccoli frammenti chiamati pacchetti. Pertanto, se un computer trasmette un file di grandi dimensioni, questo verrà suddiviso in piccoli frammenti che vengono inviati attraverso la rete per essere ricomposti a destinazione. Il TCP/IP definisce il formato di questi pacchetti, ovvero:
- origine del pacchetto
- destinazione
- lunghezza
- tipo
8.1.2. Il modello OSI
I protocolli TCP/IP seguono in linea di massima il modello di rete aperto noto come OSI (Open Systems Interconnection Reference Model) definito dall'ISO (International Standards Organization). Questo modello descrive una rete ideale in cui la comunicazione tra macchine può essere rappresentata da un modello a sette livelli:

Ogni livello riceve servizi dal livello sottostante e fornisce i propri servizi al livello sovrastante. Supponiamo che due applicazioni situate su macchine diverse A e B vogliano comunicare: lo fanno a livello di Applicazione. Non hanno bisogno di conoscere tutti i dettagli di come funziona la rete: ogni applicazione passa le informazioni che desidera trasmettere al livello sottostante: il livello di Presentazione. L'applicazione deve quindi conoscere solo le regole per interfacciarsi con il livello di Presentazione.
Una volta che le informazioni si trovano nel livello di Presentazione, vengono passate secondo altre regole al livello di Sessione, e così via, fino a quando le informazioni raggiungono il mezzo fisico e vengono trasmesse fisicamente alla macchina di destinazione. Lì, subiranno il processo inverso di quello che hanno subito sulla macchina di invio.
Ad ogni livello, il processo mittente responsabile dell'invio delle informazioni le invia a un processo ricevente sull'altra macchina appartenente allo stesso livello. Lo fa secondo determinate regole note come protocollo di livello. Abbiamo quindi il seguente diagramma di comunicazione finale:

I ruoli dei diversi livelli sono i seguenti:
Garantisce la trasmissione di bit su un supporto fisico. Questo livello include apparecchiature terminali di elaborazione dati (DPTE) come terminali o computer, nonché apparecchiature di terminazione del circuito dati (DCTE) come modulatori/demodulatori, multiplexer e concentratori. I punti chiave a questo livello sono: . la scelta della codifica delle informazioni (analogica o digitale) . la scelta della modalità di trasmissione (sincrona o asincrona). | |
Nasconde le caratteristiche fisiche del livello fisico. Rileva e corregge gli errori di trasmissione. | |
Gestisce il percorso che le informazioni inviate attraverso la rete devono seguire. Questo processo è chiamato routing: consiste nel determinare il percorso che le informazioni devono seguire per raggiungere la loro destinazione. | |
Consente la comunicazione tra due applicazioni, mentre i livelli precedenti permettevano solo la comunicazione tra macchine. Un servizio fornito da questo livello può essere il multiplexing: il livello di trasporto può utilizzare una singola connessione di rete (da macchina a macchina) per trasmettere dati appartenenti a più applicazioni. | |
Questo livello fornisce servizi che consentono a un'applicazione di aprire e mantenere una sessione di lavoro su una macchina remota. | |
Ha lo scopo di standardizzare la rappresentazione dei dati tra macchine diverse. Pertanto, i dati provenienti dalla macchina A saranno "formattati" dal livello di presentazione della macchina A secondo un formato standard prima di essere inviati attraverso la rete. Una volta raggiunto il livello di presentazione della macchina di destinazione B, che li riconoscerà grazie al loro formato standard, saranno formattati in modo diverso in modo che l'applicazione sulla macchina B possa riconoscerli. | |
A questo livello si trovano applicazioni che sono generalmente vicine all'utente, come la posta elettronica o il trasferimento di file. |
8.1.3. Il modello TCP/IP
Il modello OSI è un modello ideale che non è mai stato pienamente realizzato. La suite di protocolli TCP/IP lo approssima nel modo seguente:

Livello fisico
Nelle reti locali, si utilizzano generalmente le tecnologie Ethernet o Token Ring. In questa sede ci concentreremo esclusivamente sulla tecnologia Ethernet.
Ethernet
Questo è il nome dato a una tecnologia di rete locale a commutazione di pacchetti inventata presso il Xerox PARC nei primi anni '70 e standardizzata da Xerox, Intel e Digital Equipment nel 1978. La rete è costituita fisicamente da un cavo coassiale di circa 1,27 cm di diametro e lungo fino a 500 m. Può essere estesa utilizzando ripetitori, con non più di due ripetitori che separano due macchine qualsiasi. Il cavo è passivo: tutti i componenti attivi si trovano sulle macchine collegate al cavo. Ogni macchina è collegata al cavo tramite una scheda di accesso alla rete che comprende:
- un ricetrasmettitore che rileva la presenza di segnali sul cavo e converte i segnali analogici in segnali digitali e viceversa.
- un accoppiatore che riceve i segnali digitali dal ricetrasmettitore e li trasmette al computer per l'elaborazione, o viceversa.
Le caratteristiche principali della tecnologia Ethernet sono le seguenti:
- Capacità di 10 megabit al secondo.
- Topologia a bus: tutte le macchine sono collegate allo stesso cavo

- Rete broadcast: una macchina trasmittente invia informazioni sul cavo indicando l’indirizzo della macchina ricevente. Tutte le macchine collegate ricevono quindi queste informazioni, ma solo il destinatario previsto le conserva.
- Il metodo di accesso è il seguente: il trasmettitore che desidera trasmettere ascolta il cavo — quindi rileva se è presente o meno un'onda portante, la cui presenza indicherebbe che è in corso una trasmissione. Questa è la tecnica CSMA (Carrier Sense Multiple Access). In assenza di una portante, un trasmettitore può decidere di trasmettere a sua volta. Diversi trasmettitori possono prendere questa decisione. I segnali trasmessi si sovrappongono: questo fenomeno è chiamato collisione. Il trasmettitore rileva questa situazione: mentre trasmette sul cavo, ascolta anche ciò che effettivamente lo attraversa. Se rileva che le informazioni che viaggiano sul cavo non sono quelle che ha trasmesso, conclude che si è verificata una collisione e interrompe la trasmissione. Gli altri trasmettitori che stavano trasmettendo faranno lo stesso. Ciascuno riprenderà la trasmissione dopo un ritardo casuale che dipende dal singolo trasmettitore. Questa tecnica è chiamata CD (Collision Detect). Il metodo di accesso è quindi chiamato CSMA/CD.
- Indirizzamento a 48 bit. Ogni macchina ha un indirizzo, qui indicato come indirizzo fisico, che è scritto sulla scheda che la collega al cavo. Questo indirizzo è chiamato indirizzo Ethernet della macchina.
Livello di rete
A questo livello, troviamo i protocolli IP, ICMP, ARP e RARP.
IP (Internet Protocol) | Trasmette i pacchetti tra due nodi di rete |
ICMP (Internet Control Message Protocol) | L'ICMP facilita la comunicazione tra il programma del protocollo IP su una macchina e quello su un'altra macchina. Si tratta quindi di un protocollo di scambio di messaggi all'interno del protocollo IP stesso. |
ARP (Address Resolution Protocol) | mappa l'indirizzo IP di un computer al suo indirizzo fisico |
RARP (Reverse Address Resolution Protocol) | mappa l'indirizzo fisico di una macchina al suo indirizzo Internet |
Livelli di trasporto/sessione
Questo livello include i seguenti protocolli:
TCP (Transmission Control Protocol) | Garantisce la consegna affidabile delle informazioni tra due client |
UDP (User Datagram Protocol) | Garantisce la trasmissione non affidabile di informazioni tra due client |
Livelli Applicazione/Presentazione/Sessione
Qui si trovano vari protocolli:
Un emulatore di terminale che permette alla macchina A di connettersi alla macchina B come terminale | |
Consente il trasferimento di file | |
consente il trasferimento di file | |
consente lo scambio di messaggi tra gli utenti della rete | |
converte il nome di un computer nel suo indirizzo Internet | |
Creato da Sun Microsystems, specifica uno standard di rappresentazione dei dati indipendente dalla macchina | |
definito anch'esso da Sun, è un protocollo di comunicazione tra applicazioni remote, indipendente dal livello di trasporto. Questo protocollo è importante: solleva il programmatore dalla necessità di conoscere i dettagli del livello di trasporto e rende le applicazioni portabili. Questo protocollo si basa sul protocollo XDR | |
Definito anch'esso da Sun, questo protocollo permette a una macchina di "vedere" il file system di un'altra macchina. Si basa sul protocollo RPC menzionato sopra |
8.1.4. Come funzionano i protocolli Internet
Le applicazioni sviluppate nell'ambiente TCP/IP utilizzano generalmente diversi protocolli presenti in questo ambiente. Un programma applicativo comunica con il livello più alto dei protocolli. Questo livello trasmette le informazioni al livello sottostante e così via, fino a raggiungere il supporto fisico. Qui, le informazioni vengono trasferite fisicamente alla macchina di destinazione, dove attraverseranno nuovamente gli stessi livelli, questa volta in direzione opposta, fino a raggiungere l'applicazione destinata a ricevere le informazioni inviate. Il diagramma seguente mostra il percorso delle informazioni:

Facciamo un esempio: l'applicazione FTP, definita a livello di applicazione, che consente il trasferimento di file tra macchine.
- L'applicazione invia una sequenza di byte da trasmettere al livello di trasporto.
- Il livello di trasporto divide questa sequenza di byte in segmenti TCP e aggiunge il numero di segmento all'inizio di ciascun segmento. I segmenti vengono passati al livello di rete, che è regolato dal protocollo IP.
- Il livello IP crea un pacchetto che incapsula il segmento TCP ricevuto. Nell'intestazione di questo pacchetto, inserisce gli indirizzi Internet delle macchine di origine e di destinazione. Determina inoltre l'indirizzo fisico della macchina di destinazione. L'intero pacchetto viene passato al livello Data Link & Physical, ovvero alla scheda di rete che collega la macchina alla rete fisica.
- Lì, il pacchetto IP viene a sua volta incapsulato in un frame fisico e inviato al destinatario tramite il cavo.
- Sulla macchina di destinazione, il livello Data Link & Physical fa l'opposto: decapsula il pacchetto IP dal frame fisico e lo passa al livello IP.
- Il livello IP verifica che il pacchetto sia corretto: calcola un checksum basato sui bit ricevuti, che deve trovare nell'intestazione del pacchetto. In caso contrario, il pacchetto viene rifiutato.
- Se il pacchetto viene ritenuto valido, il livello IP decapsula il segmento TCP in esso contenuto e lo passa al livello di trasporto.
- Il livello di trasporto — nel nostro esempio il livello TCP — esamina il numero del segmento per assicurarsi che i segmenti siano nell'ordine corretto.
- Calcola inoltre un checksum per il segmento TCP. Se risulta corretto, il livello TCP invia un riconoscimento alla macchina di origine; in caso contrario, il segmento TCP viene rifiutato.
- Tutto ciò che resta da fare al livello TCP è trasmettere la parte di dati del segmento all'applicazione destinata a riceverla nel livello superiore.
8.1.5. Affrontare i problemi su Internet
Un nodo di rete può essere un computer, una stampante intelligente, un file server — qualsiasi cosa, in effetti, che possa comunicare utilizzando i protocolli TCP/IP. Ogni nodo ha un indirizzo fisico con un formato che dipende dal tipo di rete. Su una rete Ethernet, l'indirizzo fisico è codificato su 6 byte. Un indirizzo di rete X.25 è un numero di 14 cifre.
L'indirizzo Internet di un nodo è un indirizzo logico: è indipendente dall'hardware e dalla rete utilizzati. Si tratta di un indirizzo di 4 byte che identifica sia una rete locale che un nodo su quella rete. L'indirizzo Internet è solitamente rappresentato da quattro numeri — i valori dei quattro byte — separati da un punto. Pertanto, l'indirizzo del computer Lagaffe presso la Facoltà di Scienze di Angers è 193.49.144.1, mentre quello del computer Liny è 193.49.144.9. Da ciò si deduce che l'indirizzo Internet della rete locale è 193.49.144.0. Su questa rete possono esserci fino a 254 nodi.
Poiché gli indirizzi Internet, o indirizzi IP, sono indipendenti dalla rete, un computer sulla rete A può comunicare con un computer sulla rete B senza preoccuparsi del tipo di rete su cui si trova: deve semplicemente conoscere il suo indirizzo IP. Il protocollo IP di ciascuna rete gestisce la conversione tra indirizzo IP e indirizzo fisico, in entrambe le direzioni.
Tutti gli indirizzi IP devono essere univoci. Le organizzazioni ufficiali sono responsabili della loro assegnazione. Infatti, queste organizzazioni assegnano un indirizzo alle reti locali, ad esempio 193.49.144.0 per la rete della Facoltà di Scienze di Angers. L'amministratore di questa rete può quindi assegnare gli indirizzi IP da 193.49.144.1 a 193.49.144.254 a sua discrezione. Questo indirizzo è generalmente memorizzato in un file specifico su ogni macchina collegata alla rete.
8.1.5.1. Classi di indirizzi IP
Un indirizzo IP è una sequenza di 4 byte, spesso scritta come I1.I2.I3.I4, che in realtà contiene due indirizzi:
- l'indirizzo di rete
- l'indirizzo di un nodo su quella rete
A seconda delle dimensioni di questi due campi, gli indirizzi IP sono suddivisi in 3 classi: classi A, B e C.
Classe A
L'indirizzo IP: I1.I2.I3.I4 ha la forma R1.N1.N2.N3 dove
R1 è l'indirizzo di rete
N1.N2.N3 è l'indirizzo di una macchina su quella rete
Più precisamente, il formato di un indirizzo IP di Classe A è il seguente:

L'indirizzo di rete è lungo 7 bit e l'indirizzo host è lungo 24 bit. Possono quindi esserci 127 reti di Classe A, ciascuna contenente fino a 2²⁴ host.
Classe B
In questo caso, l'indirizzo IP: I1.I2.I3.I4 ha la forma R1.R2.N1.N2 dove
R1.R2 è l'indirizzo di rete
N1.N2 è l'indirizzo di una macchina su quella rete
Più precisamente, il formato di un indirizzo IP di Classe B è il seguente:

L'indirizzo di rete è di 2 byte (14 bit, per l'esattezza), così come l'indirizzo del nodo. Ciò significa che possono esserci 2¹⁴ reti di Classe B, ciascuna contenente fino a 2¹⁶ nodi.
Classe C
In questa classe, l'indirizzo IP: I1.I2.I3.I4 ha la forma R1.R2.R3.N1 dove
R1.R2.R3 è l'indirizzo di rete
N1 è l'indirizzo di una macchina su quella rete
Più precisamente, il formato di un indirizzo IP di Classe C è il seguente:

L'indirizzo di rete occupa 3 byte (meno 3 bit) e l'indirizzo host occupa 1 byte. È quindi possibile avere 2²¹ reti di Classe C, ciascuna delle quali può contenere fino a 256 host.
Poiché l'indirizzo della macchina Lagaffe presso la Facoltà di Scienze di Angers è 193.49.144.1, vediamo che il byte più significativo è 193, che in binario è 11000001. Possiamo dedurre che la rete è una rete di Classe C.
Indirizzi riservati
. Alcuni indirizzi IP sono indirizzi di rete piuttosto che indirizzi di nodo all'interno della rete. Si tratta di quelli in cui l'indirizzo del nodo è impostato su 0. Pertanto, l'indirizzo 193.49.144.0 è l'indirizzo IP della rete della Facoltà di Scienze di Angers. Di conseguenza, nessun nodo su una rete può avere l'indirizzo zero.
. Quando l'indirizzo del nodo in un indirizzo IP è composto interamente da 1, si tratta di un indirizzo di broadcast: questo indirizzo si riferisce a tutti i nodi della rete.
. In una rete di Classe C, che teoricamente consente 2⁸ = 256 nodi, se eliminiamo i due indirizzi proibiti, ci rimangono solo 254 indirizzi autorizzati.
8.1.5.2. Protocolli di conversione tra indirizzi Internet e indirizzi fisici
Abbiamo visto che quando i dati vengono trasmessi da una macchina all'altra, vengono incapsulati in pacchetti mentre attraversano il livello IP. Questi pacchetti hanno la seguente forma:

Il pacchetto IP contiene quindi gli indirizzi Internet delle macchine di origine e di destinazione. Quando questo pacchetto viene trasmesso al livello responsabile dell'invio sulla rete fisica, vengono aggiunte ulteriori informazioni per formare il frame fisico che verrà infine inviato sulla rete. Ad esempio, il formato di un frame su una rete Ethernet è il seguente:

Nel frame finale sono presenti gli indirizzi fisici delle macchine di origine e di destinazione. Come vengono ottenuti?
Il computer mittente, conoscendo l’indirizzo IP del computer con cui desidera comunicare, ottiene l’indirizzo fisico di quest’ultimo utilizzando un protocollo specifico chiamato ARP (Address Resolution Protocol).
- Invia un tipo speciale di pacchetto chiamato pacchetto ARP contenente l’indirizzo IP del computer di cui si sta cercando l’indirizzo fisico. Nel pacchetto include anche il proprio indirizzo IP e il proprio indirizzo fisico.
- Questo pacchetto viene inviato a tutti i nodi della rete.
- Questi nodi riconoscono la natura speciale del pacchetto. Il nodo che riconosce il proprio indirizzo IP nel pacchetto risponde inviando il proprio indirizzo fisico al mittente del pacchetto. Come può farlo? Ha trovato gli indirizzi IP e fisici del mittente nel pacchetto.
- Il mittente riceve così l'indirizzo fisico che stava cercando. Lo memorizza in modo da poterlo utilizzare in seguito se altri pacchetti devono essere inviati allo stesso destinatario.
L'indirizzo IP di una macchina è normalmente memorizzato in uno dei suoi file di configurazione, che può consultare per recuperarlo. Questo indirizzo può essere modificato: basta modificare il file. L'indirizzo fisico, invece, è memorizzato nella memoria della scheda di rete e non può essere modificato.
Quando un amministratore desidera riorganizzare la rete, potrebbe dover modificare gli indirizzi IP di tutti i nodi e quindi modificare i vari file di configurazione per ciascun nodo. Ciò può risultare noioso e soggetto a errori se le macchine sono numerose. Un metodo consiste nel non assegnare un indirizzo IP alle macchine: invece, viene inserito un codice speciale nel file in cui la macchina troverebbe normalmente il proprio indirizzo IP. Una volta scoperto di non avere un indirizzo IP, la macchina ne richiede uno utilizzando un protocollo chiamato RARP (Reverse Address Resolution Protocol). Invia quindi un pacchetto speciale chiamato pacchetto RARP sulla rete — simile al precedente pacchetto ARP — contenente il proprio indirizzo fisico. Questo pacchetto viene inviato a tutti i nodi che riconoscono un pacchetto RARP. Uno di essi, chiamato server RARP, mantiene un file contenente le mappature indirizzo fisico <--> indirizzo IP per tutti i nodi. Risponde quindi al mittente del pacchetto RARP inviando il proprio indirizzo IP. Un amministratore che desideri riconfigurare la propria rete deve quindi solo modificare il file di mappatura sul server RARP ( ). Questo server deve normalmente avere un indirizzo IP fisso che deve essere in grado di conoscere senza dover utilizzare il protocollo RARP stesso.
8.1.6. Il livello di rete, noto come livello IP di Internet
L'IP (Internet Protocol) definisce il formato che i pacchetti devono assumere e come devono essere gestiti durante la trasmissione o la ricezione. Questo specifico tipo di pacchetto è chiamato datagramma IP. Ne abbiamo già parlato:

Il punto importante è che, oltre ai dati da trasmettere, il datagramma IP contiene gli indirizzi Internet delle macchine di origine e di destinazione. In questo modo, la macchina ricevente sa chi le sta inviando un messaggio.
A differenza di un frame di rete, la cui lunghezza è determinata dalle caratteristiche fisiche della rete su cui viaggia, la lunghezza del datagramma IP è fissata dal software e sarà quindi la stessa su reti fisiche diverse. Abbiamo visto che, scendendo dal livello di rete al livello fisico, il datagramma IP viene incapsulato all'interno di un frame fisico. Abbiamo fatto l'esempio del frame fisico di una rete Ethernet:

I frame fisici viaggiano da nodo a nodo verso la loro destinazione, che potrebbe non trovarsi sulla stessa rete fisica del computer mittente. Il pacchetto IP può quindi essere incapsulato in successione in diversi frame fisici nei nodi che collegano due reti di tipo diverso. È anche possibile che il pacchetto IP sia troppo grande per essere incapsulato in un singolo frame fisico. Il software IP sul nodo in cui si verifica questo problema suddivide quindi il pacchetto IP in frammenti secondo regole specifiche, ciascuno dei quali viene poi inviato sulla rete fisica. Essi verranno riassemblati solo alla loro destinazione finale.
8.1.6.1. Routing
Il routing è il metodo utilizzato per indirizzare i pacchetti IP verso la loro destinazione. Esistono due metodi: il routing diretto e il routing indiretto.
Routing diretto
Il routing diretto si riferisce alla trasmissione di un pacchetto IP direttamente dal mittente al destinatario all'interno della stessa rete:
- Il dispositivo che invia un datagramma IP possiede l'indirizzo IP del destinatario.
- Ottiene l'indirizzo fisico del destinatario tramite il protocollo ARP o dalle proprie tabelle, se tale indirizzo è già stato ottenuto.
- Invia il pacchetto sulla rete a quell'indirizzo fisico.
Routing indiretto
Il routing indiretto si riferisce alla trasmissione di un pacchetto IP verso una destinazione situata su una rete diversa da quella a cui appartiene il mittente. In questo caso, le parti relative all’indirizzo di rete degli indirizzi IP delle macchine di origine e di destinazione sono diverse. La macchina di origine lo riconosce. Invia quindi il pacchetto a un nodo speciale chiamato router, un nodo che collega una rete locale ad altre reti e il cui indirizzo IP trova nelle proprie tabelle: un indirizzo inizialmente ottenuto da un file, dalla memoria permanente o tramite informazioni che circolano sulla rete.
Un router è collegato a due reti e possiede un indirizzo IP all'interno di entrambe queste reti.

Nel nostro esempio sopra:
- La rete n. 1 ha l'indirizzo Internet 193.49.144.0 e la rete n. 2 ha l'indirizzo 193.49.145.0.
- All'interno della rete n. 1, il router ha l'indirizzo 193.49.144.6 e l'indirizzo 193.49.145.3 all'interno della rete n. 2.
Il ruolo del router è quello di prendere il pacchetto IP che riceve, contenuto in un frame fisico tipico della rete n. 1, e inserirlo in un frame fisico in grado di viaggiare sulla rete n. 2. Se l'indirizzo IP della destinazione del pacchetto si trova all'interno della rete n. 2, il router invierà il pacchetto direttamente a essa; altrimenti, lo invierà a un altro router, collegando la rete n. 2 alla rete n. 3, e così via.
8.1.6.2. Messaggi di errore e di controllo
Sempre all'interno del livello di rete, allo stesso livello del protocollo IP, si trova l'ICMP (Internet Control Message Protocol). Viene utilizzato per inviare messaggi relativi al funzionamento interno della rete: nodi guasti, congestione a un router, ecc. I messaggi ICMP sono incapsulati in pacchetti IP e inviati sulla rete. I livelli IP dei vari nodi intraprendono le azioni appropriate in base ai messaggi ICMP che ricevono. Pertanto, un'applicazione non vede mai questi problemi specifici della rete.
Un nodo utilizzerà le informazioni ICMP per aggiornare le proprie tabelle di routing.
8.1.7. Il livello di trasporto: i protocolli UDP e TCP
8.1.7.1. Il protocollo UDP: User Datagram Protocol
Il protocollo UDP consente uno scambio di dati inaffidabile tra due punti, il che significa che la consegna di un pacchetto a destinazione non è garantita. L'applicazione, se lo desidera, può gestire questo aspetto autonomamente, ad esempio attendendo un riconoscimento dopo l'invio di un messaggio prima di inviare quello successivo.
Finora, a livello di rete, abbiamo parlato degli indirizzi IP delle macchine. Tuttavia, su una singola macchina possono coesistere contemporaneamente diversi processi, che possono comunicare tra loro. Pertanto, quando si invia un messaggio, è necessario specificare non solo l'indirizzo IP del computer di destinazione, ma anche il "nome" del processo di destinazione. Questo nome è in realtà un numero, chiamato numero di porta. Alcuni numeri sono riservati alle applicazioni standard: la porta 69 per l'applicazione TFTP (Trivial File Transfer Protocol), ad esempio.
I pacchetti gestiti dal protocollo UDP sono chiamati anche datagrammi. Hanno la seguente forma:

Questi datagrammi sono incapsulati in pacchetti IP e poi in frame fisici.
8.1.7.2. Il protocollo TCP: Transmission Control Protocol
Per comunicazioni sicure, il protocollo UDP è insufficiente: lo sviluppatore dell'applicazione deve creare un protocollo autonomamente per garantire che i pacchetti vengano instradati correttamente.
Il TCP (Transmission Control Protocol) evita questi problemi. Le sue caratteristiche sono le seguenti:
- Il processo che desidera trasmettere stabilisce innanzitutto una connessione con il processo che riceve le informazioni che sta per trasmettere. Questa connessione viene stabilita tra una porta sulla macchina mittente e una porta sulla macchina ricevente. Viene così creato un percorso virtuale tra le due porte, che sarà riservato esclusivamente ai due processi che hanno stabilito la connessione.
- Tutti i pacchetti inviati dal processo di origine seguono questo percorso virtuale e arrivano nell'ordine in cui sono stati inviati, cosa che non era garantita nel protocollo UDP poiché i pacchetti potevano seguire percorsi diversi.
- I dati trasmessi appaiono continui. Il processo mittente invia i dati al proprio ritmo. Questi dati non vengono necessariamente inviati immediatamente: il protocollo TCP attende di averne una quantità sufficiente da inviare. Essi vengono memorizzati in una struttura denominata segmento TCP. Una volta che questo segmento è pieno, viene trasmesso al livello IP, dove viene incapsulato in un pacchetto IP.
- Ogni segmento inviato dal protocollo TCP è numerato. Il protocollo TCP ricevente verifica di ricevere i segmenti in sequenza. Per ogni segmento ricevuto correttamente, invia un riconoscimento al mittente.
- Quando il destinatario lo riceve, ne dà notifica al mittente. Il mittente può così confermare che un segmento è stato consegnato con successo, cosa che non era possibile con il protocollo UDP.
- Se, trascorso un certo periodo di tempo, il protocollo TCP che ha inviato un segmento non riceve un riconoscimento, esso ritrasmette il segmento in questione, garantendo così la qualità del servizio di trasmissione delle informazioni.
- Il circuito virtuale stabilito tra i due processi in comunicazione è full-duplex: ciò significa che le informazioni possono fluire in entrambe le direzioni. Pertanto, il processo di destinazione può inviare conferme anche mentre il processo di origine continua a inviare informazioni. Ciò consente, ad esempio, al protocollo TCP di origine di inviare più segmenti senza attendere una conferma. Se, dopo un certo periodo di tempo, si rende conto di non aver ricevuto un riconoscimento per un segmento specifico n. n, riprenderà l'invio dei segmenti da quel punto.
8.1.8. Il livello applicativo
Al di sopra dei protocolli UDP e TCP, esistono vari protocolli standard:
TELNET
Questo protocollo consente a un utente sulla macchina A in rete di connettersi alla macchina B (spesso chiamata macchina host). TELNET emula un cosiddetto terminale universale sulla macchina A. L'utente si comporta quindi come se avesse un terminale collegato alla macchina B. Telnet si basa sul protocollo TCP.
FTP: (File Transfer Protocol)
Questo protocollo consente lo scambio di file tra due macchine remote, nonché operazioni sui file come, ad esempio, la creazione di directory. Si basa sul protocollo TCP.
TFTP: (Trivial File Transfer Protocol)
Questo protocollo è una variante dell'FTP. Si basa sul protocollo UDP ed è meno sofisticato dell'FTP.
DNS: (Domain Name System)
Quando un utente desidera scambiare file con una macchina remota, ad esempio tramite FTP, deve conoscere l'indirizzo Internet di quella macchina. Ad esempio, per utilizzare FTP sulla macchina Lagaffe dell'Università di Angers, è necessario eseguire FTP come segue: FTP 193.49.144.1
Ciò richiede una directory che associ le macchine agli indirizzi IP. In questa directory, le macchine sarebbero probabilmente designate da nomi simbolici come:
Macchina DPX2/320 presso l'Università di Angers
Macchina Sun presso l'ISERPA ad Angers
È chiaro che sarebbe più comodo riferirsi a una macchina con un nome piuttosto che con il suo indirizzo IP. Ciò solleva la questione dell'unicità dei nomi: ci sono milioni di macchine interconnesse. Si potrebbe immaginare un organismo centralizzato che assegni i nomi. Ciò sarebbe senza dubbio piuttosto macchinoso. Il controllo sui nomi è stato infatti distribuito tra **i domini**. Ogni dominio è gestito da un'organizzazione generalmente molto snella che ha completa libertà nella scelta dei nomi delle macchine. Pertanto, le macchine in Francia appartengono al dominio **fr**, gestito dall'Inria di Parigi. Per semplificare ulteriormente le cose, il controllo è distribuito ancora di più: all'interno del dominio **fr** vengono creati dei sottodomini. Pertanto, l'Università di Angers appartiene al dominio **univ-Angers**. Il dipartimento che gestisce questo dominio ha completa libertà di denominare le macchine sulla rete dell'Università di Angers. Per ora, questo dominio non è stato suddiviso. Ma in una grande università con molte macchine collegate in rete, potrebbe esserlo.
La macchina DPX2/320 dell'Università di Angers è stata denominata *Lagaffe*, mentre un PC 486DX50 è stato denominato *liny*. Come si fa a fare riferimento a queste macchine dall'esterno? Specificando la gerarchia dei domini a cui appartengono. Pertanto, il nome completo della macchina Lagaffe sarà:
All'interno dei domini è possibile utilizzare nomi relativi. Pertanto, all'interno del dominio **fr** e all'esterno del dominio **univ-Angers**, è possibile fare riferimento alla macchina Lagaffe come
Infine, all'interno del dominio *univ-Angers*, è possibile fare riferimento ad essa semplicemente come
Un'applicazione può quindi fare riferimento a una macchina tramite il suo nome. In definitiva, però, è comunque necessario ottenere l'indirizzo IP della macchina. Come si fa? Supponiamo di voler comunicare dalla macchina A alla macchina B.
- Se la macchina B appartiene allo stesso dominio della macchina A, il suo indirizzo IP si troverà probabilmente in un file presente sulla macchina A.
- Altrimenti, la macchina A troverà, in un altro file o nello stesso di prima, un elenco di diversi server dei nomi con i relativi indirizzi IP. Un server dei nomi è responsabile della mappatura del nome di una macchina al suo indirizzo IP. La macchina A invierà una richiesta speciale al primo server dei nomi del suo elenco, nota come query DNS, che include il nome della macchina ricercata. Se il server interrogato ha quel nome nei propri record, invierà l'indirizzo IP corrispondente al computer A. Altrimenti, il server troverà nei propri file un elenco di server dei nomi che può interrogare. Lo farà quindi. In questo modo, verranno interrogati diversi server dei nomi, non a caso ma in modo da ridurre al minimo il numero di richieste. Se il computer viene finalmente trovato, la risposta verrà inviata al computer A.
XDR: (eXternal Data Representation)
Creato da Sun Microsystems, questo protocollo definisce uno standard di rappresentazione dei dati indipendente dalla piattaforma.
RPC: (Remote Procedure Call)
Definito anch'esso da Sun, è un protocollo di comunicazione tra applicazioni remote, indipendente dal livello di trasporto. Questo protocollo è importante: solleva il programmatore dalla necessità di conoscere i dettagli del livello di trasporto e rende le applicazioni portabili. Questo protocollo si basa sul protocollo XDR
NFS: Network File System
Definito anch'esso da Sun, questo protocollo permette a una macchina di "vedere" il file system di un'altra macchina. Si basa sul protocollo RPC descritto sopra.
8.1.9. Conclusione
In questa introduzione abbiamo presentato alcune delle caratteristiche principali dei protocolli Internet. Per approfondire questo argomento, i lettori possono consultare l'eccellente libro di Douglas Comer:
Titolo: TCP/IP: Architettura, protocolli, applicazioni.
Autore: Douglas COMER
Editore: InterEditions
8.2. Gestione degli indirizzi di rete in Java
8.2.1. Definizione
Ogni macchina su Internet è identificata da un indirizzo o un nome univoco. Queste due entità sono gestite in Java dalla classe InetAddress, che include i seguenti metodi:
restituisce i 4 byte dell'indirizzo IP dell'istanza InetAddress corrente | |
restituisce l'indirizzo IP dell'istanza InetAddress corrente | |
restituisce il nome Internet dell'istanza InetAddress corrente | |
restituisce l'indirizzo IP/il nome Internet dell'istanza InetAddress corrente | |
crea l'istanza InetAddress per la macchina specificata da Host. Genera un'eccezione se Host è sconosciuto. Host può essere il nome Internet di una macchina o il suo indirizzo IP nella forma I1.I2.I3.I4 | |
crea l'istanza InetAddress del computer su cui è in esecuzione il programma contenente questa istruzione. |
8.2.2. Alcuni esempi
8.2.2.1. Identifica la macchina locale
import java.net.*;
public class localhost{
public static void main (String arg[]){
try{
InetAddress adresse=InetAddress.getLocalHost();
byte[] IP=adresse.getAddress();
System.out.print("IP=");
int i;
for(i=0;i<IP.length-1;i++) System.out.print(IP[i]+".");
System.out.println(IP[i]);
System.out.println("adresse="+adresse.getHostAddress());
System.out.println("nom="+adresse.getHostName());
System.out.println("identité="+adresse);
} catch (UnknownHostException e){
System.out.println ("Erreur getLocalHost : "+e);
}// fin try
}// fine hand
}// fin class
I risultati dell'esecuzione sono i seguenti:
Ogni macchina ha un indirizzo IP interno, che è 127.0.0.1. Quando un programma utilizza questo indirizzo di rete, si riferisce alla macchina su cui è in esecuzione. Il vantaggio di questo indirizzo è che non richiede una scheda di rete. Ciò significa che è possibile testare i programmi di rete senza essere connessi a una rete. Un altro modo per riferirsi alla macchina locale è utilizzare il nome localhost.
8.2.2.2. Identificazione di qualsiasi macchina
import java.net.*;
public class getbyname{
public static void main (String arg[]){
String nomMachine;
// we retrieve the argument
if(arg.length==0)
nomMachine="localhost";
else nomMachine=arg[0];
// we try to obtain the machine address
try{
InetAddress adresse=InetAddress.getByName(nomMachine);
System.out.println("IP : "+ adresse.getHostAddress());
System.out.println("nom : "+ adresse.getHostName());
System.out.println("identité : "+ adresse);
} catch (UnknownHostException e){
System.out.println ("Erreur getByName : "+e);
}// fin try
}// fine hand
}// fin class
Con la chiamata Java getByName, otteniamo i seguenti risultati:
Con la chiamata Java getbyname shiva.istia.univ-angers.fr, otteniamo:
Con la chiamata Java **getbyname www.ibm.com**, otteniamo:
8.3. Programmazione TCP-IP
8.3.1. Informazioni generali

Quando un'applicazione AppA sul computer A vuole comunicare con un'applicazione AppB sul computer B su Internet, deve conoscere diverse cose:
- l'indirizzo IP o il nome host del computer B
- il numero di porta utilizzato dall'applicazione AppB. Questo perché il computer B potrebbe ospitare molte applicazioni che operano su Internet. Quando riceve informazioni dalla rete, deve sapere a quale applicazione sono destinate. Le applicazioni sul computer B accedono alla rete tramite interfacce note anche come porte di comunicazione. Queste informazioni sono contenute nel pacchetto ricevuto dal computer B in modo che possa essere consegnato all'applicazione corretta.
- I protocolli di comunicazione compresi dal computer B. Nel nostro studio, useremo solo i protocolli TCP-IP.
- Il protocollo di comunicazione accettato dall'applicazione AppB. Infatti, le macchine A e B "parleranno" tra loro. Ciò che diranno sarà incapsulato all'interno dei protocolli TCP/IP. Tuttavia, quando, alla fine della catena, l'applicazione AppB riceve le informazioni inviate dall'applicazione AppA, deve essere in grado di interpretarle. Ciò è analogo alla situazione in cui due persone, A e B, comunicano per telefono: la loro conversazione è veicolata dal telefono. Il parlato viene codificato come segnali dal telefono A, trasmesso sulle linee telefoniche e ricevuto dal telefono B, dove viene decodificato. La persona B sente quindi il parlato. È qui che entra in gioco il concetto di protocollo di comunicazione: se A parla francese e B non capisce quella lingua, A e B non saranno in grado di comunicare efficacemente.
Pertanto, le due applicazioni che comunicano devono concordare il tipo di dialogo che useranno. Ad esempio, il dialogo con un servizio FTP non è lo stesso di quello con un servizio POP: questi due servizi non accettano gli stessi comandi. Hanno un protocollo di dialogo diverso.
8.3.2. Caratteristiche del protocollo TCP
Qui esamineremo solo le comunicazioni di rete che utilizzano il protocollo di trasporto TCP. Rivediamo le caratteristiche di questo protocollo:
- Il processo che desidera trasmettere dati stabilisce innanzitutto una connessione con il processo che riceverà le informazioni che sta per trasmettere. Questa connessione viene stabilita tra una porta sulla macchina mittente e una porta sulla macchina ricevente. Viene così creato un percorso virtuale tra le due porte, che sarà riservato esclusivamente ai due processi che hanno stabilito la connessione.
- Tutti i pacchetti inviati dal processo di origine seguono questo percorso virtuale e arrivano nell'ordine in cui sono stati inviati
- I dati trasmessi appaiono continui. Il processo mittente invia i dati al proprio ritmo. Questi dati non vengono necessariamente inviati immediatamente: il protocollo TCP attende di averne una quantità sufficiente da inviare. Essi vengono memorizzati in una struttura chiamata segmento TCP. Una volta che questo segmento è pieno, viene trasmesso al livello IP, dove viene incapsulato in un pacchetto IP.
- Ogni segmento inviato dal protocollo TCP è numerato. Il protocollo TCP ricevente verifica di ricevere i segmenti in sequenza. Per ogni segmento ricevuto correttamente, invia un riconoscimento al mittente.
- Quando il mittente riceve questo riconoscimento, ne informa il processo di invio. Il processo di invio può così confermare che un segmento è arrivato a destinazione.
- Se, dopo un certo periodo di tempo, il protocollo TCP che ha inviato un segmento non riceve un riconoscimento, ritrasmette il segmento in questione, garantendo così la qualità del servizio di consegna delle informazioni.
- Il circuito virtuale stabilito tra i due processi in comunicazione è full-duplex: ciò significa che le informazioni possono fluire in entrambe le direzioni. Pertanto, il processo di destinazione può inviare conferme anche mentre il processo di origine continua a inviare informazioni. Ciò consente, ad esempio, al protocollo TCP di origine di inviare più segmenti senza attendere una conferma. Se, dopo un certo periodo di tempo, si rende conto di non aver ricevuto un riconoscimento per un segmento specifico n. n, riprenderà l’invio dei segmenti da quel punto.
8.3.3. Il rapporto client-server
La comunicazione su Internet è spesso asimmetrica: la macchina A avvia una connessione per richiedere un servizio alla macchina B, specificando che desidera aprire una connessione con il servizio SB1 sulla macchina B. La macchina B accetta o rifiuta. Se accetta, la macchina A può inviare le proprie richieste al servizio SB1. Queste richieste devono essere conformi al protocollo di comunicazione compreso dal servizio SB1. Si instaura così un dialogo richiesta-risposta tra la macchina A, detta macchina client, e la macchina B, detta macchina server. Uno dei due partner chiuderà la connessione.
8.3.4. Architettura client
L'architettura di un programma di rete che richiede i servizi di un'applicazione server sarà la seguente:
ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
8.3.5. Architettura del server
L'architettura di un programma che offre servizi sarà la seguente:
ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
Il programma server gestisce la richiesta di connessione iniziale di un client in modo diverso dalle sue successive richieste di servizio. Il programma non fornisce il servizio stesso. Se lo facesse, non sarebbe più in ascolto delle richieste di connessione mentre il servizio è in corso e i client non verrebbero serviti. Proceda quindi in modo diverso: non appena una richiesta di connessione viene ricevuta sulla porta di ascolto e quindi accettata, il server crea un'attività responsabile di fornire il servizio richiesto dal client. Questo servizio viene fornito su un'altra porta della macchina server chiamata porta di servizio. Ciò consente di servire più client contemporaneamente.
Un'attività di servizio avrà la seguente struttura:
tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
8.3.6. La classe Socket
8.3.6.1. Definizione
Lo strumento fondamentale utilizzato dai programmi che comunicano via Internet è il socket. Questo termine inglese significa "presa elettrica"; in questo contesto, il suo significato è esteso a "presa di rete". Affinché un'applicazione possa inviare e ricevere informazioni su Internet, ha bisogno di una presa di rete, un socket. Questo strumento è stato originariamente creato nelle versioni Berkeley di Unix. Da allora è stato portato su tutti i sistemi Unix e sull'ambiente Windows. Esiste anche sulle macchine virtuali Java in due forme: la classe Socket per le applicazioni client e la classe ServerSocket per le applicazioni server. Qui spieghiamo alcuni dei costruttori e dei metodi della classe Socket:
apre una connessione remota alla porta port sulla macchina host |
restituisce il numero di porta locale utilizzato dal socket | |||
restituisce il numero della porta remota a cui è connesso il socket | |||
restituisce l'InetAddress locale a cui è associato il socket | |||
restituisce l'InetAddress remoto a cui è associato il socket | |||
restituisce un flusso di input utilizzato per leggere i dati inviati dal partner remoto | |||
restituisce un flusso di output utilizzato per inviare dati al partner remoto | |||
chiude il flusso di input del socket | |||
chiude il flusso di output del socket | |||
chiude il socket e i suoi flussi di I/O | |||
restituisce una stringa che "rappresenta" il socket | |||
8.3.6.2. Stabilire una connessione con un server
Abbiamo visto che affinché la macchina A possa stabilire una connessione con un servizio sulla macchina B, sono necessarie due informazioni:
- l'indirizzo IP o il nome host della macchina B
- il numero di porta su cui è in esecuzione il servizio desiderato
Il costruttore
crea un socket e lo connette al computer host sulla porta port. Questo costruttore genera un'eccezione in diversi casi:
- indirizzo errato
- porta errata
- richiesta rifiutata
- …
Dobbiamo gestire questa eccezione:
Socket sClient=null;
try{
sClient=new Socket(host,port);
} catch(Exception e){
// la connexion a échoué - on traite l'erreur
….
}
Se la richiesta di connessione va a buon fine, al client viene assegnata una porta locale per comunicare con il computer B. Una volta stabilita la connessione, è possibile recuperare questa porta utilizzando il metodo:
Se la connessione va a buon fine, abbiamo visto che, dal suo lato, il server dispone di un altro processo che gestisce il servizio su una cosiddetta porta di servizio. Il numero di questa porta può essere ottenuto utilizzando il metodo:
8.3.6.3. Invio di informazioni sulla rete
È possibile ottenere un flusso di scrittura sul socket — e quindi sulla rete — utilizzando il metodo:
Tutto ciò che viene inviato a questo flusso verrà ricevuto sulla porta di servizio del server. Molte applicazioni utilizzano un'interfaccia testuale costituita da righe di testo seguite da un carattere di nuova riga. Il metodo println è quindi molto utile in questi casi. Convertiamo quindi il flusso di output OutputStream in un flusso PrintWriter, che fornisce il metodo println. La scrittura può generare un'eccezione.
8.3.6.4. Lettura di informazioni dalla rete
È possibile ottenere un flusso di lettura per i dati in arrivo sul socket utilizzando il metodo:
Tutto ciò che viene letto da questo flusso proviene dalla porta di servizio del server. Per le applicazioni con una finestra di dialogo composta da righe di testo terminate da un carattere di nuova riga, è consigliabile utilizzare il metodo readLine. A tal fine, si converte l'InputStream in un BufferedReader, che dispone del metodo readLine(). La lettura potrebbe generare un'eccezione.
8.3.6.5. Chiusura della connessione
Questo viene fatto utilizzando il metodo:
Questo metodo può generare un'eccezione. Le risorse utilizzate, in particolare la porta di rete, vengono liberate.
8.3.6.6. Architettura client
Ora disponiamo degli elementi necessari per descrivere l'architettura di base di un client Internet:
Socket sClient=null;
try{
// connect to the service running on port P of machine M
sClient=new Socket(M,P);
// create client socket I/O streams
BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);
// request-response loop
boolean fini=false;
String demande;
String réponse;
while (! fini){
// preparing the application
demande=…
// we send it
out.println(demande);
// we read the answer
réponse=in.readLine();
// the answer is processed
…
}
// it's over
sClient.close();
} catch(Exception e){
// we handle the exception
….
}
Per mantenere l'esempio semplice, non abbiamo cercato di gestire i vari tipi di eccezioni generate dal costruttore Socket o dai metodi readLine, getInputStream, getOutputStream e close. Tutto è stato consolidato in un'unica eccezione.
8.3.7. La classe ServerSocket
8.3.7.1. Definizione
Questa classe è destinata alla gestione dei socket sul lato server. Di seguito illustriamo alcuni dei costruttori e dei metodi di questa classe:
crea un socket in ascolto sulla porta port | |
Come sopra, ma imposta la dimensione della coda su count, ovvero il numero massimo di connessioni client che verranno messe in coda se il server è occupato quando arriva la connessione del client. |
restituisce il numero della porta di ascolto utilizzata dal socket | |
Restituisce l'InetAddress locale a cui è associato il socket | |
Mette il server in uno stato di attesa per una connessione (operazione di blocco). All'arrivo di una connessione client, restituisce un socket attraverso il quale verrà fornito il servizio al client. | |
Chiude il socket e i relativi flussi di I/O | |
Restituisce una stringa che "rappresenta" il socket | |
chiude il socket del servizio e libera le risorse ad esso associate |
8.3.7.2. Apertura del servizio
Ciò avviene utilizzando i due costruttori:
port è la porta di ascolto del servizio: quella a cui i client inviano le loro richieste di connessione. count è la dimensione massima della coda del servizio (50 per impostazione predefinita), che memorizza le richieste di connessione dei client a cui il server non ha ancora risposto. Quando la coda è piena, le richieste di connessione in arrivo vengono rifiutate. Entrambi i costruttori generano un'eccezione.
8.3.7.3. Accettare una richiesta di connessione
Quando un client invia una richiesta di connessione alla porta di ascolto del servizio, il servizio la accetta utilizzando il metodo:
Questo metodo restituisce un'istanza Socket: si tratta del socket del servizio, attraverso il quale verrà fornito il servizio, il più delle volte da un altro thread. Il metodo può generare un'eccezione.
8.3.7.4. Lettura/scrittura tramite il socket del servizio
Poiché il socket di servizio è un'istanza della classe Socket, fare riferimento alle sezioni precedenti in cui è stato trattato questo argomento.
8.3.7.5. Identificazione del client
Una volta ottenuto il socket di servizio, è possibile identificare il client utilizzando il metodo
della classe Socket. Ciò consente di accedere all'indirizzo IP e al nome del client.
8.3.7.6. Chiusura del servizio
Questo viene effettuato utilizzando il metodo
della classe ServerSocket. Questo libera le risorse in uso, in particolare la porta di ascolto. Il metodo potrebbe generare un'eccezione.
8.3.7.7. Architettura di base del server
Sulla base di quanto detto, possiamo scrivere la struttura di base di un server:
SocketServer sEcoute=null;
try{
// ouverture du service
int portEcoute=…
int maxConnexions=…
sEcoute=new ServerSocket(portEcoute,maxConnexions);
// traitement des demandes de connexion
boolean fini=false;
Socket sService=null;
while( ! fini){
// attente et acceptation d'une demande
sService=sEcoute.accept();
// le service est rendu par une autre tâche à laquelle on passe la socket de service
new Service(sService).start();
// on se remet en attente des demandes de connexion
}
// c'est fini - on clôt le service
sEcoute.close();
} catch (Exception e){
// on traite l'exception
…
}
La classe Service è un thread che potrebbe presentarsi in questo modo:
public class Service extends Thread{
Socket sService; // service socket
// manufacturer
public Service(Socket S){
sService=S;
}
// run
public void run(){
try{
// create input-output flows
BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);
// request-response loop
boolean fini=false;
String demande;
String réponse;
while (! fini){
// we read the request
demande=in.readLine();
// we treat it
…
// we prepare the answer
réponse=…
// we send it
out.println(réponse);
}
// it's over
sService.close();
} catch(Exception e){
// we handle the exception
….
}// try
} // run
8.4. Applicazioni
8.4.1. Server Echo
Proponiamo di scrivere un server echo che verrà avviato da una finestra DOS utilizzando il comando:
Il server funziona sulla porta passata come parametro. Si limita a rinviare al client la richiesta che il client gli ha inviato, insieme all'identità del client (IP+nome). Accetta 2 connessioni nella sua coda. Qui abbiamo tutti i componenti di un server TCP. Il programma è il seguente:
// call: serveurEcho port
// echo server
// returns the line sent to the customer
import java.net.*;
import java.io.*;
public class serveurEcho{
public final static String syntaxe="Syntaxe : serveurEcho port";
public final static int nbConnexions=2;
// main program
public static void main (String arg[]){
// is there an argument
if(arg.length != 1)
erreur(syntaxe,1);
// this argument must be integer >0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(arg[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// create the listening socket
ServerSocket ecoute=null;
try{
ecoute=new ServerSocket(port,nbConnexions);
} catch (Exception e){
erreur("Erreur lors de la création de la socket d'écoute ("+e+")",3);
}
// follow-up
System.out.println("Serveur d'écho lancé sur le port " + port);
// service loop
boolean serviceFini=false;
Socket service=null;
while (! serviceFini){
// waiting for a customer
try{
service=ecoute.accept();
} catch (IOException e){
erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
}
// we identify the link
try{
System.out.println("Client ["+identifie(service.getInetAddress())+","+
service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
+ "," + service.getLocalPort() + "]");
} catch (Exception e) {
erreur("identification liaison",1);
}
// the service is provided by another task
new traiteClientEcho(service).start();
}// end while
}// fine hand
// error display
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
// identifies
private static String identifie(InetAddress Host){
// host identification
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// end class
// provides service to an echo server client
class traiteClientEcho extends Thread{
private Socket service; // service socket
private BufferedReader in; // iNPUTS
private PrintWriter out; // output flow
// manufacturer
public traiteClientEcho(Socket service){
this.service=service;
}
// run method
public void run(){
// creation of input and output flows
try{
in=new BufferedReader(new InputStreamReader(service.getInputStream()));
} catch (IOException e){
erreur("Erreur lors de la création du flux déentrée de la socket de service ("+e+")",1);
}// fin try
try{
out=new PrintWriter(service.getOutputStream(),true);
} catch (IOException e){
erreur("Erreur lors de la création du flux de sortie de la socket de service ("+e+")",1);
}// fin try
// link identification is sent to the customer
try{
out.println("Client ["+identifie(service.getInetAddress())+","+
service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
+ "," + service.getLocalPort() + "]");
} catch (Exception e) {
erreur("identification liaison",1);
}
// loop read request/write response
String demande,reponse;
try{
// the service stops when the client sends an end-of-file marker
while ((demande=in.readLine())!=null){
// echo of demand
reponse="["+demande+"]";
out.println(reponse);
// service stops when client sends "end
if(demande.trim().toLowerCase().equals("fin")) break;
}// end while
} catch (IOException e){
erreur("Erreur lors des échanges client/serveur ("+e+")",3);
}// fin try
// close the socket
try{
service.close();
} catch (IOException e){
erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
}// fin try
}// end run
// error display
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}// end error
// identifies
private String identifie(InetAddress Host){
// host identification
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// fin class
Le due classi necessarie per il servizio sono state unite in un unico file sorgente. Solo una di esse, quella contenente la funzione main, ha l'attributo public. La struttura del server è conforme all'architettura generale dei server TCP. È stato aggiunto un metodo (identify) per identificare la connessione tra il server e un client. Ecco alcuni risultati:
Il server viene avviato dal comando
Viene quindi visualizzato il seguente messaggio nella finestra della console:
Per testare questo server, utilizziamo il programma telnet, disponibile sia su Unix che su Windows. Telnet è un client TCP universale adatto a tutti i server che accettano righe di testo terminate da un carattere di fine riga nella loro comunicazione. Questo è il caso del nostro server echo. Avviamo un primo client telnet su Windows (2000 in questo esempio) digitando telnet in una finestra DOS:
DOS>telnet
Microsoft (R) Windows 2000 (TM) version 5.00 (numéro 2195)
Client Telnet Microsoft
Client Telnet numéro 5.00.99203.1
Le caractère d'escapement is 'CTRL+$'
Microsoft Telnet> help
Les commandes peuvent être abrégées. Les commandes prises en charge sont :
close ferme la connexion en cours
display affiche les paramètres d'operation
open ouvre une connexion à un site
quit quitte telnet
set définit les options (entrez 'set ?' to display the list)
status affiche les informations d'status
unset annule les options (entrez 'unset ?' to display the list)
? ou help affiche des informations d'help
Microsoft Telnet> set ?
NTLM Active l'authentication NTLM.
LOCAL_ECHO Active l'local echo.
TERM x (où x est ANSI, VT100, VT52 ou VTNT))
CRLF Envoi de CR et de LF
Microsoft Telnet> set local_echo
Microsoft Telnet> open localhost 187
Per impostazione predefinita, il programma Telnet non ripete i comandi digitati sulla tastiera. Per abilitare questa funzione, immettere il comando:
Per aprire una connessione al server, specificando la porta del servizio echo (187) e l'indirizzo del computer su cui è in esecuzione (localhost), digitare il seguente comando:
Nella finestra DOS del client, verrà quindi visualizzato il messaggio:
Nella finestra del server appare il seguente messaggio:
Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
In questo caso, tahe e localhost si riferiscono alla stessa macchina. Nella finestra del client Telnet è possibile digitare righe di testo. Il server le ripete:
Client [127.0.0.1,tahe,1059] connectÚ au serveur [127.0.0.1,tahe,187]
je suis là
[je suis là]
au revoir
[au revoir]
Si noti che la porta client (1059) viene rilevata correttamente, ma la porta del servizio (187) è identica alla porta di ascolto (187), il che è inaspettato. Ci si aspetterebbe infatti di ottenere la porta del socket del servizio piuttosto che la porta di ascolto. Dovremmo verificare se otteniamo gli stessi risultati su Unix. Ora, avviamo un secondo client telnet. La finestra del server diventa:
Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
Nella finestra del secondo client, è anche possibile digitare righe di testo:
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]
Ciò dimostra che il server echo può servire più client contemporaneamente. I client Telnet possono essere chiusi chiudendo la finestra DOS in cui sono in esecuzione.
8.4.2. Un client Java per il server echo
Nella sezione precedente abbiamo utilizzato un client Telnet per testare il servizio echo. Ora scriveremo il nostro client:
// call: clientEcho machine port
// echo server client
// sends lines to the server, which echoes them back to the server
import java.net.*;
import java.io.*;
public class clientEcho{
public final static String syntaxe="Syntaxe : clientEcho machine port";
// main program
public static void main (String arg[]){
// are there two arguments
if(arg.length != 2)
erreur(syntaxe,1);
// the first argument must be the name of an existing machine
String machine=arg[0];
InetAddress serveurAddress=null;
try{
serveurAddress=InetAddress.getByName(machine);
} catch (Exception e){
erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
}
// port must be integer >0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(arg[1]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\nPort incorrect ("+E+")",3);
// connect to the server
Socket sClient=null;
try{
sClient=new Socket(machine,port);
} catch (Exception e){
erreur("Erreur lors de la création de la socket de communication ("+e+")",4);
}
// we identify the link
try{
System.out.println("Client : Client ["+identifie(InetAddress.getLocalHost())+","+
sClient.getLocalPort()+"] connecté au serveur [" + identifie (sClient.getInetAddress())
+ "," + sClient.getPort() + "]");
} catch (Exception e) {
erreur("identification liaison ("+e+")",5);
}
// creation of a flow for reading lines typed on the keyboard
BufferedReader IN=null;
try{
IN=new BufferedReader(new InputStreamReader(System.in));
} catch (Exception e){
erreur("Création du flux d'entrée clavier ("+e+")",6);
}
// creation of the input stream associated with the client socket
BufferedReader in=null;
try{
in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
} catch (Exception e){
erreur("Création du flux d'entrée de la socket client("+e+")",7);
}
// creation of the output stream associated with the client socket
PrintWriter out=null;
try{
out=new PrintWriter(sClient.getOutputStream(),true);
} catch (Exception e){
erreur("Création du flux de sortie de la socket ("+e+")",8);
}
// request-response loops
boolean serviceFini=false;
String demande=null;
String reponse=null;
// we read the message sent by the server just after connection
try{
reponse=in.readLine();
} catch (IOException e){
erreur("Lecture réponse ("+e+")",4);
}
// response display
System.out.println("Serveur : " +reponse);
while (! serviceFini){
// read a line typed on the keyboard
System.out.print("Client : ");
try{
demande=IN.readLine();
} catch (Exception e){
erreur("Lecture ligne ("+e+")",9);
}
// sending demand on the network
try{
out.println(demande);
} catch (Exception e){
erreur("Envoi demande ("+e+")",10);
}
// wait/read answer
try{
reponse=in.readLine();
} catch (IOException e){
erreur("Lecture réponse ("+e+")",4);
}
// response display
System.out.println("Serveur : " +reponse);
// is it over?
if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
}
// it's over
try{
sClient.close();
} catch(Exception e){
erreur("Fermeture socket ("+e+")",11);
}
}// hand
// error display
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
// identifies
private static String identifie(InetAddress Host){
// host identification
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// fin class
La struttura di questo client è conforme all'architettura generale dei client TCP. In questo caso, abbiamo gestito una per una le varie eccezioni possibili, il che appesantisce il programma. Ecco i risultati ottenuti durante il test di questo client:
Client : Client [127.0.0.1,tahe,1045] connecté au serveur [127.0.0.1,localhost,187]
Serveur : Client [127.0.0.1,localhost,1045] connectÚ au serveur [127.0.0.1,tahe,187]
Client : 123
Serveur : [123]
Client : abcd
Serveur : [abcd]
Client : je suis là
Serveur : [je suis là]
Client : fin
Serveur : [fin]
Le righe che iniziano con Client sono quelle inviate dal client, mentre quelle che iniziano con Server sono quelle restituite dal server.
8.4.3. Un client TCP generico
Molti servizi creati agli albori di Internet funzionano secondo il modello echo server discusso in precedenza: la comunicazione client-server consiste nello scambio di righe di testo. Scriveremo un client TCP generico che verrà avviato come segue: java cltTCPgenerique server port
Questo client TCP si connetterà alla porta port sul server server. Una volta connesso, creerà due thread:
- un thread responsabile della lettura dei comandi digitati sulla tastiera e del loro invio al server
- un thread responsabile della lettura delle risposte del server e della loro visualizzazione sullo schermo
Perché due thread quando non era necessario nell'applicazione precedente? In quell'applicazione, il protocollo di comunicazione era fisso: il client inviava una singola riga e il server rispondeva con una singola riga. Ogni servizio ha il proprio protocollo specifico e ci imbattiamo anche nelle seguenti situazioni:
- il client deve inviare diverse righe di testo prima di ricevere una risposta
- la risposta di un server può contenere più righe di testo
Pertanto, il ciclo che invia una singola riga al server e riceve una singola riga dal server non è sempre adatto. Creeremo quindi due cicli separati:
- un ciclo per leggere i comandi digitati sulla tastiera da inviare al server. L'utente segnalerà la fine dei comandi con la parola chiave "fin".
- un ciclo per ricevere e visualizzare le risposte del server. Si tratterà di un ciclo infinito che verrà interrotto solo dalla chiusura della connessione di rete da parte del server o dalla digitazione del comando "end" da parte dell'utente sulla tastiera.
Per avere questi due cicli separati, abbiamo bisogno di due thread indipendenti. Vediamo un esempio di esecuzione in cui il nostro client TCP generico si connette a un servizio SMTP (Simple Mail Transfer Protocol). Questo servizio è responsabile dell'inoltro delle e-mail ai destinatari. Opera sulla porta 25 e utilizza un protocollo di scambio basato su testo.
Dos>java clientTCPgenerique istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test
ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Commentiamo questi scambi client-server:
- Il servizio SMTP invia un messaggio di benvenuto quando un client si connette ad esso:
- Alcuni servizi dispongono di un comando "help" che fornisce informazioni sui comandi disponibili per quel servizio. In questo caso non è così. I comandi SMTP utilizzati nell'esempio sono i seguenti:
- mail from: mittente, per specificare l'indirizzo e-mail del mittente
- rcpt to: destinatario, per specificare l'indirizzo e-mail del destinatario del messaggio. Se ci sono più destinatari, il comando rcpt to: viene ripetuto tante volte quante necessarie per ciascun destinatario.
- data, che segnala al server SMTP che il messaggio sta per essere inviato. Come indicato nella risposta del server, questo consiste in una serie di righe che terminano con una riga contenente solo un punto. Un messaggio può avere intestazioni separate dal corpo del messaggio da una riga vuota. Nel nostro esempio, abbiamo incluso un oggetto utilizzando la parola chiave Subject:
- Una volta inviato il messaggio, possiamo comunicare al server che abbiamo terminato utilizzando il comando quit. Il server chiude quindi la connessione di rete. Il thread di lettura può rilevare questo evento e arrestarsi.
- L'utente digita quindi "end" sulla tastiera per arrestare anche il thread che legge i comandi digitati sulla tastiera.
Se controlliamo l'e-mail ricevuta, vediamo quanto segue (Outlook):

Si noti che il servizio SMTP non è in grado di rilevare se un mittente sia valido o meno. Pertanto, non ci si può mai fidare del campo "Da" di un messaggio. In questo caso, il mittente machin@univ-angers.fr non esisteva.
Questo client TCP generico ci permette di scoprire il protocollo di comunicazione dei servizi Internet e, da lì, creare classi specializzate per i client di questi servizi. Esploriamo il protocollo di comunicazione del servizio POP (Post Office Protocol), che ci permette di recuperare le email memorizzate su un server. Opera sulla porta 110.
Dos> java clientTCPgenerique istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<-- by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<-- Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<-- by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<-- Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2 Tl. : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 , Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
I comandi principali sono i seguenti:
- user login, dove si inserisce il proprio login sulla macchina che ospita le proprie e-mail
- pass password, dove si inserisce la password associata al login precedente
- list, per ottenere un elenco dei messaggi nel formato numero, dimensione in byte
- retr i, per leggere il messaggio numero i
- quit, per terminare la sessione.
Esploriamo ora il protocollo di comunicazione tra un client e un server web, che in genere funziona sulla porta 80:
Dos> java clientTCPgenerique istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
<--
<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<--
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Un client web invia i propri comandi al server secondo il seguente schema:
Il server web risponde solo dopo aver ricevuto la riga vuota. In questo esempio, abbiamo utilizzato un solo comando:
che richiede l'URL /index.html dal server e indica che sta utilizzando la versione 1.0 di HTTP. La versione più recente di questo protocollo è la 1.1. L'esempio mostra che il server ha risposto inviando il contenuto del file index.html e poi ha chiuso la connessione, come possiamo vedere dalla lettura della risposta "thread terminating". Prima di inviare il contenuto del file index.html, il server web ha inviato una serie di intestazioni seguite da una riga vuota:
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
La riga <html> è la prima riga del file /index.html. Il testo che la precede è chiamato intestazioni HTTP (HyperText Transfer Protocol). Non entreremo nei dettagli su queste intestazioni in questa sede, ma tieni presente che il nostro client generico fornisce l'accesso ad esse, il che può essere utile per comprenderle. Ad esempio, la prima riga:
indica che il server web contattato supporta il protocollo HTTP/1.1 e che ha trovato con successo il file richiesto (200 OK), dove 200 è un codice di risposta HTTP. Le righe
comunicano al client che riceverà 11.251 byte di testo HTML (HyperText Markup Language) e che la connessione verrà chiusa una volta inviati i dati.
Ecco quindi un client TCP molto pratico. Certamente fa meno del programma telnet che abbiamo usato in precedenza, ma è stato interessante scriverlo da soli. Il programma client TCP generico è il seguente:
// imported packages
import java.io.*;
import java.net.*;
public class clientTCPgenerique{
// receives the characteristics of a service as a parameter in the form
// server port
// connects to the service
// creates a thread to read keyboard commands
// these will be sent to the
// creates a thread to read server responses
// these will be displayed on the screen
// the whole thing ends with the command end typed on the keyboard
// instance variable
private static Socket client;
public static void main(String[] args){
// syntax
final String syntaxe="pg serveur port";
// number of arguments
if(args.length != 2)
erreur(syntaxe,1);
// note the server name
String serveur=args[0];
// port must be integer >0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(args[1]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
client=null;
// there may be problems
try{
// connect to the service
client=new Socket(serveur,port);
}catch(Exception ex){
// error
erreur("Impossible de se connecter au service ("+ serveur
+","+port+"), erreur : "+ex.getMessage(),3);
// end
return;
}//catch
// create read/write threads
new ClientSend(client).start();
new ClientReceive(client).start();
// end thread main
return;
}// hand
// error display
public static void erreur(String msg, int exitCode){
// error display
System.err.println(msg);
// stop with error
System.exit(exitCode);
}//error
}//class
class ClientSend extends Thread {
// class for reading keyboard commands
// and send them to a server via a tcp client passed in parameter
private Socket client; // tcp client
// manufacturer
public ClientSend(Socket client){
// we note the tcp client
this.client=client;
}//manufacturer
// thread Run method
public void run(){
// local data
PrintWriter OUT=null; // network write streams
BufferedReader IN=null; // keyboard flow
String commande=null; // command read from keyboard
// error management
try{
// network write stream creation
OUT=new PrintWriter(client.getOutputStream(),true);
// keyboard input stream creation
IN=new BufferedReader(new InputStreamReader(System.in));
// order entry-send loop
System.out.println("Commandes : ");
while(true){
// read command typed on keyboard
commande=IN.readLine().trim();
// finished?
if (commande.toLowerCase().equals("fin")) break;
// send order to server
OUT.println(commande);
// next order
}//while
}catch(Exception ex){
// error
System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// end - we close the feeds
try{
OUT.close();client.close();
}catch(Exception ex){}
// signals the end of the thread
System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
}//run
}//class
class ClientReceive extends Thread{
// class responsible for reading lines of text intended for a
// tcp client passed as parameter
private Socket client; // tcp client
// manufacturer
public ClientReceive(Socket client){
// we note the tcp client
this.client=client;
}//manufacturer
// thread Run method
public void run(){
// local data
BufferedReader IN=null; // network read stream
String réponse=null; // server response
// error management
try{
// create network read stream
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
// loop read text lines from IN stream
while(true){
// network streaming
réponse=IN.readLine();
// closed flow?
if(réponse==null) break;
// display
System.out.println("<-- "+réponse);
}//while
}catch(Exception ex){
// error
System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// end - we close the feeds
try{
IN.close();client.close();
}catch(Exception ex){}
// signals the end of the thread
System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
}//run
}//class
8.4.4. Un server TCP generico
Ora vedremo un server
- che visualizza sullo schermo i comandi inviati dai propri client
- e invia loro, in risposta, le righe di testo digitate sulla tastiera da un utente. È quindi l’utente a fungere da server.
Il programma viene avviato con: java genericTCPserver listeningPort, dove listeningPort è la porta a cui i client devono connettersi. Il servizio client sarà gestito da due thread:
- un thread dedicato esclusivamente alla lettura delle righe di testo inviate dal client
- un thread dedicato esclusivamente alla lettura delle risposte digitate dall'utente. Questo thread segnalerà, utilizzando il comando `fin`, che sta chiudendo la connessione con il client.
Il server crea due thread per ogni client. Se ci sono n client, ci saranno 2n thread attivi contemporaneamente. Il server stesso non si ferma mai a meno che l'utente non prema Ctrl-C sulla tastiera. Vediamo alcuni esempi.
Il server è in esecuzione sulla porta 100 e utilizziamo il client generico per comunicare con esso. La finestra del client ha questo aspetto:
E:\data\serge\MSNET\c#\réseau\client tcp générique> java clientTCPgenerique localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]
Le righe che iniziano con <-- sono quelle inviate dal server al client; le altre provengono dal client al server. La finestra del server è la seguente:
Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Le righe che iniziano con <-- sono quelle inviate dal cliente al server. Le righe N: sono quelle inviate dal server al cliente N. Il server sopra indicato è ancora attivo anche se il cliente 1 ha terminato. Avviamo un secondo cliente per lo stesso server:
Dos> java clientTCPgenerique localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]
La finestra del server appare quindi così:
Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- commande 3 du client 2
réponse 3 au client 2
2 : [fin du Thread de lecture des demandes du client 2]
fin
[fin du Thread de lecture des réponses du serveur au client 2]
^C
Ora simuliamo un server web avviando il nostro server generico sulla porta 88:
Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Ora apriamo un browser e richiediamo l'URL http://localhost:88/exemple.html. Il browser si connetterà quindi alla porta 88 sul computer localhost e richiederà la pagina /example.html:

Ora diamo un'occhiata alla finestra del nostro server:
Dos>java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
Questo mostra le intestazioni HTTP inviate dal browser. Ciò ci permette di imparare gradualmente il protocollo HTTP. In un esempio precedente, abbiamo creato un client web che inviava solo il comando GET. Era sufficiente. Qui vediamo che il browser invia ulteriori informazioni al server. Queste informazioni hanno lo scopo di comunicare al server con quale tipo di client ha a che fare. Notiamo inoltre che le intestazioni HTTP terminano con una riga vuota.
Creiamo una risposta per il nostro client. L'utente alla tastiera è il vero e proprio server in questo caso e può creare manualmente una risposta. Ricordiamo la risposta inviata da un server web in un esempio precedente:
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
Proviamo a fornire una risposta simile:
...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 : <head><title>Serveur generique</title></head>
2 : <body>
2 : <center>
2 : <h2>Reponse du serveur generique</h2>
2 : </center>
2 : </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]
Le righe che iniziano con 2: vengono inviate dal server al client n. 2. Il comando end chiude la connessione dal server al client. Nella nostra risposta, ci siamo limitati alle seguenti intestazioni HTTP:
HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
Non specifichiamo la dimensione del file che stiamo inviando (Content-Length), ma indichiamo semplicemente che chiuderemo la connessione (Connection: close) dopo l'invio. Questo è sufficiente per il browser. Non appena rileva la chiusura della connessione, capirà che la risposta del server è completa e visualizzerà la pagina HTML che gli è stata inviata. La pagina è la seguente:
2 : <html>
2 : <head><title>Serveur generique</title></head>
2 : <body>
2 : <center>
2 : <h2>Reponse du serveur generique</h2>
2 : </center>
2 : </body>
2 : </html>
L'utente chiude quindi la connessione al client digitando il comando "fin". Il browser capisce così che la risposta del server è completa e può visualizzarla:

Se, nell'esempio sopra riportato, si seleziona Visualizza/Sorgente per vedere cosa ha ricevuto il browser, si ottiene:

cioè esattamente ciò che è stato inviato dal server generico.
Il codice per il server TCP generico è il seguente:
// packages
import java.io.*;
import java.net.*;
public class serveurTCPgenerique{
// main program
public static void main (String[] args){
// receives the port of listening to customer requests
// creates a thread to read client requests
// these will be displayed on the screen
// creates a thread to read keyboard commands
// these will be sent as a reply to the customer
// the whole thing ends with the command end typed on the keyboard
final String syntaxe="Syntaxe : pg port";
// instance variable
// is there an argument
if(args.length != 1)
erreur(syntaxe,1);
// port must be integer >0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(args[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// we create the listening service
ServerSocket ecoute=null;
int nbClients=0; // no. of customers handled
try{
// create the service
ecoute=new ServerSocket(port);
// follow-up
System.out.println("Serveur générique lancé sur le port " + port);
// customer service loop
Socket client=null;
while (true){ // infinite loop - will be stopped by Ctrl-C
// waiting for a customer
client=ecoute.accept();
// the service is provided by separate threads
nbClients++;
// create read/write threads
new ServeurSend(client,nbClients).start();
new ServeurReceive(client,nbClients).start();
// back to listening to requests
}// end while
}catch(Exception ex){
// we report the error
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
}//catch
}// fine hand
// error display
public static void erreur(String msg, int exitCode){
// error display
System.err.println(msg);
// stop with error
System.exit(exitCode);
}//error
}//class
class ServeurSend extends Thread{
// class responsible for reading typed responses
// and send them to a client via a tcp client passed to the
Socket client; // tcp client
int numClient; // customer no
// manufacturer
public ServeurSend(Socket client, int numClient){
// we note the tcp client
this.client=client;
// and its
this.numClient=numClient;
}//manufacturer
// thread Run method
public void run(){
// local data
PrintWriter OUT=null; // network write streams
String réponse=null; // answer read from keyboard
BufferedReader IN=null; // keyboard flow
// follow-up
System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
// error management
try{
// network write stream creation
OUT=new PrintWriter(client.getOutputStream(),true);
// keyboard flow creation
IN=new BufferedReader(new InputStreamReader(System.in));
// order entry-send loop
while(true){
// customer identification
System.out.print("--> " + numClient + " : ");
// read response typed on keyboard
réponse=IN.readLine().trim();
// finished?
if (réponse.toLowerCase().equals("fin")) break;
// send response to server
OUT.println(réponse);
// following response
}//while
}catch(Exception ex){
// error
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// end - we close the feeds
try{
OUT.close();client.close();
}catch(Exception ex){}
// signals the end of the thread
System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
}//run
}//class
class ServeurReceive extends Thread{
// class responsible for reading text lines sent to the server
// via a tcp client passed to the builder
Socket client; // tcp client
int numClient; // customer no
// manufacturer
public ServeurReceive(Socket client, int numClient){
// we note the tcp client
this.client=client;
// and its
this.numClient=numClient;
}//manufacturer
// thread Run method
public void run(){
// local data
BufferedReader IN=null; // network read stream
String réponse=null; // server response
// follow-up
System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
// error management
try{
// create network read stream
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
// loop read text lines from IN stream
while(true){
// network streaming
réponse=IN.readLine();
// closed flow?
if(réponse==null) break;
// display
System.out.println("<-- "+réponse);
}//while
}catch(Exception ex){
// error
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// end - we close the feeds
try{
IN.close();client.close();
}catch(Exception ex){}
// signals the end of the thread
System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
}//run
}//class
8.4.5. Un client Web
Nell'esempio precedente abbiamo visto alcune delle intestazioni HTTP inviate da un browser:
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
Scriveremo un client web che accetta un URL come parametro e visualizza il contenuto di quell'URL sullo schermo. Supporremo che il server web contattato per l'URL supporti il protocollo HTTP 1.1. Tra le intestazioni elencate sopra, useremo solo le seguenti:
- La prima intestazione indica quale pagina vogliamo
- il secondo specifica quale server stiamo interrogando
- La terza indica che vogliamo che il server chiuda la connessione dopo averci risposto.
Se nell'esempio sopra riportato sostituiamo GET con HEAD, il server ci invierà solo le intestazioni HTTP e non la pagina HTML.
Il nostro client web verrà chiamato nel modo seguente: java clientweb URL cmd, dove URL è l'URL desiderato e cmd è una delle due parole chiave GET o HEAD per indicare se vogliamo solo le intestazioni (HEAD) o anche il contenuto della pagina (GET). Vediamo un primo esempio. Avviamo il server IIS e poi il client web sulla stessa macchina:
dos>java clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private
La risposta
indica che la pagina richiesta è stata spostata (ovvero, il suo URL è cambiato). Il nuovo URL è fornito dall'intestazione Location:
Se utilizziamo GET invece di HEAD nella chiamata del client Web:
dos>java clientweb http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:33:36 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=IMFNCCMDAKPNNGMGMFIHENFE; path=/
Cache-control: private
<head><title>L'objet a changé d'emplacement</title></head>
<body><h1>L'objet a changé d'emplacement</h1>Cet objet peut être trouvé <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>
Otteniamo lo stesso risultato di HEAD, più il corpo della pagina HTML. Il programma è il seguente:
// imported packages
import java.io.*;
import java.net.*;
public class clientweb{
// requests a URL
// displays its contents on the screen
public static void main(String[] args){
// syntax
final String syntaxe="pg URI GET/HEAD";
// number of arguments
if(args.length != 2)
erreur(syntaxe,1);
// note the URI required
String URLString=args[0];
String commande=args[1].toUpperCase();
// URI validity check
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI incorrect
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//catch
// order verification
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// incorrect order
erreur("Le second paramètre doit être GET ou HEAD",3);
}
// extract useful information from URL
String path=url.getPath();
if(path.equals("")) path="/";
String query=url.getQuery();
if(query!=null) query="?"+query; else query="";
String host=url.getHost();
int port=url.getPort();
if(port==-1) port=url.getDefaultPort();
// we can work
Socket client=null; // the customer
BufferedReader IN=null; // the customer's reading flow
PrintWriter OUT=null; // the customer's writing flow
String réponse=null; // server response
try{
// connect to the server
client=new Socket(host,port);
// create customer input/output flows TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// request URL - send HTTP headers
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// we read the answer
while((réponse=IN.readLine())!=null){
// the answer is processed
System.out.println(réponse);
}//while
// it's over
client.close();
} catch(Exception e){
// we handle the exception
erreur(e.getMessage(),4);
}//catch
}//hand
// error display
public static void erreur(String msg, int exitCode){
// error display
System.err.println(msg);
// stop with error
System.exit(exitCode);
}//error
}//class
L'unica novità di questo programma è l'uso della classe URL. Il programma riceve un URL (Uniform Resource Locator) o URI (Uniform Resource Identifier) nella forma http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... La classe URL ci permette di scomporre la stringa URL nei suoi vari componenti. Un oggetto URL viene costruito dalla stringa URL ricevuta come parametro:
// vérification validité de l'URL
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI incorrecte
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//catch
Se la stringa URL ricevuta come parametro non è un URL valido (protocollo mancante, server, ecc.), viene generata un'eccezione. Questo ci permette di verificare la validità del parametro ricevuto. Una volta costruito l'oggetto URL, abbiamo accesso ai suoi vari elementi. Pertanto, se l'oggetto URL del codice precedente fosse stato costruito dalla stringa
avremo:
url.getHost() = server
url.getPort()=porta o -1 se la porta non è specificata
url.getPath() = HTMLPagePath o una stringa vuota se non c'è alcun percorso
url.getQuery() = param1=val1;param2=val2;... oppure null se non c'è query
uri.getProtocol() = http
8.4.6. Gestione dei reindirizzamenti da parte del client Web
Il client web precedente non gestisce alcun reindirizzamento dell'URL richiesto. Il client seguente lo gestisce.
- Legge la prima riga delle intestazioni HTTP inviate dal server per verificare se contiene la stringa "302 Object moved", che indica un reindirizzamento
- legge le intestazioni successive. Se c'è un reindirizzamento, cerca la riga "Location: url" che fornisce il nuovo URL della pagina richiesta e ne prende nota.
- Visualizza il resto della risposta del server. Se c'è un reindirizzamento, i passaggi da 1 a 3 vengono ripetuti con il nuovo URL. Il programma non accetta più di un reindirizzamento. Questo limite è definito da una costante che può essere modificata.
Ecco un esempio:
Dos>java clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private
<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>
<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781
<html>
<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
</html>
Il programma è il seguente:
// imported packages
import java.io.*;
import java.net.*;
import java.util.regex.*;
public class clientweb2{
// requests a URL
// displays its contents on the screen
public static void main(String[] args){
// syntax
final String syntaxe="pg URL GET/HEAD";
// number of arguments
if(args.length != 2)
erreur(syntaxe,1);
// note the URI required
String URLString=args[0];
String commande=args[1].toUpperCase();
// URI validity check
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI incorrect
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//catch
// order verification
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// incorrect order
erreur("Le second paramètre doit être GET ou HEAD",3);
}
// we can work
Socket client=null; // the customer
BufferedReader IN=null; // the customer's reading flow
PrintWriter OUT=null; // the customer's writing flow
String réponse=null; // server response
final int nbRedirsMax=1; // no more than one redirection accepted
int nbRedirs=0; // number of redirects in progress
String premièreLigne; // 1st line of the answer
boolean redir=false; // indicates redirection or not
String locationString=""; // the URL string of a possible redirection
// regular expression to find a URL redirect
Pattern location=Pattern.compile("^Location: (.+?)$");
// error management
try{
// you may have several URL to request if there are redirections
while(nbRedirs<=nbRedirsMax){
// extract useful information from URL
String protocol=url.getProtocol();
String path=url.getPath();
if(path.equals("")) path="/";
String query=url.getQuery();
if(query!=null) query="?"+query; else query="";
String host=url.getHost();
int port=url.getPort();
if(port==-1) port=url.getDefaultPort();
// connect to the server
client=new Socket(host,port);
// create customer input/output flows TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// request URL - send HTTP headers
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// read the first line of the answer
premièreLigne=IN.readLine();
// screen echo
System.out.println(premièreLigne);
// redirection?
if(premièreLigne.endsWith("302 Object moved")){
// there is a redirection
redir=true;
nbRedirs++;
}//if
// next HTTP headers until you find the empty line signalling the end of the headers
boolean locationFound=false;
while(!(réponse=IN.readLine()).equals("")){
// the answer is displayed
System.out.println(réponse);
// if there is a redirection, we search for the Location header
if(redir && ! locationFound){
// compare the line with the relational expression location
Matcher résultat=location.matcher(réponse);
if(résultat.find()){
// if found, note the URL of redirection
locationString=résultat.group(1);
// we note that we found
locationFound=true;
}//if
}//if
// next header
}//while
// following lines of the answer
System.out.println(réponse);
while((réponse=IN.readLine())!=null){
// the answer is displayed
System.out.println(réponse);
}//while
// close the connection
client.close();
// are we done?
if ( ! locationFound || nbRedirs>nbRedirsMax)
break;
// there is a redirection to be made - the new URL is built
URLString=protocol +"://"+host+":"+port+locationString;
url=new URL(URLString);
// follow-up
System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
}//while
} catch(Exception e){
// we handle the exception
erreur(e.getMessage(),4);
}//catch
}//hand
// error display
public static void erreur(String msg, int exitCode){
// error display
System.err.println(msg);
// stop with error
System.exit(exitCode);
}//error
}//class
8.4.7. Server di calcolo delle imposte
Stiamo riesaminando l'esercizio TAXES, che è già stato trattato in varie forme. Rivediamo l'ultima versione:
È stata creata una classe base denominata **impots**. I suoi attributi sono tre array di numeri:
public class impots{
// data required for tax calculation
// come from an external source
protected double[] limites=null;
protected double[] coeffR=null;
protected double[] coeffN=null;
// empty builder
protected impots(){}
// manufacturer
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
La classe impots ha due costruttori:
- un costruttore che accetta i tre array di dati necessari per calcolare l'imposta
- un costruttore senza parametri utilizzabile solo dalle classi figlie
La classe **impotsJDBC** è stata derivata da questa classe, consentendo di popolare i tre array limites*, *coeffR* e coeffN* con i contenuti di un database:
public class impotsJDBC extends impots{
// addition of a constructor for building
// limit tables, coeffr, coeffn from table
// database taxes
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
// dsnIMPOTS: DSN database name
// userIMPOTS, mdpIMPOTS: database login/password
Era stata scritta un'applicazione grafica. L'applicazione utilizzava un oggetto della classe impotsJDBC. L'applicazione e questo oggetto si trovavano sulla stessa macchina. Proponiamo di collocare il programma di test e l'oggetto impotsJDBC su macchine diverse. Avremo un'applicazione client-server in cui l'oggetto impotsJDBC remoto fungerà da server. La nuova classe si chiama TaxServer ed è derivata dalla classe impotsJDBC:
// imported packages
import java.net.*;
import java.io.*;
import java.sql.*;
public class ServeurImpots extends impotsJDBC {
// attributes
int portEcoute; // the ability to listen to customer requests
boolean actif; // server status
// manufacturer
public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
throws IOException, SQLException, ClassNotFoundException {
// parent construction
super(DSNimpots, USERimpots, MDPimpots);
// we note the listening port
this.portEcoute=portEcoute;
// currently inactive
actif=false;
// creates and launches a thread for reading keyboard commands
// the server will be managed using these commands
Thread admin=new Thread(){
public void run(){
try{
admin();
}catch (Exception ignored){}
}
};
admin.start();
}//ServeurImpots
L'unico nuovo parametro nel costruttore è la porta utilizzata per ascoltare le richieste dei client. Gli altri parametri vengono passati direttamente alla classe base impotsJDBC. Il server fiscale è controllato da comandi immessi tramite la tastiera. Pertanto, creiamo un thread per leggere questi comandi. Ci saranno due comandi possibili: start per avviare il servizio e stop per arrestarlo definitivamente. Il metodo admin che gestisce questi comandi è il seguente:
public void admin() throws IOException{
// reads server administration commands typed from the keyboard
// in an endless loop
String commande=null;
BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
while(true){
// invite
System.out.print("Serveur d'impôts>");
// read command
commande=IN.readLine().trim().toLowerCase();
// order execution
if(commande.equals("start")){
// active?
if(actif){
//error
System.out.println("Le serveur est déjà actif");
// we continue
continue;
}//if
// create and launch the listening service
Thread ecoute=new Thread(){
public void run(){
ecoute();
}
};
ecoute.start();
}//if
else if(commande.equals("stop")){
// end of all execution threads
System.exit(0);
}//if
else {
// error
System.out.println("Commande incorrecte. Utilisez (start,stop)");
}//if
}//while
}//admin
Se il comando immesso tramite la tastiera è start, viene avviato un thread che ascolta le richieste dei client. Se il comando immesso è stop, tutti i thread vengono arrestati. Il thread di ascolto esegue il metodo listen:
public void ecoute(){
// thread for listening to customer requests
// we create the listening service
ServerSocket ecoute=null;
try{
// create the service
ecoute=new ServerSocket(portEcoute);
// follow-up
System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);
// service loop
Socket liaisonClient=null;
while (true){ // infinite loop
// waiting for a customer
liaisonClient=ecoute.accept();
// the service is provided by another task
new traiteClientImpots(liaisonClient,this).start();
// back to listening to requests
}// end while
}catch(Exception ex){
// we report the error
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
}//catch
}//listening thread
Qui abbiamo un classico server TCP in ascolto sulla porta portEcoute. Le richieste dei client vengono gestite dal metodo run del thread traiteCientImpots, al quale vengono passati due parametri al momento della creazione:
- l'oggetto Socket liaisonClient, che consentirà l'accesso al client
- l'oggetto impotsJDBC, che fornisce l'accesso al metodo this.calculate per il calcolo dell'imposta.
// -------------------------------------------------------
// provides service to a tax server client
class traiteClientImpots extends Thread{
private Socket liaisonClient; // customer liaison
private BufferedReader IN; // iNPUTS
private PrintWriter OUT; // output flow
private impotsJDBC objImpots; // object Tax
// manufacturer
public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
this.liaisonClient=liaisonClient;
this.objImpots=objImpots;
}//manufacturer
Il metodo run elabora le richieste dei clienti. Si tratta di righe di testo che possono assumere due forme:
- sposato(sì/no) numeroFigli stipendioAnnuale
- endCalculation
Il modulo 1 calcola l'imposta, mentre il modulo 2 chiude la connessione client-server.
// run method
public void run(){
// renders service to the customer
try{
// iNPUTS
IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
// output flow
OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
// send a welcome message to the customer
OUT.println("Bienvenue sur le serveur d'impôts");
// loop read request/write response
String demande=null;
String[] champs=null; // elements of the request
String commande=null; // customer order: calculation or fincalculs
while ((demande=IN.readLine())!=null){
// demand is broken down into fields
champs=demande.trim().toLowerCase().split("\\s+");
// two successful applications: calcul and fincalculs
commande=champs[0];
if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
// customer error
OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
// next order
continue;
}//if
if(commande.equals("calcul")) calculerImpôt(champs);
if(commande.equals("fincalculs")){
// good-bye message to customer
OUT.println("Au revoir...");
// freeing up resources
try{ OUT.close();IN.close();liaisonClient.close();}
catch(Exception ex){}
// end
return;
}//if
//following request
}//while
}catch (Exception e){
erreur("L'erreur suivante s'est produite ("+e+")",2);
}// fin try
}// end Run
Il calcolo dell'imposta viene eseguito dal metodo calculateTax, che accetta come parametro l'array dei campi della richiesta del cliente. Viene verificata la validità della richiesta e, se valida, l'imposta viene calcolata e restituita al cliente.
// tax calculation
public void calculerImpôt(String[] champs){
// processing the application: calculation married nbEnfants salaireAnnuel
// broken down into fields in the fields table
String marié=null;
int nbEnfants=0;
int salaireAnnuel=0;
// validity of arguments
try{
// at least 4 fields are required
if(champs.length!=4) throw new Exception();
// married
marié=champs[1];
if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
// children
nbEnfants=Integer.parseInt(champs[2]);
// salary
salaireAnnuel=Integer.parseInt(champs[3]);
}catch (Exception ignored){
// format error
OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
// finish
return;
}//if
// tax can be calculated
long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
// we send the response to the customer
OUT.println(""+impot);
}//calculate
Un programma di prova potrebbe essere simile a questo:
// call: serveurImpots port dsnImpots userImpots mdpImpots
import java.io.*;
public class testServeurImpots{
public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";
// main program
public static void main (String[] args){
// you need 4 arguments
if(args.length != 4)
erreur(syntaxe,1);
// port must be integer >0
int port=0;
boolean erreurPort=false;
Exception E=null;
try{
port=Integer.parseInt(args[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// we create the tax server
try{
new ServeurImpots(port,args[1],args[2],args[3]);
}catch(Exception ex){
//error
System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
}//catch
}//Main
// error display
public static void erreur(String msg, int exitCode){
// error display
System.err.println(msg);
// stop with error
System.exit(exitCode);
}//error
}// end class
Passiamo al programma di test i dati necessari per costruire un oggetto TaxServer, e da lì il programma crea questo oggetto.
Proviamo a eseguirlo per la prima volta:
dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop
Il comando
crea un oggetto TaxServer che non è ancora in ascolto delle richieste dei client. È il comando start digitato sulla tastiera che avvia questo processo di ascolto. Il comando stop arresta il server. Utilizziamo ora un client. Useremo il client generico creato in precedenza. Il server è in esecuzione:
dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
Il client generico viene avviato in un'altra finestra DOS:
Possiamo vedere che il client ha ricevuto correttamente il messaggio di benvenuto dal server. Inviamo altri comandi:
x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<-- syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Torniamo alla finestra del server per interromperlo:
dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop
8.5. Esercizi
8.5.1. Esercizio 1 - Grafico generico del client TCP
8.5.1.1. Panoramica dell'applicazione
Proponiamo di creare un programma in grado di comunicare via Internet con i principali servizi TCP. Lo chiameremo client TCP generico. Una volta compresa questa applicazione, vi renderete conto che tutti i client TCP sono simili. La finestra del programma ha questo aspetto:

I significati dei vari controlli sono i seguenti:
N. | Nome | Tipo | ruolo |
1 | TxtRemoteHost | JTextField | nome del computer che fornisce il servizio richiesto |
2 | TxtPort | JTextField | porta del servizio richiesto |
3 | TxtSend | JTextField | testo del messaggio da inviare al server da parte del client |
4 | OptRCLF OptLF | JCheckBox | pulsanti utilizzati per specificare come terminano le righe nella finestra di dialogo client/server RCLF: ritorno a capo (#13) + avanzamento riga (#10) LF: avanzamento riga (#10) |
5 | LstSuivi | JList | visualizza i messaggi relativi allo stato della comunicazione tra il client e il server |
6 | LstDialogue | JList | visualizza i messaggi scambiati dal client (->) e dal server (<-) |
7 | CmdCancel | JButton | nascosto - situato sotto l'elenco della finestra di dialogo - Appare quando la connessione è in corso e consente di interromperla se il server non risponde |
Le opzioni di menu disponibili sono le seguenti:
opzione | sottoopzioni | ruolo |
Connessione | Connetti | collega il client al server |
Disconnetti | Chiude la connessione | |
Esci | Esce dal programma | |
Messaggi | Invia | Invia il messaggio dal controllo TxtSend al server |
ClearTracking | Cancella l'elenco LstSuivi | |
ClearDialogue | Cancella l'elenco LstDialogue | |
Autore | Visualizza una finestra di copyright |
8.5.1.2. COME FUNZIONA L'APPLICAZIONE
Quando viene caricata la finestra principale dell'applicazione, vengono eseguite le seguenti azioni:
- il foglio viene centrato sullo schermo
- Sono attive solo le opzioni di menu "Accedi/Esci" e "Autore"
- Il pulsante Annulla è nascosto
- Gli elenchi LstSuivi e LstDialogue sono vuoti
Questa opzione è disponibile solo quando i campi Host remoto e N. porta non sono vuoti e non c'è alcuna connessione attualmente attiva. Facendo clic su questa opzione si eseguono le seguenti operazioni:
- La porta viene convalidata: deve essere un numero intero maggiore di 0
- Viene avviato un thread per stabilire la connessione al server
- Viene visualizzato il pulsante Annulla per consentire all'utente di interrompere la connessione in corso
- Tutte le opzioni del menu sono disabilitate tranne Esci e Autore
La connessione può terminare in diversi modi:
- L'utente ha cliccato sul pulsante Annulla: il thread di connessione viene interrotto e il menu viene riportato allo stato iniziale. Il log indica che l'utente ha chiuso la connessione.
- La connessione termina con un errore: si procede come in precedenza e, in aggiunta, nel log viene indicata la causa dell'errore.
- La connessione viene completata con successo: il pulsante Annulla viene rimosso, il log indica che la connessione è stata stabilita, il menu ResetLog è abilitato, il menu Connetti è disabilitato e il menu Disconnetti è abilitato
Questa opzione è disponibile solo quando è attiva una connessione al server. Se attivata, chiude la connessione al server e riporta il menu allo stato iniziale. Il registro indica che la connessione è stata chiusa dal client.
Questa opzione chiude qualsiasi connessione attiva al server e chiude l'applicazione.
Questa opzione è disponibile solo se sono soddisfatte le seguenti condizioni:
-
è stata stabilita una connessione al server
-
è presente un messaggio da inviare
Se queste condizioni sono soddisfatte, il testo nel campo TxtSend (3) viene inviato al server, terminato dalla sequenza RCLF se l'opzione RCLF è stata selezionata, oppure dalla sequenza LF in caso contrario. Eventuali errori di trasmissione vengono segnalati nell'elenco di tracciamento.
Cancella rispettivamente gli elenchi LstSuivi e LstDialogue. Queste opzioni sono disabilitate quando gli elenchi corrispondenti sono vuoti.
Questo pulsante, situato nella parte inferiore del modulo, appare solo quando il client sta tentando di connettersi al server. La connessione potrebbe non andare a buon fine perché il server non risponde o risponde in modo errato. Il pulsante Annulla offre quindi all'utente la possibilità di interrompere la richiesta di connessione.
L'elenco LstSuivi (5) tiene traccia della connessione. Indica i momenti chiave della connessione:
-
la sua creazione da parte del client
-
la sua chiusura da parte del server o del client
-
eventuali errori che possono verificarsi mentre la connessione è attiva
La lista LstDialogue (6) tiene traccia del dialogo stabilito tra il client e il server. Un thread monitora in background ciò che accade sul socket di comunicazione del client e lo visualizza nella lista 6.
Questo menu apre una finestra denominata Copyright:

Gli errori di connessione vengono segnalati nell'elenco di tracciamento 6, mentre quelli relativi al dialogo client/server vengono segnalati nell'elenco dei dialoghi 7. In caso di errore di connessione, il dialogo client/server viene chiuso e il modulo viene riportato allo stato iniziale, pronto per una nuova connessione.
8.5.1.3. ATTIVITÀ
Implementare il lavoro sopra descritto in due forme:
- applicazione autonoma
- applet
8.5.2. Esercizio 2 - Un server di risorse
8.5.2.1. INTRODUZIONE
Un'istituzione dispone di diversi potenti server di calcolo accessibili tramite Internet. Qualsiasi macchina che desideri utilizzare questi servizi di calcolo invia un file di dati alla porta 756 su uno dei server. Questo file contiene varie informazioni: login, password, comandi che specificano il tipo di calcolo desiderato e i dati su cui eseguire il calcolo. Se il file di dati è valido, il server di calcolo selezionato lo elabora e restituisce i risultati al client sotto forma di file di testo.
I vantaggi di questa configurazione sono numerosi:
- qualsiasi tipo di client (PC, Mac, Unix, ecc.) può utilizzare questo servizio
- il client può trovarsi ovunque su Internet
- le risorse di calcolo sono ottimizzate: sono necessarie solo poche macchine potenti. Pertanto, una piccola organizzazione priva di risorse di calcolo può utilizzare questo servizio a un costo calcolato in base al tempo di calcolo utilizzato.
Nonostante la potenza delle macchine, un calcolo può talvolta richiedere diverse ore: il server risulta quindi non disponibile per altri client. Ciò pone il problema per un client di trovare un server di calcolo disponibile. Per risolvere questo problema, viene utilizzato un “gestore delle risorse di calcolo”, di seguito denominato server GRC. Questo servizio gira su una singola macchina e opera sulla porta 864 in modalità TCP. Un cliente che desidera accedere a un server di calcolo contatta questo servizio. Il server GRC, che mantiene un elenco completo dei server di calcolo, risponde inviando al cliente il nome di un server attualmente inattivo. Il cliente invia quindi semplicemente i propri dati al server designato.
Proponiamo di sviluppare il server GRC.
8.5.2.2. L'INTERFACCIA VISIVA
L'interfaccia visiva sarà la seguente:

L'interfaccia mostra due elenchi di server:
- a sinistra, l'elenco dei server inattivi, che sono quindi disponibili per i calcoli
- a destra, l'elenco dei server occupati dai calcoli di un cliente.
La struttura del menu è la seguente:
Menu principale | Menu secondario | Ruolo |
Servizio | Avvia | Avvia il servizio TCP sulla porta 864 |
Arresta | Arresta il servizio | |
Esci | Esci dall'applicazione | |
Autore | Informazioni sul copyright |
La struttura dei controlli nel modulo è la seguente:
Nome | Tipo | Ruolo |
listLibres | JList | Elenco dei server disponibili |
listBusy | JList | Elenco dei server occupati |
8.5.2.3. COME FUNZIONA L'APP
Quando l'applicazione viene caricata, l'elenco listLibres viene popolato con l'elenco dei nomi dei server di calcolo gestiti dal GRC. Questi sono definiti in un file Servers passato come parametro. Questo file contiene un elenco di nomi di server, uno per riga, e viene quindi utilizzato per popolare l'elenco listLibres. Il menu Start è abilitato; il menu Stop è disabilitato.
Questa opzione
- avvia il servizio di ascolto sulla porta 864 della macchina
- disabilita il menu Avvia
- abilita il menu Stop
Questa opzione arresta il servizio:
- l'elenco dei server occupati viene cancellato
- l'elenco dei server liberi viene popolato con il contenuto del file Servers
- il menu Avvia viene abilitato
- il menu Stop viene disabilitato
L'applicazione si chiude.
Il dialogo client/server avviene tramite lo scambio di righe di testo terminate dalla sequenza RCLF. Il server GRC riconosce due comandi: getserver e endservice. Di seguito descriviamo in dettaglio il ruolo di questi due comandi:
- 1-getserver
Il client chiede se è disponibile un server di calcolo per lui.
Il server GRC prende quindi il primo server trovato nel proprio elenco di server disponibili e restituisce il suo nome al client nel seguente formato:
Inoltre, sposta il server assegnato al client nell'elenco dei server occupati nel seguente formato:
come mostrato nell'esempio seguente, in cui il server *calcul1.istia.univ-angers.fr* è occupato a servire il client con indirizzo IP *193.52.43.5*:

Un client non può inviare un comando **getserver** se gli è già stato assegnato un server di calcolo. Pertanto, prima di rispondere al client, il server GRC verifica che l'indirizzo IP del client non sia già presente tra quelli registrati nell'elenco dei server occupati. Se così fosse, il server GRC risponde:
Infine, c'è il caso in cui non sia disponibile alcun server di calcolo: l'elenco dei server liberi è vuoto. In questo caso, il server GRC risponde:
In tutti i casi, dopo aver risposto al client, il server GRC chiude la connessione con il client in modo da poter servire altri client.
- 2-finservice
Il client indica che non ha più bisogno del server di calcolo che stava utilizzando.
Il server GRC verifica innanzitutto che il cliente sia effettivamente uno di quelli che stava servendo. A tal fine, controlla se l'indirizzo IP del cliente è tra quelli registrati nell'elenco dei server occupati. Se così non fosse, il server GRC risponde:
Se il client viene riconosciuto, il server GRC risponde:
e sposta il server di calcolo assegnato a quel client nell'elenco dei server liberi. Per riprendere l'esempio precedente, se il client invia il comando **di fine servizio**, il display del server GRC diventa:

Dopo aver inviato la risposta, indipendentemente dal suo contenuto, il server GRC chiude la connessione.
8.5.2.4. ATTIVITÀ
Scrivi l'applicazione come programma autonomo che possa essere testato, ad esempio, con un client telnet o con il client TCP generico dell'esercizio precedente.
8.5.3. Esercizio 3 - Un client SMTP
8.5.3.1. INTRODUZIONE
In questa sede, vogliamo realizzare un client per il servizio SMTP (Simple Mail Transfer Protocol), che consente di inviare e-mail. Su Unix o Windows, il programma telnet è un client che funziona con il protocollo TCP. Può "comunicare" con qualsiasi servizio TCP che accetti comandi testuali terminanti con la sequenza RCLF, ovvero i caratteri ASCII 13 e 10. Ecco un esempio di conversazione con il servizio SMTP per l'invio di e-mail:
$ telnet istia.univ-angers.fr 25 // appel du service smtp
// risposta dal server SMTP
Trying 193.52.43.2...
Connected to istia.univ-angers.fr.
Escape character is '^]'.
220-Istia.Istia.Univ-Angers.fr Sendmail 8.6.10/8.6.9 ready at Tue, 16 Jan 1996 07:53:12 +0100
220 ESMTP spoken here
// commenti --------------
Il programma telnet può connettersi a qualsiasi servizio utilizzando la sintassi
***telnet*** **macchina\_servizio porta\_servizio**
Gli scambi client/server utilizzano righe di testo terminate dalla sequenza RCLF.
Le risposte dal servizio SMTP hanno la forma:
**Numero-messaggio oppure**
**Numero del messaggio**
Il server SMTP può inviare più righe di risposta. L'ultima riga della risposta è indicata da un numero seguito da uno spazio, mentre per le righe precedenti della risposta il numero è seguito da un trattino -.
Un numero maggiore o uguale a 500 indica un messaggio di errore.
// Fine dei commenti
// risposta dal server SMTP
214-Commands:
214- HELO EHLO MAIL RCPT DATA
214- RSET NOOP QUIT HELP VRFY
214- EXPN VERB
214-For more info use "HELP <topic>".
214-To report bugs in the implementation send email to
214- sendmail@CS.Berkeley.EDU.
214-For local information send email to Postmaster at your site.
214 End of HELP info
mail from: serge.tahe@istia.univ-angers.fr // nouvelle commande émise au clavier
// commenti ---------
Il comando mail ha la seguente sintassi:
**mail da: indirizzo e-mail del mittente del messaggio**
// fine dei commenti
// risposta dal server SMTP
// commenti
Il server SMTP non verifica la validità dell'indirizzo del mittente: lo accetta così com'è
// fine dei commenti
rcpt to: user1@istia.univ-angers.fr // nouvelle commande émise au clavier
// commenti ---------
Il comando rcpt ha la seguente sintassi:
**rcpt a: indirizzo e-mail del destinatario del messaggio**
Se l'indirizzo e-mail è un indirizzo presente sul computer su cui è in esecuzione il server SMTP, ne verifica l'esistenza; in caso contrario, non esegue alcuna verifica. Se è stata eseguita la verifica ed è stato rilevato un errore, questo verrà segnalato con un numero >= 500.
Puoi inserire tutti i comandi "rcpt" che vuoi: questo ti permette di inviare un messaggio a più persone.
// fine dei commenti
// Risposta del server SMTP
// commenti ---------
Il comando data ha la seguente sintassi:
**data**
**riga1**
**riga2**
**...**
**.**
Seguono le righe di testo che compongono il messaggio, che deve terminare con una riga contenente solo il carattere "punto".
Il messaggio viene quindi inviato al destinatario specificato dal comando rcpt.
// fine dei commenti
// risposta dal server SMTP
// testo del messaggio digitato sulla tastiera
subject: essai smtp
essai smtp a partir de telnet
.
// commenti
Nelle righe di testo del comando data, è possibile includere una riga subject: per specificare l'oggetto dell'e-mail. Questa riga deve essere seguita da una riga vuota.
// risposta dal server SMTP
// commenti
**Il comando quit chiude la connessione al servizio** ***SMTP***
// fine dei commenti
// risposta dal server SMTP
8.5.3.2. L'INTERFACCIA VISIVA
Proponiamo di realizzare un programma con la seguente interfaccia visiva:

I controlli hanno le seguenti funzioni:
Numero | Tipo | Ruolo |
1 | JTextField | Un elenco di indirizzi e-mail separati da virgole |
2 | JTextField | Testo dell'oggetto del messaggio |
3 | JTextField | Elenco di indirizzi e-mail separati da virgole |
4 | JTextField | Un elenco di indirizzi e-mail separati da virgole |
5 | JTextArea | Testo del messaggio |
6 | JList | Elenco dei tracciamenti |
7 | JList | elenco di dialogo |
8 | JButton | Pulsante "Annulla" non visibile, che appare quando il client richiede una connessione al server SMTP. Consente all'utente di annullare questa richiesta se il server non risponde. |
8.5.3.3. MENU
La struttura dei menu dell'applicazione è la seguente:
Menu principale | Menu secondario | Ruolo |
Posta | ||
Invia | Invia il messaggio dal controllo 5 | |
Esci | Esci dall'app | |
Opzioni | ||
Nascondi tracciamento | Nascondi controllo 6 | |
Cancella lista di controllo | Cancella la lista di controllo 6 | |
Nascondi finestra di dialogo | Nasconde la finestra di dialogo 7 | |
Cancella finestra di dialogo | Cancella l'elenco delle finestre di dialogo 7 | |
Configura | Consente all'utente di specificare - l'indirizzo del server SMTP utilizzato dal programma - il proprio indirizzo e-mail | |
Salva... | Salva la configurazione precedente in un file .ini | |
Autore | Informazioni sul copyright |
8.5.3.4. COME FUNZIONA L'APPLICAZIONE
Questo menu visualizza la seguente finestra:

Entrambi i campi devono essere compilati affinché il pulsante OK sia attivo. Entrambe le informazioni devono essere memorizzate in variabili globali in modo che siano disponibili per altri moduli.
Questa opzione è disponibile solo se sono soddisfatte le seguenti condizioni:
- la configurazione è stata completata
- c'è un messaggio da inviare
- c'è un oggetto
- c'è almeno un destinatario nei campi 1, 3 e 4
Se queste condizioni sono soddisfatte, la sequenza degli eventi è la seguente:
- il modulo viene impostato in uno stato in cui tutte le azioni che potrebbero interferire con il dialogo client/server sono disabilitate
- Viene stabilita una connessione sulla porta 25 del server specificato nella configurazione
- Il client comunica quindi con il server SMTP secondo il protocollo sopra descritto
- Il campo "Mail From" utilizza l'indirizzo e-mail del mittente specificato nella configurazione
- Il comando "rcpt to:" viene utilizzato per ciascuno degli indirizzi e-mail presenti nei campi 1, 3 e 4
- Nelle righe inviate dopo il comando data, apparirà il seguente testo:
- una riga Subject:: testo dell'oggetto dal controllo 2
- una riga Cc: indirizzi da test 3
- una riga Bcc: indirizzi dal test 4
- il testo del messaggio per il controllo 5
- il punto finale
Questo pulsante, situato nella parte inferiore del modulo, appare solo quando il client sta tentando di connettersi al server SMTP. La connessione potrebbe non andare a buon fine perché il server SMTP non risponde o risponde in modo errato. Il pulsante Annulla consente quindi all'utente di interrompere la richiesta di connessione.
L'elenco (6) tiene traccia della connessione. Indica i momenti chiave della connessione:
- la sua creazione da parte del client
- la sua chiusura da parte del server o del client
- tutti gli errori di connessione
L'elenco (7) tiene traccia del dialogo SMTP stabilito tra il client e il server.
Questi due elenchi sono associati alle opzioni del menu:
Nascondi tracciamento | Nasconde l'elenco di tracciamento 6 e l'etichetta sopra di esso. Se l'altezza occupata da questi due controlli è H, tutti i controlli situati sotto di essi vengono spostati verso l'alto di un'altezza pari a H e la dimensione totale del modulo viene ridotta di H. Inoltre, Nascondi tracciamento nasconde l'opzione Cancella tracciamento sottostante. |
Cancella tracciamento | Cancella l'elenco di tracciamento 6 |
Nascondi finestra di dialogo | Nasconde l'elenco della finestra di dialogo 7, l'etichetta sopra di esso e l'opzione di menu Cancella finestra di dialogo sottostante. Come per Nascondi tracciamento, la posizione dei controlli situati sotto (ad esempio il pulsante Annulla) viene ricalcolata e la dimensione della finestra viene ridotta. |
Cancella finestra di dialogo | Cancella l'elenco delle finestre di dialogo 7 |
Questo menu apre una finestra denominata Copyright:

Gli errori di connessione vengono segnalati nell'elenco di tracciamento 6, mentre quelli relativi alla finestra di dialogo client/server vengono segnalati nell'elenco della finestra di dialogo 7. Quando si verifica un errore, l'utente viene avvisato tramite una finestra di errore e viene visualizzato l'elenco contenente la causa dell'errore, se in precedenza era nascosto. Inoltre, la finestra di dialogo client/server viene chiusa e il modulo viene riportato allo stato iniziale.
8.5.3.5. GESTIONE DI UN FILE DI CONFIGURAZIONE
È auspicabile che l'utente non debba riconfigurare il software ogni volta che lo utilizza. A tal fine, se l'opzione Opzioni/Salva configurazione all'uscita è selezionata, la chiusura del programma salva le due informazioni impostate tramite l'opzione Opzioni/Configura, nonché lo stato dei due elenchi di tracciamento, in un file sendmail.ini situato nella stessa directory del file .exe del programma. Questo file ha il seguente formato:
SmtpServer=shiva.istia.univ-angers.fr
ReplyAddress=serge.tahe@istia.univ-angers.fr
Suivi=0
Dialogue=1
Le righe SmtpServer e ReplyAddress contengono le due informazioni inserite tramite il menu Opzioni/Configura. Le righe Tracking e Dialogue indicano lo stato delle liste di tracciamento e di dialogo: 1 (presente), 0 (assente).
All'avvio del programma, se esiste il file sendmail.ini, questo viene letto e il modulo viene configurato di conseguenza. Se il file sendmail.ini non esiste, il programma agisce come se fosse presente:
Se il file sendmail.ini esiste ma è incompleto (mancano delle righe), la riga mancante viene sostituita dalla riga corrispondente sopra riportata. Pertanto, se manca la riga Suivi=..., la trattiamo come se avessimo Suivi=1.
Tutte le righe che non corrispondono al modello:
vengono ignorate, così come quelle in cui la parola chiave non è valida. La parola chiave può essere scritta in maiuscolo o minuscolo: non ha importanza.
Nel menu Opzioni/Configura vengono visualizzati i valori correnti di SmtpServer e ReplyAddress. L'utente può quindi modificarli se lo desidera.
8.5.3.6. ATTIVITÀ DA SVOLGERE
Completare l'operazione sopra descritta. Si consiglia di occuparsi della gestione dei file di configurazione per ultima.
8.5.4. Esercizio 4 - Client POPPASS
8.5.4.1. Introduzione
Proponiamo di creare un client TCP in grado di comunicare con il server POPPASSD in esecuzione sulla porta 106. Questo servizio consente di modificare la propria password su una macchina UNIX. Il protocollo di comunicazione client/server è il seguente:
1 - La comunicazione avviene tramite lo scambio di messaggi terminati dalla sequenza RCLF
2 - Il client invia i comandi al server
- Il server risponde con messaggi che iniziano con un numero di 3 cifre: XXX. Se XXX=200, il comando è stato eseguito con successo; in caso contrario, si è verificato un errore.
3 - La sequenza degli scambi è la seguente:
- Il server risponde con un messaggio di benvenuto
- Il server risponde richiedendo la password se il login viene accettato; in caso contrario, restituisce un errore
- Il server risponde richiedendo la nuova password se la password viene accettata; in caso contrario, restituisce un errore
- Il server risponde confermando che la nuova password è stata accettata; in caso contrario, restituisce un errore
- Il server invia un messaggio di fine sessione e chiude la connessione
8.5.4.2. Il modulo client

Il significato dei vari controlli è il seguente:
N. | nome | tipo | ruolo |
1 | txtRemoteHost | JTextField | nome del server |
2 | txtLogin | JTextField | accesso utente |
3 | txtPassword | JTextField | Password utente |
4 | txtNuovaPassword | JTextField | Nuova password dell'utente |
5 | txtConferma | JTextField | Conferma nuova password |
6 | lstTracking | JList | Messaggi di tracciamento della connessione |
7 | lstDialogue | JList | Messaggi di dialogo client/server |
10 | cmdCancel | JButton | non mostrato - Pulsante che appare quando la connessione al server è in corso. Consente di interromperla. |
8.5.4.3. Menu
Titolo | Nome controllo | Funzione |
Connessione | loginmenu | |
Connetti | mnuconnect | avvia la connessione al server |
Esci | mnuQuitter | chiude l'applicazione |
Messaggi | mnuMessaggi | |
Cancella cronologia | mnuClearTracking | cancella l'elenco lstSuivi |
ClearDialog | mnuClearDialog | cancella l'elenco lstDialogue |
Author | mnuAuthor | visualizza la finestra del copyright |
8.5.4.4. Come funziona l'applicazione
Quando viene caricato il foglio principale dell'applicazione, si verificano le seguenti azioni:
- il foglio viene centrato sullo schermo
- sono attive solo le opzioni di menu Login/Logout e Autorizza
- il pulsante Annulla è nascosto
- gli elenchi LstSuivi e LstDialogue sono vuoti
Questa opzione è disponibile solo quando i campi da 1 a 5 sono stati compilati. Facendo clic su questa opzione si attivano le seguenti azioni:
- Viene avviato un thread per stabilire la connessione al server
- viene visualizzato il pulsante Annulla per consentire all'utente di interrompere la connessione in corso
- tutte le opzioni del menu vengono disabilitate tranne Esci e Autore
La sequenza di eventi è quindi la seguente:
- L'utente ha cliccato sul pulsante Annulla: il thread di connessione viene interrotto e il menu viene riportato allo stato iniziale. Il log indica che l'utente ha chiuso la connessione.
- La richiesta di connessione viene accettata dal server. Avviamo quindi il dialogo con il server per modificare la password. Gli scambi in questo dialogo vengono registrati nell'elenco LstDialogue. Una volta completato il dialogo, la connessione con il server viene chiusa e il menu del modulo viene riportato allo stato iniziale.
- Finché il dialogo è attivo, il pulsante Annulla rimane visibile per consentire all'utente di chiudere la connessione se lo desidera.
- Se si verifica un errore durante la comunicazione, la connessione viene chiusa e la causa dell'errore viene visualizzata nell'elenco di tracciamento LstSuivi.
Questa opzione chiude tutte le connessioni attive con il server e chiude l'applicazione.
Cancella rispettivamente gli elenchi LstSuivi e LstDialogue. Queste opzioni sono disabilitate quando gli elenchi corrispondenti sono vuoti.
Questo pulsante, situato nella parte inferiore del modulo, appare solo quando il client si sta connettendo o è connesso al server. Il pulsante Annulla consente all'utente di interrompere la comunicazione con il server.
L'elenco LstSuivi (5) tiene traccia della connessione. Indica i momenti chiave della connessione:
-
la sua instaurazione da parte del client
-
la sua chiusura da parte del server o del client
-
eventuali errori che possono verificarsi mentre la connessione è attiva
La lista LstDialogue (6) tiene traccia del dialogo tra il client e il server.
Questo menu apre una finestra denominata Finestra Copyright:

Gli errori di comunicazione vengono segnalati nell'elenco di tracciamento 6, mentre quelli relativi al dialogo client/server vengono segnalati nell'elenco di dialogo 7. In caso di errore di connessione, il dialogo client/server viene chiuso e il modulo viene riportato allo stato iniziale, pronto per una nuova connessione.
8.5.4.5. ATTIVITÀ
Implementare il lavoro descritto sopra come applicazione autonoma e poi come applet.