8. Programmazione TCP-IP
8.1. Informazioni generali
8.1.1. I protocolli di Internet
In questa sede forniamo un'introduzione ai protocolli di comunicazione di Internet, noti anche come suite di protocolli TCP/IP (Transfer Control Protocol / Internet Protocol), dal nome dei due protocolli principali. È opportuno che il lettore abbia una comprensione generale del funzionamento delle reti e, in particolare, dei protocolli TCP/IP prima di affrontare la realizzazione di applicazioni distribuite.
Il testo che segue è una traduzione parziale di un testo tratto dal documento "Lan Workplace for Dos - Administrator's Guide" di NOVELL, un documento risalente agli inizi degli anni '90.
Il concetto generale di creazione di una rete di computer eterogenei deriva dalle ricerche condotte 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 denominata ARPAnet, rete che in seguito è diventata la rete INTERNET. I protocolli TCP/IP definiscono formati e regole di trasmissione e ricezione indipendenti dall’organizzazione delle reti e dall’hardware utilizzato.
La rete progettata dal DARPA e gestita dai protocolli TCP/IP è una rete a commutazione di pacchetti. Una rete di questo tipo trasmette le informazioni sulla rete in piccoli frammenti chiamati pacchetti. Pertanto, se un computer trasmette un file di grandi dimensioni, quest’ultimo verrà suddiviso in piccoli frammenti che saranno inviati sulla rete per essere ricomposti a destinazione. 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 approssimativamente il modello di rete aperta denominato OSI (Open Systems Interconnection Reference Model) definito dall'ISO (International Standards Organisation). 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 al livello superiore. Supponiamo che due applicazioni situate su macchine diverse, A e B, vogliano comunicare: lo fanno a livello del livello Application. Non hanno bisogno di conoscere tutti i dettagli del funzionamento della rete: ogni applicazione trasmette le informazioni che desidera inviare al livello sottostante, ovvero il livello Présentation. L’applicazione deve quindi conoscere solo le regole di interfaccia con il livello Présentation.
Una volta che l'informazione si trova nel livello Présentation, viene trasferita, secondo altre regole, al livello Session e così via, fino a quando l'informazione non raggiunge il supporto fisico e viene trasmessa fisicamente al dispositivo di destinazione. A quel punto, subirà il processo inverso rispetto a quello a cui è stata sottoposta sul computer mittente.
Ad ogni livello, il processo mittente incaricato di inviare le informazioni le invia a un processo ricevente sull’altra macchina appartenente allo stesso livello. Lo fa secondo determinate regole che vengono definite protocollo di livello. Si ottiene quindi il seguente schema di comunicazione finale:

Il ruolo dei diversi livelli è il seguente:
Garantisce la trasmissione dei bit su un supporto fisico. In questo livello si trovano apparecchiature terminali per l’elaborazione dei dati (E.T.T.D), quali terminali o computer, nonché apparecchiature di terminazione dei circuiti dati (E.T.C.D), quali modulatori/demodulatori, multiplexer e concentratori. I punti di interesse 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 devono seguire le informazioni inviate sulla rete. Questo processo è denominato routage: determinare il percorso che un’informazione deve seguire per arrivare al destinatario. | |
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 potrà utilizzare una stessa connessione di rete (da macchina a macchina) per trasmettere informazioni appartenenti a più applicazioni. | |
In questo livello si trovano servizi che consentono a un’applicazione di aprire e mantenere una sessione di lavoro su una macchina remota. | |
Il suo scopo è uniformare la rappresentazione dei dati sulle diverse macchine. Pertanto, i dati provenienti da una macchina A saranno “formattati” dal livello Présentation della macchina A secondo un formato standard prima di essere inviati sulla rete. Una volta giunti al livello Présentation del computer destinatario B, che li riconoscerà grazie al loro formato standard, verranno rielaborati in modo diverso affinché l’applicazione del computer B possa riconoscerli. | |
A questo livello si trovano le applicazioni 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 mai realizzato. La suite di protocolli TCP/IP se ne avvicina nella forma seguente:

Livello fisico
Nelle reti locali si utilizzano generalmente le tecnologie Ethernet o Token-Ring. In questa sede ci limiteremo a presentare la tecnologia Ethernet.
Ethernet
È il nome dato a una tecnologia di reti locali a commutazione di pacchetti inventata da PARC Xerox all’inizio degli 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 di lunghezza non superiore a 500 m. Può essere estesa tramite répéteurs, ma due dispositivi non possono essere separati da più di due ripetitori. Il cavo è passivo: tutti i componenti attivi si trovano sulle apparecchiature collegate al cavo. Ciascuna apparecchiatura è collegata al cavo tramite una scheda di accesso alla rete che comprende:
- un trasmettitore (transceiver) 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 trasmettitore 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: un dispositivo che trasmette invia le informazioni sul cavo indicando l’indirizzo del dispositivo destinatario. Tutti i dispositivi collegati ricevono quindi queste informazioni e solo quello a cui sono destinate le conserva.
- Il metodo di accesso è il seguente: il trasmettitore che desidera trasmettere ascolta il cavo e rileva così la presenza o meno di un'onda portante, la cui presenza indicherebbe che è in corso una trasmissione. Si tratta della tecnica CSMA (Carrier Sense Multiple Access). In assenza di portante, un trasmettitore può decidere di trasmettere a sua volta. Possono essere diversi i trasmettitori a prendere questa decisione. I segnali emessi si sovrappongono: si parla allora di collisione. Il trasmettitore rileva questa situazione: mentre trasmette sul cavo, ascolta ciò che effettivamente vi transita. Se rileva che l’informazione che transita sul cavo non è quella che ha trasmesso, ne deduce che vi è una collisione e interromperà la trasmissione. Gli altri trasmettitori che stavano trasmettendo faranno lo stesso. Ciascuno riprenderà la propria trasmissione dopo un intervallo di tempo casuale che dipende da ciascun trasmettitore. Questa tecnica è denominata CD (Collision Detect). Il metodo di accesso è quindi denominato CSMA/CD.
- un indirizzamento a 48 bit. Ogni macchina ha un indirizzo, qui denominato indirizzo fisico, che è riportato sulla scheda che la collega al cavo. Questo indirizzo è denominato indirizzo Ethernet della macchina.
Livello di rete
A questo livello troviamo i protocolli IP, ICMP, ARP e RARP.
IP (Internet Protocol) | Trasmette pacchetti tra due nodi della rete |
ICMP (Internet Control Message Protocol) | ICMP gestisce la comunicazione tra il programma del protocollo IP di una macchina e quello di un'altra macchina. Si tratta quindi di un protocollo di scambio di messaggi all'interno dello stesso protocollo IP. |
ARP (Address Resolution Protocol) | effettua la corrispondenza tra l'indirizzo Internet della macchina e l'indirizzo fisico della macchina |
RARP (Reverse Address Resolution Protocol) | effettua la corrispondenza tra l'indirizzo fisico del computer e l'indirizzo Internet del computer |
Livelli di trasporto/sessione
In questo livello si trovano i seguenti protocolli:
TCP (Transmission Control Protocol) | Garantisce una trasmissione affidabile delle informazioni tra due client |
UDP (User Datagram Protocol) | Garantisce una trasmissione non affidabile di informazioni tra due client |
Livelli Applicazione/Presentazione/Sessione
Qui si trovano diversi protocolli:
Emulatore di terminale che consente a una macchina A di connettersi a una 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 un nome di computer in un indirizzo Internet del computer | |
creato da Sun MicroSystems, specifica una rappresentazione standard dei dati, indipendente dal tipo di 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 | |
, anch’esso definito da Sun; questo protocollo consente a una macchina di “vedere” il sistema di file di un’altra macchina. Si basa sul precedente protocollo RPC |
8.1.4. Funzionamento dei protocolli Internet
Le applicazioni sviluppate nell’ambiente TCP/IP utilizzano generalmente diversi protocolli di tale ambiente. Un programma applicativo comunica con il livello più alto dei protocolli. Quest’ultimo trasmette le informazioni al livello sottostante e così via fino a raggiungere il supporto fisico. A quel punto, le informazioni vengono trasferite fisicamente al dispositivo destinatario, dove attraverseranno nuovamente gli stessi livelli, questa volta in senso inverso, fino a raggiungere l’applicazione destinataria delle informazioni inviate. Lo schema seguente illustra il percorso delle informazioni:

Facciamo un esempio: l’applicazione FTP, definita a livello del livello Application e che consente il trasferimento di file tra macchine.
- L’applicazione fornisce una sequenza di byte da trasmettere al livello transport.
- Il livello transport suddivide questa sequenza di byte in segments e TCP, aggiungendo all’inizio di ciascun segmento il numero che lo identifica. I segmenti vengono passati al livello di rete gestito dal protocollo IP.
- Il livello IP crea un pacchetto che incapsula il segmento TCP ricevuto. All’inizio di questo pacchetto, inserisce gli indirizzi Internet del computer di origine e di destinazione. Determina inoltre l’indirizzo fisico del computer di destinazione. Il tutto viene trasmesso al livello di collegamento dati e collegamento fisico, ovvero alla scheda di rete che collega il computer alla rete fisica.
- A quel punto, il pacchetto IP viene a sua volta incapsulato in una trama fisica e inviato al destinatario tramite il cavo.
- Sul computer destinatario, il livello di collegamento dati e collegamento fisico esegue l’operazione inversa: incapsula il pacchetto IP dalla trama fisica e lo passa al livello IP.
- Il livello IP verifica che il pacchetto sia corretto: calcola una somma, in base ai bit ricevuti (checksum), somma che deve ritrovare nell’intestazione del pacchetto. In caso contrario, il pacchetto viene scartato.
- Se il pacchetto viene dichiarato corretto, il livello IP decapsula il segmento TCP in esso contenuto e lo trasmette al livello superiore transport.
- Il livello transport, ovvero il livello TCP nel nostro esempio, esamina il numero del segmento per ripristinare il corretto ordine dei segmenti.
- Calcola inoltre un checksum per il segmento TCP. Se risulta corretto, il livello TCP invia una conferma di ricezione alla macchina di origine; in caso contrario, il segmento TCP viene rifiutato.
- Al livello TCP non resta che trasmettere la parte dati del segmento all’applicazione destinataria di tali dati nel livello superiore.
8.1.5. I problemi di indirizzamento in Internet
Un nodo di una rete può essere un computer, una stampante intelligente, un server di file, in pratica qualsiasi dispositivo in grado di comunicare tramite i protocolli TCP/IP. Ogni nodo ha un indirizzo fisico il cui formato dipende dal tipo di rete. Su una rete Ethernet, l’indirizzo fisico è codificato su 6 byte. Un indirizzo di una rete X25 è un numero di 14 cifre.
L’indirizzo Internet di un nodo è un indirizzo logico: è indipendente dall’hardware e dalla rete utilizzata. Si tratta di un indirizzo di 4 byte che identifica sia una rete locale sia un nodo di tale rete. L’indirizzo Internet è solitamente rappresentato sotto forma di 4 numeri, corrispondenti ai valori dei 4 byte, separati da un punto. Pertanto, l’indirizzo del computer Lagaffe della Facoltà di Scienze di Angers è 193.49.144.1 e quello del computer Liny è 193.49.144.9. Ne consegue 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 gli indirizzi IP sono indipendenti dalla rete, un computer di una rete A può comunicare con un computer di una rete B senza preoccuparsi del tipo di rete su cui si trova: è sufficiente che conosca il proprio indirizzo IP. Il protocollo IP di ciascuna rete si occupa di effettuare la conversione tra indirizzo IP e indirizzo fisico, in entrambe le direzioni.
Gli indirizzi IP devono essere tutti diversi. La loro distribuzione è affidata a organismi ufficiali. Infatti, questi organismi 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 registrato in un file specifico su ogni macchina collegata alla rete.
8.1.5.1. Le classi di indirizzi IP
Un indirizzo IP è una sequenza di 4 byte, spesso indicata come I1.I2.I3.I4, che contiene in realtà due indirizzi:
- l'indirizzo della rete
- l'indirizzo di un nodo di tale rete
A seconda della dimensione di questi due campi, gli indirizzi IP si dividono 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 un computer in tale rete
Più precisamente, la forma di un indirizzo di classe A IP è la seguente:

L'indirizzo di rete è a 7 bit e l'indirizzo del nodo a 24 bit. Si possono quindi avere 127 reti di classe A, ciascuna delle quali può contenere fino a 224 nodi.
Classe B
In questo caso, l’indirizzo IP: I1.I2.I3.I4 ha la forma R1.R2.N1.N2 dove
R1.R2 è l’indirizzo della rete
N1.N2 è l'indirizzo di un computer in questa rete
Più precisamente, la forma di un indirizzo di classe B IP è la seguente:

L'indirizzo di rete è composto da 2 byte (14 bit esatti), così come quello del nodo. Si possono quindi avere 2¹⁴ reti di classe B, ciascuna delle quali può contenere 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 della rete
N1 è l'indirizzo di un computer in questa rete
Più precisamente, la forma di un indirizzo di classe C IP è la seguente:

L'indirizzo di rete è composto da 3 byte (meno 3 bit) e l'indirizzo del nodo da 1 byte. Si possono quindi avere 221 reti di classe C contenenti fino a 256 nodi.
Poiché l’indirizzo del computer Lagaffe della Facoltà di Scienze di Angers è 193.49.144.1, si nota che l’occhetto di peso maggiore è pari a 193, ovvero 11000001 in binario. Ne consegue che la rete è di classe C.
Indirizzi riservati
. Alcuni indirizzi IP sono indirizzi di rete piuttosto che indirizzi di nodi all’interno della rete. Si tratta di quelli in cui l'indirizzo del nodo è impostato a 0. Pertanto, l'indirizzo 193.49.144.0 è l'indirizzo IP della rete della Facoltà di Scienze di Angers. Di conseguenza, nessun nodo di una rete può avere l'indirizzo zero.
. Quando in un indirizzo IP l'indirizzo del nodo è composto esclusivamente da 1, si tratta di un indirizzo di broadcast: tale indirizzo indica tutti i nodi della rete.
. In una rete di classe C, che teoricamente consente 2⁸ = 256 nodi, se si escludono i due indirizzi vietati, rimangono solo 254 indirizzi consentiti.
8.1.5.2. I protocolli di conversione Indirizzo Internet <--> Indirizzo fisico
Abbiamo visto che durante la trasmissione di informazioni da una macchina all’altra, queste, attraversando il livello IP, venivano incapsulate in pacchetti. Questi hanno la seguente forma:

Il pacchetto IP contiene quindi gli indirizzi Internet del computer di origine e di destinazione. Quando questo pacchetto viene trasmesso al livello incaricato di inviarlo sulla rete fisica, vengono aggiunte altre informazioni per formare la trama fisica che verrà infine inviata sulla rete. Ad esempio, il formato di una trama su una rete Ethernet è il seguente:

Nella trama finale sono presenti gli indirizzi fisici dei computer di origine e di destinazione. Come vengono ottenuti?
Il dispositivo mittente, conoscendo l’indirizzo IP del dispositivo con cui desidera comunicare, ottiene l’indirizzo fisico di quest’ultimo utilizzando un protocollo specifico chiamato ARP (Address Resolution Protocol).
- Invia un pacchetto di un tipo speciale denominato pacchetto ARP contenente l’indirizzo IP del computer di cui si sta cercando l’indirizzo fisico. Ha inoltre provveduto a inserirvi il proprio indirizzo IP e il proprio indirizzo fisico.
- Questo pacchetto viene inviato a tutti i nodi della rete.
- Questi ultimi riconoscono la natura speciale del pacchetto. Il nodo che riconosce il proprio indirizzo IP nel pacchetto risponde inviando al mittente del pacchetto il proprio indirizzo fisico. Come è possibile? Ha trovato nel pacchetto gli indirizzi IP e l’indirizzo fisico del mittente.
- Il mittente riceve quindi l’indirizzo fisico che stava cercando. Lo memorizza in modo da poterlo utilizzare in seguito, qualora debbano essere inviati altri pacchetti allo stesso destinatario.
L’indirizzo IP di una macchina è normalmente registrato in uno dei suoi file, che può quindi consultare per conoscerlo. Questo indirizzo può essere modificato: basta modificare il file. L’indirizzo fisico, invece, è registrato nella memoria della scheda di rete e non può essere modificato.
Quando un amministratore desidera riorganizzare la propria rete, può trovarsi nella necessità di modificare gli indirizzi IP di tutti i nodi e quindi di modificare i diversi file di configurazione dei vari nodi. Ciò può risultare noioso e fonte di errori se il numero di macchine è elevato. Un metodo consiste nel non assegnare alcun indirizzo IP alle macchine: si inserisce quindi un codice speciale nel file in cui la macchina dovrebbe trovare il proprio indirizzo IP. Scoprendo di non avere un indirizzo IP, la macchina lo richiede secondo un protocollo denominato RARP (Reverse Address Resolution Protocol). Invia quindi sulla rete un pacchetto speciale chiamato pacchetto RARP, analogo al precedente pacchetto ARP, nel quale inserisce il proprio indirizzo fisico. Questo pacchetto viene inviato a tutti i nodi, che riconoscono quindi un pacchetto RARP. Uno di essi, denominato server RARP, possiede un file che riporta la corrispondenza tra indirizzo fisico e indirizzo IP di tutti i nodi. Risponde quindi al mittente del pacchetto RARP, rinviandogli il suo indirizzo IP. Un amministratore che desideri riconfigurare la propria rete deve quindi semplicemente modificare il file di corrispondenze del server RARP. Quest’ultimo deve normalmente avere un indirizzo fisso IP che deve poter conoscere senza dover utilizzare lui stesso il protocollo RARP.
8.1.6. Il livello di rete denominato livello IP di Internet
Il protocollo IP (Internet Protocol) definisce la forma che i pacchetti devono assumere e il modo in cui devono essere gestiti durante la loro trasmissione o ricezione. Questo particolare tipo di pacchetto è chiamato datagramma IP. Lo abbiamo già presentato:

L’aspetto importante è che, oltre ai dati da trasmettere, il datagramma IP contiene gli indirizzi Internet del computer di origine e di destinazione. In questo modo il computer destinatario sa chi gli sta inviando un messaggio.
A differenza di una trama di rete, la cui lunghezza è determinata dalle caratteristiche fisiche della rete su cui transita, 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 veniva incapsulato in una trama fisica. Abbiamo fornito l’esempio della trama fisica di una rete Ethernet:

Le frame fisiche circolano da nodo a nodo verso la loro destinazione, che potrebbe non trovarsi sulla stessa rete fisica del dispositivo mittente. Il pacchetto IP può quindi essere incapsulato in successione in diverse trame fisiche a livello dei nodi che fungono da collegamento tra due reti di tipo diverso. È anche possibile che il pacchetto IP sia troppo grande per essere incapsulato in una trama fisica. Il software IP del nodo in cui si verifica questo problema scompone quindi il pacchetto IP in fragments secondo regole precise; ciascuno di essi viene poi inviato sulla rete fisica. Verranno ricomposti solo alla loro destinazione finale.
8.1.6.1. Il routing
L’instradamento è il metodo utilizzato per convogliare i pacchetti IP verso la loro destinazione. Esistono due metodi: l’instradamento diretto e l’instradamento indiretto.
Routing diretto
Il routing diretto indica l’inoltro di un pacchetto IP direttamente dal mittente al destinatario all’interno della stessa rete:
- Il computer mittente di un datagramma IP dispone dell’indirizzo IP del destinatario.
- Ottiene l'indirizzo fisico di quest'ultimo tramite il protocollo ARP o dalle proprie tabelle, se tale indirizzo è già stato acquisito.
- Invia il pacchetto sulla rete a tale indirizzo fisico.
Routing indiretto
Per instradamento indiretto si intende l'inoltro 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 dei computer di origine e di destinazione sono diverse. Il computer di origine ne prende atto. Invia quindi il pacchetto a un nodo speciale chiamato router (router), nodo che collega una rete locale alle altre reti e di cui trova l’indirizzo IP nelle proprie tabelle, indirizzo ottenuto inizialmente da un file, da una memoria permanente o tramite informazioni che circolano sulla rete.
Un router è collegato a due reti e possiede un indirizzo IP all’interno di queste due reti.

Nel nostro esempio sopra riportato:
- La rete n. 1 ha l’indirizzo Internet 193.49.144.0 e la rete n. 2 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 router ha il compito di convertire il pacchetto IP che riceve, contenuto in una trama fisica tipica della rete n. 1, in una trama fisica in grado di circolare sulla rete n. 2. Se l’indirizzo IP del destinatario del pacchetto si trova nella rete n. 2, il router gli invierà il pacchetto direttamente; in caso contrario, lo invierà a un altro router, collegando la rete n. 2 a una rete n. 3 e così via.
8.1.6.2. Messaggi di errore e di controllo
Sempre nel livello di rete, quindi allo stesso livello del protocollo IP, esiste il protocollo ICMP (Internet Control Message Protocol). Serve a inviare messaggi sul funzionamento interno della rete: nodi in avaria, congestione a livello di un router, ecc... I messaggi ICMP vengono incapsulati in pacchetti IP e inviati sulla rete. I livelli IP dei diversi nodi intraprendono le azioni appropriate in base ai messaggi ICMP che ricevono. In questo modo, un'applicazione non si accorge mai di 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 non affidabile di dati tra due punti, ovvero non garantisce il corretto instradamento di un pacchetto alla sua destinazione. L’applicazione, se lo desidera, può gestire autonomamente questa situazione, ad esempio attendendo, dopo l’invio di un messaggio, una conferma di ricezione prima di inviare quello successivo.
Finora, a livello di rete, abbiamo parlato di indirizzi IP delle macchine. Tuttavia, su una macchina possono coesistere contemporaneamente diversi processi, tutti in grado di comunicare tra loro. È quindi necessario indicare, al momento dell’invio di un messaggio, non solo l’indirizzo IP del computer destinatario, ma anche il “nome” del processo destinatario. 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 saranno incapsulati in pacchetti IP, quindi in frame fisici.
8.1.7.2. Il protocollo TCP: Transfer Control Protocol
Per garantire comunicazioni sicure, il protocollo UDP non è sufficiente: lo sviluppatore di applicazioni deve elaborare autonomamente un protocollo che gli consenta di verificare il corretto instradamento dei pacchetti.
Il protocollo TCP (Transfer Control Protocol) evita questi problemi. Le sue caratteristiche sono le seguenti:
- Il processo che intende trasmettere stabilisce innanzitutto una connessione con il processo destinatario delle informazioni che sta per inviare. Tale connessione avviene tra una porta del computer mittente e una porta del computer ricevente. Tra le due porte viene così creato un percorso virtuale che sarà riservato esclusivamente ai due processi che hanno stabilito la connessione.
- Tutti i pacchetti inviati dal processo sorgente 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.
- Le informazioni trasmesse hanno un carattere continuo. Il processo mittente invia le informazioni al proprio ritmo. Queste non vengono necessariamente inviate immediatamente: il protocollo TCP attende di averne una quantità sufficiente per inviarle. Vengono memorizzate in una struttura denominata segmento TCP. Una volta riempito, questo segmento verrà trasmesso al livello IP, dove verrà incapsulato in un pacchetto IP.
- Ogni segmento inviato dal protocollo TCP è numerato. Il protocollo TCP destinatario verifica di ricevere correttamente i segmenti in sequenza. Per ogni segmento ricevuto correttamente, invia una conferma di ricezione al mittente.
- Quando quest’ultimo lo riceve, lo comunica al processo mittente. Quest’ultimo può quindi sapere che un segmento è arrivato a destinazione, cosa che non era possibile con il protocollo UDP.
- Se, trascorso un certo tempo, il protocollo TCP che ha trasmesso un segmento non riceve una conferma di ricezione, ritrasmette il segmento in questione, garantendo così la qualità del servizio di inoltro delle informazioni.
- Il circuito virtuale stabilito tra i due processi che comunicano è full-duplex: ciò significa che le informazioni possono transitare in entrambe le direzioni. In questo modo il processo di destinazione può inviare conferme di ricezione anche mentre il processo di origine continua a inviare informazioni. Ciò consente, ad esempio, al protocollo sorgente TCP di inviare più segmenti senza attendere la conferma di ricezione. Se dopo un certo tempo si accorge di non aver ricevuto la conferma di ricezione di un determinato segmento n. n, riprenderà la trasmissione dei segmenti da quel punto.
8.1.8. Il livello Applicazioni
Al di sopra dei protocolli UDP e TCP, esistono vari protocolli standard:
TELNET
Questo protocollo consente a un utente di un computer A della rete di connettersi a un computer B (spesso denominato computer host). TELNET emula sul computer A un terminale cosiddetto universale. L'utente si comporta quindi come se disponesse di un terminale collegato al computer 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 quali, ad esempio, la creazione di directory. Si basa sul protocollo TCP.
TFTP: (Trivial File Transfer Control)
Questo protocollo è una variante di FTP. Si basa sul protocollo UDP ed è meno sofisticato rispetto a FTP.
DNS: (Domain Name System)
Quando un utente desidera scambiare file con un computer remoto, ad esempio tramite FTP, deve conoscere l’indirizzo Internet di tale computer. Ad esempio, per eseguire FTP sul computer Lagaffe dell’Università di Angers, bisognerebbe avviare FTP come segue: FTP 193.49.144.1
Ciò richiede la presenza di una directory che metta in corrispondenza macchina <--> indirizzo IP. Probabilmente in questa directory le macchine sarebbero designate con nomi simbolici quali:
macchina DPX2/320 dell’Università di Angers
macchina Sun dell'Università di Angers ISERPA
È evidente che sarebbe più pratico indicare una macchina con un nome piuttosto che con il suo indirizzo IP. Si pone quindi il problema dell’unicità del nome: ci sono milioni di macchine interconnesse. Si potrebbe immaginare che un organismo centralizzato attribuisca i nomi. Sarebbe senza dubbio piuttosto oneroso. Il controllo dei nomi è stato infatti distribuito in domini. Ogni dominio è gestito da un organismo generalmente molto snello che gode di piena libertà nella scelta dei nomi dei computer. Così, i computer in Francia appartengono al dominio «fr», gestito dall’Inria di Parigi. Per semplificare ulteriormente le cose, il controllo viene ulteriormente distribuito: all’interno del dominio «fr» vengono creati altri domini. Ad esempio, l’Università di Angers appartiene al dominio «univ-Angers». Il servizio che gestisce questo dominio ha piena libertà di denominare i computer della rete dell’Università di Angers. Per il momento questo dominio non è stato suddiviso. Ma in una grande università con molti computer in rete, potrebbe esserlo.
Il computer DPX2/320 dell’Università di Angers è stata denominata Lagaffe, mentre una PC e una 486DX50 sono state denominate 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 al di fuori del dominio univ-Angers, la macchina Lagaffe potrà essere indicata come
Infine, all’interno del dominio univ-Angers, potrà essere indicata semplicemente come
Un'applicazione può quindi fare riferimento a una macchina tramite il suo nome. In definitiva, è comunque necessario ottenere l'indirizzo Internet di tale macchina. Come avviene ciò? Supponiamo di voler comunicare da una macchina A con una macchina B.
- Se la macchina B appartiene allo stesso dominio della macchina A, probabilmente il suo indirizzo IP si troverà in un file della macchina A.
- Altrimenti, il computer A troverà in un altro file, o nello stesso di prima, un elenco di alcuni server dei nomi con i relativi indirizzi IP. Un server dei nomi ha il compito di stabilire la corrispondenza tra il nome di un computer e il suo indirizzo IP. Il computer A invierà una richiesta speciale al primo server dei nomi della sua lista, denominata richiesta DNS, che include quindi il nome del computer ricercato. Se il server interpellato possiede tale nome nei propri registri, invierà al computer A l’indirizzo IP corrispondente. In caso contrario, anche il server troverà nei propri file un elenco di server dei nomi che può interpellare. Lo farà quindi. In questo modo verrà interpellato un certo numero di server dei nomi, non in modo casuale ma in modo da ridurre al minimo le richieste. Se la macchina viene infine individuata, la risposta tornerà alla macchina A.
XDR: (Rappresentazione dei dati eXternal)
Creato da sun MicroSystems, questo protocollo specifica una rappresentazione standard dei dati, indipendente dalle macchine.
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
Sempre definito da Sun, questo protocollo consente a una macchina di «vedere» il sistema di file di un’altra macchina. Si basa sul precedente protocollo RPC.
8.1.9. Conclusione
In questa introduzione abbiamo presentato alcune linee generali dei protocolli Internet. Per approfondire questo argomento, si consiglia la lettura dell'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 presente su Internet è identificata da un indirizzo o da un nome univoco. Queste due entità sono gestite in Java dalla classe InetAddress, di cui riportiamo alcuni metodi:
restituisce i 4 byte dell'indirizzo IP dell'istanza InetAddress corrente | |
restituisce l'indirizzo IP dell'istanza corrente InetAddress | |
fornisce il nome Internet dell'istanza corrente InetAddress | |
fornisce l'identità (indirizzo IP) e il nome Internet dell'istanza corrente InetAddress | |
crea l'istanza InetAddress del computer indicato da Host. Genera un'eccezione se Host è sconosciuto. Host può essere il nome Internet di un computer 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. Identificare 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);
}// fine try
}// fine main
}// fine della classe
I risultati dell'esecuzione sono i seguenti:
Ogni macchina ha un indirizzo interno IP, ovvero 127.0.0.1. Quando un programma utilizza questo indirizzo di rete, fa riferimento alla macchina su cui è in esecuzione. Il vantaggio di questo indirizzo è che non richiede una scheda di rete. È quindi possibile testare programmi di rete senza essere connessi a una rete. Un altro modo per indicare il computer locale è utilizzare il nome «localhost».
8.2.2.2. Identificare un computer qualsiasi
import java.net.*;
public class getbyname{
public static void main (String arg[]){
String nomMachine;
// si recupera l'argomento
if(arg.length==0)
nomMachine="localhost";
else nomMachine=arg[0];
// si tenta di ottenere l'indirizzo della macchina
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);
}// fine try
}// fine main
}// fine classe
Con la chiamata java getbyname, si ottengono i seguenti risultati:
Con la chiamata java getbyname shiva.istia.univ-angers.fr, si ottiene:
Con la chiamata Java getbyname www.ibm.com, si ottiene:
8.3. Comunicazioni TCP-IP
8.3.1. Informazioni generali

Quando un'applicazione AppA su un computer A vuole comunicare con un'applicazione AppB su un computer B in Internet, deve conoscere diverse informazioni:
- l’indirizzo IP o il nome del computer B
- il numero della porta su cui opera l’applicazione AppB. Infatti, il computer B può ospitare numerose applicazioni che operano su Internet. Quando riceve informazioni provenienti dalla rete, deve sapere a quale applicazione sono destinate tali informazioni. Le applicazioni del computer B accedono alla rete tramite interfacce chiamate anche porte di comunicazione. Questa informazione è contenuta nel pacchetto ricevuto dal computer B affinché possa essere consegnato all’applicazione corretta.
- I protocolli di comunicazione supportati dal computer B. Nel nostro studio utilizzeremo esclusivamente i protocolli TCP-IP.
- il protocollo di dialogo accettato dall’applicazione AppB. Infatti, i computer A e B «comunicheranno» tra loro. Ciò che si scambieranno sarà incapsulato nei protocolli TCP-IP. Tuttavia, quando, alla fine della catena, l’applicazione AppB riceverà le informazioni inviate dall’applicazione AppA, dovrà essere in grado di interpretarle. Ciò è analogo alla situazione in cui due persone, A e B, comunicano al telefono: il loro dialogo viene trasportato dal telefono. La voce viene codificata sotto forma di segnali dal telefono A, trasportata attraverso le linee telefoniche, arriva al telefono B per essere decodificata. La persona B sente quindi le parole. È qui che entra in gioco il concetto di protocollo di dialogo: se A parla francese e B non capisce questa lingua, A e B non potranno dialogare in modo efficace.
Pertanto, le due applicazioni che comunicano tra loro devono concordare sul tipo di dialogo che adotteranno. 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 comunicazione diverso.
8.3.2. Le caratteristiche del protocollo TCP
In questa sede esamineremo solo le comunicazioni di rete che utilizzano il protocollo di trasporto TCP. Ricordiamo qui le caratteristiche di questo protocollo:
- Il processo che intende trasmettere stabilisce innanzitutto una connessione con il processo destinatario delle informazioni che sta per inviare. Questa connessione avviene tra una porta del computer mittente e una porta del computer ricevente. Tra le due porte viene così creato un percorso virtuale che sarà riservato esclusivamente ai due processi che hanno stabilito la connessione.
- Tutti i pacchetti inviati dal processo sorgente seguono questo percorso virtuale e arrivano nell’ordine in cui sono stati inviati
- Le informazioni trasmesse hanno un carattere continuo. Il processo mittente invia le informazioni al proprio ritmo. Queste non vengono necessariamente inviate immediatamente: il protocollo TCP attende di averne una quantità sufficiente per inviarle. Vengono memorizzate in una struttura denominata segmento TCP. Una volta riempito, questo segmento verrà trasmesso al livello IP, dove verrà incapsulato in un pacchetto IP.
- Ogni segmento inviato dal protocollo TCP è numerato. Il protocollo TCP destinatario verifica di ricevere i segmenti in sequenza. Per ogni segmento ricevuto correttamente, invia una conferma di ricezione al mittente.
- Quando quest’ultimo lo riceve, ne dà comunicazione al processo mittente. Quest’ultimo può quindi sapere che un segmento è giunto a destinazione.
- Se, trascorso un certo tempo, il protocollo TCP che ha trasmesso un segmento non riceve una conferma di ricezione, ritrasmette il segmento in questione, garantendo così la qualità del servizio di inoltro delle informazioni.
- Il circuito virtuale stabilito tra i due processi che comunicano è full-duplex: ciò significa che le informazioni possono transitare in entrambe le direzioni. In questo modo il processo di destinazione può inviare conferme di ricezione anche mentre il processo di origine continua a inviare informazioni. Ciò consente, ad esempio, al protocollo sorgente TCP di inviare più segmenti senza attendere la conferma di ricezione. Se dopo un certo tempo si accorge di non aver ricevuto la conferma di ricezione di un determinato segmento n. n, riprenderà la trasmissione dei segmenti da quel punto.
8.3.3. Il rapporto client-server
Spesso la comunicazione su Internet è asimmetrica: il computer A avvia una connessione per richiedere un servizio al computer B, specificando che desidera aprire una connessione con il servizio SB1 del computer B. Quest’ultimo accetta o rifiuta. Se accetta, la macchina A può inviare le proprie richieste al servizio SB1. Queste devono essere conformi al protocollo di dialogo compreso dal servizio SB1. Si instaura così un dialogo domanda-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 di un 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 di un 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 in modo diverso la richiesta di connessione iniziale di un client rispetto alle sue successive richieste volte a ottenere un servizio. Il programma non fornisce direttamente il servizio. Se lo facesse, durante la durata del servizio non sarebbe più in ascolto delle richieste di connessione e i client non verrebbero quindi serviti. Proceda quindi in altro modo: non appena una richiesta di connessione viene ricevuta sulla porta di ascolto e poi accettata, il server crea un’attività incaricata di fornire il servizio richiesto dal cliente. Tale servizio viene fornito su un’altra porta del server, denominata porta di servizio. In questo modo è possibile servire più clienti 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. Définition
Lo strumento di base utilizzato dai programmi che comunicano su Internet è il socket. Questo termine inglese significa "presa di corrente". In questo contesto, il suo significato è esteso a "presa di rete". Affinché un’applicazione possa inviare e ricevere informazioni sulla rete Internet, ha bisogno di una presa di rete, una socket. Questo strumento è stato inizialmente creato nelle versioni Unix dell’Università di Berkeley. Da allora è stato portato su tutti i sistemi Unix e nell’ambiente Windows. È presente anche sulle macchine virtuali Java in due forme: la classe Socket per le applicazioni client e la classe ServerSocket per le applicazioni server. Di seguito illustriamo alcuni dei costruttori e dei metodi della classe Socket:
apre una connessione remota con la porta port del computer host |
restituisce il numero della porta locale utilizzata dal socket | |||
restituisce il numero della porta remota a cui è connesso il socket | |||
restituisce l'indirizzo locale InetAddress a cui è associato il socket | |||
restituisce l'indirizzo remoto InetAddress a cui è collegato il socket | |||
crea un flusso di input che consente di leggere i dati inviati dal partner remoto | |||
restituisce un flusso di uscita che consente di inviare dati al partner remoto | |||
chiude il flusso di ingresso del socket | |||
chiude il flusso di uscita del socket | |||
chiude il socket e i relativi flussi di I/O | |||
restituisce una stringa di caratteri che "rappresenta" il socket | |||
8.3.6.2. Apertura di una connessione con un server
Abbiamo visto che affinché un computer A possa aprire una connessione con un servizio di un computer B, sono necessarie due informazioni:
- l'indirizzo IP o il nome del computer B
- il numero di porta su cui opera 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 connessione non è riuscita - si gestisce l'errore
….
}
Se la richiesta di connessione va a buon fine, al client viene assegnata localmente una porta per comunicare con la macchina B. Una volta stabilita la connessione, è possibile conoscere tale porta tramite il metodo:
Se la connessione va a buon fine, abbiamo visto che, dal canto suo, il server affida il servizio a un altro processo che opera su una cosiddetta porta di servizio. Il numero di questa porta può essere ottenuto tramite il metodo:
8.3.6.3. Invio di informazioni sulla rete
È possibile ottenere un flusso di scrittura sul socket e quindi sulla rete tramite il metodo:
Tutto ciò che verrà inviato in questo flusso verrà ricevuto sulla porta di servizio del server. Molte applicazioni presentano un dialogo sotto forma di righe di testo terminate da un'interruzione di riga. Pertanto, il metodo println risulta molto pratico in questi casi. Si trasforma quindi il flusso di uscita OutputStream in un flusso PrintWriter che utilizza il metodo println. La scrittura può generare un'eccezione.
8.3.6.4. Lettura delle informazioni provenienti dalla rete
È possibile ottenere un flusso di lettura delle informazioni in arrivo sul socket con il metodo:
Tutto ciò che verrà letto in questo flusso proviene dalla porta di servizio del server. Per le applicazioni con un dialogo sotto forma di righe di testo terminate da un'interruzione di riga, è preferibile utilizzare il metodo readLine. A tal fine, si trasforma il flusso di input InputStream in un flusso BufferedReader, che dispone del metodo readLine(). La lettura può generare un'eccezione.
8.3.6.5. Chiusura della connessione
Si effettua con il metodo:
Il metodo può generare un'eccezione. Le risorse utilizzate, in particolare la porta di rete, vengono liberate.
8.3.6.6. L'architettura del client
Ora disponiamo degli elementi necessari per descrivere l'architettura di base di un client Internet:
Socket sClient=null;
try{
// ci si connette al servizio in esecuzione sulla porta P del computer M
sClient=new Socket(M,P);
// si creano i flussi di ingresso-uscita del socket client
BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);
// ciclo richiesta-risposta
boolean fini=false;
String demande;
String réponse;
while (! fini){
// si prepara la richiesta
demande=…
// la si invia
out.println(demande);
// si legge la risposta
réponse=in.readLine();
// elaborazione della risposta
…
}
// è terminato
sClient.close();
} catch(Exception e){
// si gestisce l'eccezione
….
}
Per non complicare l'esempio, non abbiamo cercato di gestire i diversi tipi di eccezione generati dal costruttore Socket o dai metodi readline, getInputStream, getOutputStream e close. Tutto è stato raggruppato in un'unica eccezione.
8.3.7. La classe ServerSocket
8.3.7.1. Définition
Questa classe è destinata alla gestione dei socket lato server. Di seguito illustriamo alcuni dei costruttori e dei metodi di questa classe:
crea un socket di ascolto sulla porta port | |
idem, ma imposta in count la dimensione della coda e in c.a.d il numero massimo di connessioni client messe in attesa se il server è occupato al momento dell’arrivo della connessione client. |
restituisce il numero della porta di ascolto utilizzata dal socket | |
restituisce l'indirizzo locale InetAddress a cui è associato il socket | |
mette il server in attesa di una connessione (operazione bloccante). All'arrivo di una connessione client, restituisce un socket tramite il quale verrà fornito il servizio al client. | |
chiude il socket e i relativi flussi di I/O | |
restituisce una stringa di caratteri che "rappresenta" il socket | |
chiude il socket di servizio e libera le risorse ad esso associate |
8.3.7.2. Apertura del servizio
Si effettua con 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 respinte. Entrambe le istruzioni generano un'eccezione.
8.3.7.3. Accettazione di una richiesta di connessione
Quando un client invia una richiesta di connessione sulla porta di ascolto del servizio, quest’ultimo la accetta con il metodo:
Questo metodo restituisce un'istanza di Socket: si tratta del socket di servizio, attraverso il quale verrà fornito il servizio, il più delle volte da un'altra attività. Il metodo può generare un'eccezione.
8.3.7.4. Lettura/scrittura tramite il socket di servizio
Poiché il socket di servizio è un'istanza della classe Socket, si rimanda alle sezioni precedenti in cui è stato trattato questo argomento.
8.3.7.5. Identificare il cliente
Una volta ottenuto il socket di servizio, il cliente può essere identificato con il metodo
della classe Socket. Si avrà così accesso all'indirizzo IP e al nome del cliente.
8.3.7.6. Chiudere il servizio
Ciò avviene tramite il metodo
della classe ServerSocket. Ciò libera le risorse occupate, in particolare la porta di ascolto. Il metodo può generare un'eccezione.
8.3.7.7. Architettura di base di un server
Da quanto detto, è possibile descrivere la struttura di base di un server:
SocketServer sEcoute=null;
try{
// avvio del servizio
int portEcoute=…
int maxConnexions=…
sEcoute=new ServerSocket(portEcoute,maxConnexions);
// elaborazione delle richieste di connessione
boolean fini=false;
Socket sService=null;
while( ! fini){
// attesa e accettazione di una richiesta
sService=sEcoute.accept();
// il servizio viene fornito da un'altra attività a cui viene passato il socket di servizio
new Service(sService).start();
// si torna in attesa delle richieste di connessione
}
// È tutto: si chiude il servizio
sEcoute.close();
} catch (Exception e){
// si gestisce l'eccezione
…
}
La classe Service è un thread che potrebbe avere il seguente aspetto:
public class Service extends Thread{
Socket sService; // il socket di servizio
// costruttore
public Service(Socket S){
sService=S;
}
// esecuzione
public void run(){
try{
// si creano i flussi di ingresso-uscita
BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);
// ciclo richiesta-risposta
boolean fini=false;
String demande;
String réponse;
while (! fini){
// si legge la richiesta
demande=in.readLine();
// la si elabora
…
// si prepara la risposta
réponse=…
// si invia
out.println(réponse);
}
// operazione completata
sService.close();
} catch(Exception e){
// si gestisce l'eccezione
….
}// try
} // run
8.4. Applicazioni
8.4.1. Server echo
Ci proponiamo di scrivere un server echo che verrà avviato da una finestra DOS tramite il comando:
Il server opera sulla porta specificata come parametro. Si limita a rinviare al client la richiesta che quest’ultimo gli ha inviato, accompagnata dalla sua identità (IP+nome). Accetta 2 connessioni nella sua lista d’attesa. Abbiamo qui tutti gli elementi costitutivi di un server TCP. Il programma è il seguente:
// chiamata: serveurEcho porta
// server di eco
// restituisce al client la riga che questi gli ha inviato
import java.net.*;
import java.io.*;
public class serveurEcho{
public final static String syntaxe="Syntaxe : serveurEcho port";
public final static int nbConnexions=2;
// programma principale
public static void main (String arg[]){
// c'è un argomento
if(arg.length != 1)
erreur(syntaxe,1);
// questo argomento deve essere un numero intero >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);
// si crea il socket di ascolto
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);
}
// monitoraggio
System.out.println("Serveur d'écho lancé sur le port " + port);
// ciclo di servizio
boolean serviceFini=false;
Socket service=null;
while (! serviceFini){
// attesa di un client
try{
service=ecoute.accept();
} catch (IOException e){
erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
}
// si identifica la connessione
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);
}
// il servizio è gestito da un altro processo
new traiteClientEcho(service).start();
}// fine while
}// fine main
// visualizzazione degli errori
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
// identifica
private static String identifie(InetAddress Host){
// identificazione dell'host
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// fine classe
// fornisce il servizio a un client del server di eco
class traiteClientEcho extends Thread{
private Socket service; // socket di servizio
private BufferedReader in; // flusso in entrata
private PrintWriter out; // flusso di uscita
// costruttore
public traiteClientEcho(Socket service){
this.service=service;
}
// metodo run
public void run(){
// creazione dei flussi di ingresso e di uscita
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);
}// fine 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);
}// fine try
// l'identificativo della connessione viene inviato al client
try{
out.println("Client ["+identifie(service.getInetAddress())+","+
service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
+ "," + service.getLocalPort() + "]");
} catch (Exception e) {
erreur("identification liaison",1);
}
// ciclo di lettura richiesta/scrittura risposta
String demande,reponse;
try{
// il servizio si interrompe quando il client invia un indicatore di fine file
while ((demande=in.readLine())!=null){
// eco della richiesta
reponse="["+demande+"]";
out.println(reponse);
// il servizio si interrompe quando il client invia "fine"
if(demande.trim().toLowerCase().equals("fin")) break;
}// fine ciclo while
} catch (IOException e){
erreur("Erreur lors des échanges client/serveur ("+e+")",3);
}// fine try
// si chiude il socket
try{
service.close();
} catch (IOException e){
erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
}// fine try
}// fine run
// visualizzazione degli errori
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}// fine errore
// identificazione
private String identifie(InetAddress Host){
// identificazione host
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// fine classe
Le due classi necessarie al servizio sono state riunite in un unico file sorgente. Solo una di esse, quella che contiene la funzione main, possiede l'attributo public. La struttura del server è conforme all'architettura generale dei server TCP. È stato aggiunto un metodo (identifie) che consente di identificare la connessione tra il server e un client. Ecco alcuni risultati:
Il server viene avviato con il comando
Nella finestra di controllo viene quindi visualizzato il seguente messaggio:
Per testare questo server, si utilizza 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 nelle loro comunicazioni. È il caso del nostro server echo. Si avvia 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'échappement est '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'opération
open ouvre une connexion à un site
quit quitte telnet
set définit les options (entrez 'set ?' pour afficher la liste)
status affiche les informations d'état
unset annule les options (entrez 'unset ?' pour afficher la liste)
? ou help affiche des informations d'aide
Microsoft Telnet> set ?
NTLM Active l'authentification NTLM.
LOCAL_ECHO Active l'écho local.
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
Il programma telnet, per impostazione predefinita, non visualizza l'eco dei comandi digitati sulla tastiera. Per ottenere tale eco, si immette il comando:
Per aprire una connessione con il server, specificando la porta del servizio di eco (187) e l'indirizzo del computer su cui si trova (localhost), si immette il comando:
Nella finestra DOS del client viene quindi visualizzato il messaggio:
Nella finestra del server compare il 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 indicano la stessa macchina. Nella finestra del client telnet è possibile digitare righe di testo. Il server le riporta in eco:
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 del client (1059) viene rilevata correttamente, ma che la porta del servizio (187) è identica alla porta di ascolto (187), il che è inaspettato. Ci si sarebbe infatti potuti aspettare di ottenere la porta del socket di servizio e non quella di ascolto. Bisognerebbe verificare se si ottengono 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 delle righe di testo:
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]
Si può quindi notare che il server echo può servire più client contemporaneamente. I client telnet possono essere terminati 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 scriviamo il nostro client:
// chiamata: clientEcho porta macchina
// client del server di eco
// invia righe al server che le restituisce tramite eco
import java.net.*;
import java.io.*;
public class clientEcho{
public final static String syntaxe="Syntaxe : clientEcho machine port";
// programma principale
public static void main (String arg[]){
// ci sono due argomenti
if(arg.length != 2)
erreur(syntaxe,1);
// il primo argomento deve essere il nome di una macchina esistente
String machine=arg[0];
InetAddress serveurAddress=null;
try{
serveurAddress=InetAddress.getByName(machine);
} catch (Exception e){
erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
}
// la porta deve essere un numero intero maggiore di 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);
// ci si connette al 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);
}
// si identifica la connessione
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);
}
// creazione del flusso di lettura delle righe digitate sulla tastiera
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);
}
// creazione del flusso di ingresso associato al socket client
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);
}
// creazione del flusso di uscita associato al socket client
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);
}
// ciclo richieste-risposte
boolean serviceFini=false;
String demande=null;
String reponse=null;
// si legge il messaggio inviato dal server subito dopo la connessione
try{
reponse=in.readLine();
} catch (IOException e){
erreur("Lecture réponse ("+e+")",4);
}
// visualizzazione della risposta
System.out.println("Serveur : " +reponse);
while (! serviceFini){
// lettura di una riga digitata sulla tastiera
System.out.print("Client : ");
try{
demande=IN.readLine();
} catch (Exception e){
erreur("Lecture ligne ("+e+")",9);
}
// invio di una richiesta sulla rete
try{
out.println(demande);
} catch (Exception e){
erreur("Envoi demande ("+e+")",10);
}
// attesa/lettura della risposta
try{
reponse=in.readLine();
} catch (IOException e){
erreur("Lecture réponse ("+e+")",4);
}
// visualizzazione della risposta
System.out.println("Serveur : " +reponse);
// È finito?
if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
}
// è finito
try{
sClient.close();
} catch(Exception e){
erreur("Fermeture socket ("+e+")",11);
}
}// main
// visualizzazione degli errori
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
// identifica
private static String identifie(InetAddress Host){
// identificazione dell'host
String ipHost=Host.getHostAddress();
String nomHost=Host.getHostName();
String idHost;
if (nomHost == null) idHost=ipHost;
else idHost=ipHost+","+nomHost;
return idHost;
}
}// fine classe
La struttura di questo client è conforme all'architettura generale dei client tcp. In questo caso, le diverse eccezioni possibili sono state gestite una per una, 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 Serveur sono quelle che il server ha restituito.
8.4.3. Un client generico TCP
Molti servizi creati agli albori di Internet funzionano secondo il modello del server di eco esaminato in precedenza: gli scambi client-server avvengono tramite lo scambio di righe di testo. Scriveremo un client TCP generico che verrà avviato nel modo seguente: java cltTCPgenerique server porta
Questo client TCP si connetterà alla porta port del server serveur. Una volta fatto ciò, creerà due thread:
- un thread incaricato di leggere i comandi digitati sulla tastiera e di inviarli al server
- un thread incaricato di leggere le risposte del server e di visualizzarle sullo schermo
Perché due thread, mentre nell’applicazione precedente non se ne era sentita la necessità? In quest’ultima, il protocollo di dialogo era noto: il client inviava una sola riga e il server rispondeva con una sola riga. Ogni servizio ha il proprio protocollo specifico e si possono verificare anche le seguenti situazioni:
- il client deve inviare più righe di testo prima di ricevere una risposta
- la risposta di un server può comprendere più righe di testo
Pertanto, il ciclo «invio di una singola riga al server - ricezione di una singola riga inviata dal server» non è sempre adeguato. Creeremo quindi due cicli distinti:
- un ciclo di lettura dei comandi digitati sulla tastiera da inviare al server. L’utente segnalerà la fine dei comandi con la parola chiave fin.
- un ciclo di ricezione e visualizzazione delle risposte del server. Si tratterà di un ciclo infinito che verrà interrotto solo dalla chiusura del flusso di rete da parte del server o dall’utente tramite la tastiera, digitando il comando fin.
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 (SendMail Transfer Protocol). Questo servizio è responsabile dell’inoltro della posta elettronica ai destinatari. Funziona sulla porta 25 e utilizza un protocollo di comunicazione basato sullo scambio di righe di 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 indicazioni sui comandi utilizzabili con il servizio. In questo caso non è così. I comandi SMTP utilizzati nell'esempio sono i seguenti:
- mail from: expéditeur, per indicare l'indirizzo e-mail del mittente del messaggio
- rcpt to: destinataire, per indicare l’indirizzo e-mail del destinatario del messaggio. Se ci sono più destinatari, il comando rcpt to: va ripetuto tante volte quante necessarie per ciascuno di essi.
- data, che segnala al server SMTP che si sta per inviare il messaggio. Come indicato nella risposta del server, il messaggio è costituito da una sequenza di righe terminata da una riga contenente il solo carattere punto. Un messaggio può avere delle intestazioni separate dal corpo del messaggio da una riga vuota. Nel nostro esempio, abbiamo inserito un oggetto con la parola chiave Subject:
- una volta inviato il messaggio, è possibile indicare al server che si è terminato con 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 «fin» sulla tastiera per arrestare anche il thread di lettura dei comandi digitati sulla tastiera.
Se controlliamo la posta ricevuta, troviamo 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 from di un messaggio. In questo caso, il mittente machin@univ-angers.fr non esisteva.
Questo client TCP generico ci permette di individuare il protocollo di comunicazione dei servizi Internet e, partendo da lì, di creare classi specializzate per i client di tali servizi. Scopriamo il protocollo di comunicazione del servizio POP (Post Office Protocol), che consente di recuperare le proprie e-mail archiviate su un server. Funziona 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 nome utente sul server che ospita le nostre e-mail
- password password, dove si inserisce la password associata al login precedente
- list, per ottenere l'elenco dei messaggi sotto forma di numero e dimensione in byte
- retr i, per leggere il messaggio n. i
- quit, per terminare la sessione.
Scopriamo ora il protocollo di dialogo tra un client e un server Web che solitamente opera 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 lo schema seguente:
Il server Web risponde solo dopo aver ricevuto la riga vuota. Nell'esempio abbiamo utilizzato un solo comando:
che richiede al server il file URL /index.html e indica che sta operando con il protocollo HTTP versione 1.0. 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, poiché si vede che il thread di lettura delle risposte si è concluso. Prima di inviare il contenuto del file index.html, il server web ha inviato una serie di intestazioni terminata 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. Quanto sopra è denominato intestazioni HTTP (HyperText Transfer Protocol). Non entreremo qui nei dettagli di queste intestazioni, ma ricordiamo che il nostro client generico ne consente l’accesso, 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 effettivamente trovato il file richiesto (200 OK), dove 200 è un codice di risposta HTTP. Le righe
indicano al client che riceverà 11251 byte corrispondenti al testo HTML (HyperText Markup Language) e che, al termine dell’invio, la connessione verrà chiusa.
Abbiamo quindi un client TCP molto pratico. Senza dubbio fa meno del programma telnet che abbiamo utilizzato in precedenza, ma è stato interessante scriverlo da soli. Il programma del client TCP generico è il seguente:
// pacchetti importati
import java.io.*;
import java.net.*;
public class clientTCPgenerique{
// riceve come parametro le caratteristiche di un servizio nella forma
// server porta
// si connette al servizio
// crea un thread per leggere i comandi digitati sulla tastiera
// questi verranno inviati al server
// crea un thread per leggere le risposte del server
// queste verranno visualizzate sullo schermo
// il tutto termina con il comando «fine» digitato sulla tastiera
// variabile di istanza
private static Socket client;
public static void main(String[] args){
// sintassi
final String syntaxe="pg serveur port";
// numero di argomenti
if(args.length != 2)
erreur(syntaxe,1);
// si annota il nome del server
String serveur=args[0];
// la porta deve essere un numero intero >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;
// potrebbero verificarsi dei problemi
try{
// si effettua la connessione al servizio
client=new Socket(serveur,port);
}catch(Exception ex){
// errore
erreur("Impossible de se connecter au service ("+ serveur
+","+port+"), erreur : "+ex.getMessage(),3);
// fine
return;
}//catch
// si creano i thread di lettura/scrittura
new ClientSend(client).start();
new ClientReceive(client).start();
// fine thread principale
return;
}// main
// visualizzazione degli errori
public static void erreur(String msg, int exitCode){
// visualizzazione dell'errore
System.err.println(msg);
// arresto con errore
System.exit(exitCode);
}//errore
}//classe
class ClientSend extends Thread {
// classe incaricata di leggere i comandi digitati sulla tastiera
// e di inviarli a un server tramite un client TCP passato come parametro
private Socket client; // il client TCP
// costruttore
public ClientSend(Socket client){
// si specifica il client TCP
this.client=client;
}//costruttore
// metodo Run del thread
public void run(){
// dati locali
PrintWriter OUT=null; // flusso di scrittura di rete
BufferedReader IN=null; // flusso da tastiera
String commande=null; // comando letto dalla tastiera
// gestione degli errori
try{
// creazione del flusso di scrittura di rete
OUT=new PrintWriter(client.getOutputStream(),true);
// creazione del flusso di input da tastiera
IN=new BufferedReader(new InputStreamReader(System.in));
// ciclo di immissione e invio dei comandi
System.out.println("Commandes : ");
while(true){
// lettura del comando digitato sulla tastiera
commande=IN.readLine().trim();
// finito?
if (commande.toLowerCase().equals("fin")) break;
// invio del comando al server
OUT.println(commande);
// comando successivo
}//while
}catch(Exception ex){
// errore
System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// fine - si chiudono i flussi
try{
OUT.close();client.close();
}catch(Exception ex){}
// si segnala la fine del thread
System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
}//run
}//classe
class ClientReceive extends Thread{
// classe incaricata di leggere le righe di testo destinate a un
// client TCP passato come parametro
private Socket client; // il client TCP
// costruttore
public ClientReceive(Socket client){
// si registra il client TCP
this.client=client;
}//costruttore
// metodo Run del thread
public void run(){
// dati locali
BufferedReader IN=null; // flusso di lettura di rete
String réponse=null; // risposta del server
// gestione degli errori
try{
// creazione del flusso di lettura di rete
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
// ciclo di lettura delle righe di testo del flusso IN
while(true){
// lettura flusso di rete
réponse=IN.readLine();
// flusso chiuso?
if(réponse==null) break;
// visualizzazione
System.out.println("<-- "+réponse);
}//while
}catch(Exception ex){
// errore
System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// fine - si chiudono i flussi
try{
IN.close();client.close();
}catch(Exception ex){}
// si segnala la fine del thread
System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
}//run
}//classe
8.4.4. Un server TCP generico
Ora ci occupiamo di 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 quest’ultimo a fungere da server.
Il programma viene avviato con: java serveurTCPgenerique portEcoute, dove portEcoute è la porta a cui i client devono connettersi. Il servizio al 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 sulla tastiera. Quest’ultimo segnalerà, tramite il comando «fin», la chiusura della 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, dal canto suo, non si arresta mai, a meno che l’utente non prema Ctrl-C sulla tastiera. Vediamo alcuni esempi.
Il server viene avviato sulla porta 100 e si utilizza il client generico per comunicare con esso. La finestra del client è la seguente:
E:\data\serge\MSNET\c#\rete\client tcp generico> 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 sono quelle inviate 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 client al server. Le righe N: sono quelle inviate dal server al client n. N. Il server sopra indicato è ancora attivo, mentre il client 1 ha terminato l'operazione. Si avvia un secondo client 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
Simuliamo ora un server web avviando il nostro server generico sulla porta 88:
Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Apriamo ora un browser e richiediamo l'http://localhost:88/exemple.html URL. Il browser si connetterà quindi alla porta 88 del computer localhost e richiederà la pagina /exemple.html:

Diamo ora 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
<--
Si scoprono così le intestazioni HTTP inviate dal browser. Questo ci permette di scoprire poco a poco il protocollo HTTP. In un esempio precedente, avevamo creato un client Web che inviava solo il comando GET. Era stato sufficiente. Qui vediamo che il browser invia altre informazioni al server. Il loro scopo è indicare al server quale tipo di client ha di fronte. Notiamo inoltre che le intestazioni HTTP terminano con una riga vuota.
Elaboriamo una risposta per il nostro client. L’utente alla tastiera è in questo caso il vero server e può elaborare una risposta manualmente. Ricordiamo la risposta fornita 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 dare una risposta analoga:
...
<-- 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 fin chiude la connessione tra il server e il 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 per inviare (Content-Length), ma ci limitiamo a indicare che chiuderemo la connessione (Connection: close) dopo averlo inviato. Questo è sufficiente per il browser. Quando rileva che la connessione è stata chiusa, il browser capirà che la risposta del server è terminata e visualizzerà la pagina HTML che gli è stata inviata. Quest’ultima è 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 è terminata e può quindi visualizzarla:

Se, come sopra, si esegue il comando View/Source per vedere cosa ha ricevuto il browser, si ottiene:

ovvero esattamente ciò che è stato inviato dal server generico.
Il codice del server TCP generico è il seguente:
// pacchetti
import java.io.*;
import java.net.*;
public class serveurTCPgenerique{
// programma principale
public static void main (String[] args){
// riceve le richieste dei client sulla porta di ascolto
// crea un thread per leggere le richieste del client
// queste verranno visualizzate sullo schermo
// crea un thread per leggere i comandi digitati sulla tastiera
// questi dati verranno inviati al cliente come risposta
// il tutto termina con il comando "fin" digitato sulla tastiera
final String syntaxe="Syntaxe : pg port";
// variabile di istanza
// c'è un argomento
if(args.length != 1)
erreur(syntaxe,1);
// la porta deve essere un numero intero >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);
// si crea il servizio di ascolto
ServerSocket ecoute=null;
int nbClients=0; // numero di client gestiti
try{
// si crea il servizio
ecoute=new ServerSocket(port);
// monitoraggio
System.out.println("Serveur générique lancé sur le port " + port);
// ciclo di servizio ai clienti
Socket client=null;
while (true){ // ciclo infinito - verrà interrotto con Ctrl-C
// in attesa di un cliente
client=ecoute.accept();
// il servizio è gestito da thread separati
nbClients++;
// vengono creati i thread di lettura/scrittura
new ServeurSend(client,nbClients).start();
new ServeurReceive(client,nbClients).start();
// si torna ad ascoltare le richieste
}// fine ciclo while
}catch(Exception ex){
// si segnala l'errore
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
}//catch
}// fine main
// visualizzazione degli errori
public static void erreur(String msg, int exitCode){
// visualizzazione dell'errore
System.err.println(msg);
// arresto con errore
System.exit(exitCode);
}//errore
}//classe
class ServeurSend extends Thread{
// classe incaricata di leggere le risposte digitate sulla tastiera
// e di inviarle a un client tramite un client TCP passato al costruttore
Socket client; // il client TCP
int numClient; // numero del client
// costruttore
public ServeurSend(Socket client, int numClient){
// si annota il client TCP
this.client=client;
// e il relativo n.
this.numClient=numClient;
}//produttore
// metodo Run del thread
public void run(){
// dati locali
PrintWriter OUT=null; // flusso di scrittura di rete
String réponse=null; // risposta letta dalla tastiera
BufferedReader IN=null; // flusso da tastiera
// monitoraggio
System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
// gestione degli errori
try{
// creazione del flusso di scrittura di rete
OUT=new PrintWriter(client.getOutputStream(),true);
// creazione del flusso da tastiera
IN=new BufferedReader(new InputStreamReader(System.in));
// ciclo di immissione e invio dei comandi
while(true){
// identificazione del cliente
System.out.print("--> " + numClient + " : ");
// Lettura della risposta digitata sulla tastiera
réponse=IN.readLine().trim();
// finito?
if (réponse.toLowerCase().equals("fin")) break;
// invio della risposta al server
OUT.println(réponse);
// risposta successiva
}//while
}catch(Exception ex){
// errore
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// fine - si chiudono i flussi
try{
OUT.close();client.close();
}catch(Exception ex){}
// si segnala la fine del thread
System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
}//run
}//classe
class ServeurReceive extends Thread{
// classe incaricata di leggere le righe di testo inviate al server
// tramite un client TCP passato al costruttore
Socket client; // il client TCP
int numClient; // numero del client
// costruttore
public ServeurReceive(Socket client, int numClient){
// si registra il client TCP
this.client=client;
// e il suo n.
this.numClient=numClient;
}//produttore
// metodo Run del thread
public void run(){
// dati locali
BufferedReader IN=null; // flusso di lettura di rete
String réponse=null; // risposta del server
// monitoraggio
System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
// gestione degli errori
try{
// creazione del flusso di lettura di rete
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
// ciclo di lettura delle righe di testo del flusso IN
while(true){
// lettura del flusso di rete
réponse=IN.readLine();
// flusso chiuso?
if(réponse==null) break;
// visualizzazione
System.out.println("<-- "+réponse);
}//while
}catch(Exception ex){
// errore
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
}//catch
// fine - si chiudono i flussi
try{
IN.close();client.close();
}catch(Exception ex){}
// si segnala la fine del thread
System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
}//run
}//classe
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 a cui verrà passato come parametro un URL e che visualizzerà sullo schermo il contenuto di questo URL. Supporremo che il server Web contattato per l'URL supporti il protocollo HTTP 1.1. Tra le intestazioni precedenti, utilizzeremo solo le seguenti:
- la prima intestazione indica quale pagina desideriamo
- il secondo, quale server stiamo interrogando
- il terzo che desideriamo 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à richiamato nel modo seguente: java clientweb URL cmd, dove URL è l’URL desiderata e cmd una delle due parole chiave GET o HEAD per indicare se si desiderano 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
significa che la pagina richiesta è stata spostata (quindi da URL). Il nuovo URL è indicato dall'intestazione Location:
Se utilizziamo GET al posto di HEAD nella chiamata al 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, con in più il corpo della pagina HTML. Il programma è il seguente:
// pacchetti importati
import java.io.*;
import java.net.*;
public class clientweb{
// richiede un URL
// visualizza il contenuto di quest'ultima sullo schermo
public static void main(String[] args){
// sintassi
final String syntaxe="pg URI GET/HEAD";
// numero di argomenti
if(args.length != 2)
erreur(syntaxe,1);
// si rileva il codice URI richiesto
String URLString=args[0];
String commande=args[1].toUpperCase();
// verifica della validità di URI
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI non corretto
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//intercettazione
// verifica dell'ordine
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// ordine non corretto
erreur("Le second paramètre doit être GET ou HEAD",3);
}
// si estraggono le informazioni utili da 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();
// si può procedere
Socket client=null; // il cliente
BufferedReader IN=null; // il flusso di lettura del cliente
PrintWriter OUT=null; // il flusso di scrittura del client
String réponse=null; // risposta del server
try{
// ci si connette al server
client=new Socket(host,port);
// si creano i flussi di ingresso-uscita del client TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// richiesta di URL - invio delle intestazioni HTTP
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// si legge la risposta
while((réponse=IN.readLine())!=null){
// si elabora la risposta
System.out.println(réponse);
}//while
// è terminato
client.close();
} catch(Exception e){
// si gestisce l'eccezione
erreur(e.getMessage(),4);
}//catch
}//main
// visualizzazione degli errori
public static void erreur(String msg, int exitCode){
// visualizzazione dell'errore
System.err.println(msg);
// interruzione con errore
System.exit(exitCode);
}//errore
}//classe
L'unica novità in questo programma è l'utilizzo della classe URL. Il programma riceve un URL (Uniform Resource Locator) o un URI (Uniform Resource Identifier) della forma http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... La classe URL ci permette di scomporre la stringa URL nei suoi diversi elementi. Un oggetto URL viene costruito a partire dalla stringa URLstring ricevuta come parametro:
// verifica validità di URL
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI non corretto
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//catch
Se la stringa URL ricevuta come parametro non è un URL valido (mancanza del protocollo, del server, ecc.), viene generata un'eccezione. Ciò ci consente di verificare la validità del parametro ricevuto. Una volta creato l’oggetto URL, è possibile accedere ai suoi vari elementi. Pertanto, se l’oggetto url del codice precedente è stato creato a partire dalla stringa
si avrà:
url.getHost()=serveur
url.getPort()=port oppure -1 se la porta non è specificata
url.getPath()=cheminPageHTML oppure la stringa vuota se non è presente alcun percorso
url.getQuery()=param1=val1;param2=val2;... oppure null se non c'è una richiesta
uri.getProtocol()=http
8.4.6. Client Web che gestisce i reindirizzamenti
Il client Web precedente non gestisce un eventuale reindirizzamento dell'URL che ha richiesto. Il client successivo lo gestisce.
- Legge la prima riga delle intestazioni HTTP inviate dal server per verificare se vi sia 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 è presente 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:
// pacchetti importati
import java.io.*;
import java.net.*;
import java.util.regex.*;
public class clientweb2{
// richiede un URL
// visualizza il contenuto di quest'ultimo sullo schermo
public static void main(String[] args){
// sintassi
final String syntaxe="pg URL GET/HEAD";
// numero di argomenti
if(args.length != 2)
erreur(syntaxe,1);
// si prende nota dell'URI richiesto
String URLString=args[0];
String commande=args[1].toUpperCase();
// verifica della validità di URI
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI non corretto
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//intercettazione
// verifica dell'ordine
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// ordine non corretto
erreur("Le second paramètre doit être GET ou HEAD",3);
}
// si può procedere
Socket client=null; // il cliente
BufferedReader IN=null; // il flusso di lettura del cliente
PrintWriter OUT=null; // flusso di scrittura del client
String réponse=null; // risposta del server
final int nbRedirsMax=1; // non è consentito più di un reindirizzamento
int nbRedirs=0; // numero di reindirizzamenti in corso
String premièreLigne; // prima riga della risposta
boolean redir=false; // indica se c'è o meno un reindirizzamento
String locationString=""; // la stringa URL di un eventuale reindirizzamento
// espressione regolare per individuare una stringa di reindirizzamento URL
Pattern location=Pattern.compile("^Location: (.+?)$");
// gestione degli errori
try{
// è possibile avere più URL da interrogare in caso di reindirizzamenti
while(nbRedirs<=nbRedirsMax){
// si estraggono le informazioni utili dal 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();
// ci si connette al server
client=new Socket(host,port);
// si creano i flussi di ingresso-uscita del client TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// si richiede l'URL - invio delle intestazioni HTTP
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// si legge la prima riga della risposta
premièreLigne=IN.readLine();
// echo sullo schermo
System.out.println(premièreLigne);
// reindirizzamento?
if(premièreLigne.endsWith("302 Object moved")){
// c'è un reindirizzamento
redir=true;
nbRedirs++;
}//if
// le intestazioni HTTP successive fino a trovare la riga vuota che segnala la fine delle intestazioni
boolean locationFound=false;
while(!(réponse=IN.readLine()).equals("")){
// si visualizza la risposta
System.out.println(réponse);
// se c'è un reindirizzamento, si cerca l'intestazione Location
if(redir && ! locationFound){
// si confronta la riga con l'espressione relazionale location
Matcher résultat=location.matcher(réponse);
if(résultat.find()){
// se la ricerca ha esito positivo, si annota il codice di reindirizzamento URL
locationString=résultat.group(1);
// si registra che è stato trovato
locationFound=true;
}//if
}//se
// intestazione seguente
}//mentre
// righe successive della risposta
System.out.println(réponse);
while((réponse=IN.readLine())!=null){
// viene visualizzata la risposta
System.out.println(réponse);
}//while
// si chiude la connessione
client.close();
// si è terminato?
if ( ! locationFound || nbRedirs>nbRedirsMax)
break;
// è necessario effettuare un reindirizzamento - si crea la nuova URL
URLString=protocol +"://"+host+":"+port+locationString;
url=new URL(URLString);
// tracciamento
System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
}//mentre
} catch(Exception e){
// si gestisce l'eccezione
erreur(e.getMessage(),4);
}//catch
}//main
// visualizzazione degli errori
public static void erreur(String msg, int exitCode){
// visualizzazione dell'errore
System.err.println(msg);
// interruzione con errore
System.exit(exitCode);
}//errore
}//classe
8.4.7. Server per il calcolo delle imposte
Riprendiamo l'esercizio IMPOTS già trattato in varie forme. Ricordiamo l'ultima versione:
È stata creata una classe base per le imposte. I suoi attributi sono tre tabelle di numeri:
public class impots{
// i dati necessari per il calcolo dell'imposta
// provengono da una fonte esterna
protected double[] limites=null;
protected double[] coeffR=null;
protected double[] coeffN=null;
// campo «costruttore» vuoto
protected impots(){}
// costruttore
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
La classe impots ha due costruttori:
- un costruttore a cui vengono passati i tre array di dati necessari per il calcolo dell'imposta
- un costruttore senza parametri utilizzabile solo dalle classi figlie
Da questa classe è stata derivata la classe impotsJDBC, che consente di compilare i tre array limites, coeffR e coeffN a partire dal contenuto di un database:
public class impotsJDBC extends impots{
// aggiunta di un costruttore che consente di creare
// le tabelle «limites», «coeffr» e «coeffn» a partire dalla tabella
// imposte di un database
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
// dsnIMPOTS: nome DSN del database
// userIMPOTS, mdpIMPOTS: login/password di accesso al database
Era stata sviluppata un'applicazione grafica. L'applicazione utilizzava un oggetto della classe impotsJDBC. L'applicazione e tale oggetto si trovavano sulla stessa macchina. Intendiamo collocare il programma di test e l'oggetto impotsJDBC su macchine diverse. Si otterrà un'applicazione client-server in cui l'oggetto remoto impotsJDBC fungerà da server. La nuova classe si chiama ServeurImpots ed è derivata dalla classe impotsJDBC:
// pacchetti importati
import java.net.*;
import java.io.*;
import java.sql.*;
public class ServeurImpots extends impotsJDBC {
// attributi
int portEcoute; // porta di ascolto delle richieste dei clienti
boolean actif; // stato del server
// costruttore
public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
throws IOException, SQLException, ClassNotFoundException {
// costruzione padre
super(DSNimpots, USERimpots, MDPimpots);
// si nota la porta di ascolto
this.portEcoute=portEcoute;
// al momento inattivo
actif=false;
// crea e avvia un thread per la lettura dei comandi digitati sulla tastiera
// il server verrà gestito tramite questi comandi
Thread admin=new Thread(){
public void run(){
try{
admin();
}catch (Exception ignored){}
}
};
admin.start();
}//ServeurImpots
L'unico nuovo parametro nel costruttore è la porta di ascolto delle richieste dei client. Gli altri parametri vengono passati direttamente alla classe base impotsJDBC. Il server delle imposte è controllato da comandi digitati sulla tastiera. Si crea quindi un thread per leggere tali comandi. Ce ne saranno due possibili: start per avviare il servizio, stop per arrestarlo definitivamente. Il metodo admin che gestisce questi comandi è il seguente:
public void admin() throws IOException{
// legge i comandi di amministrazione del server digitati dalla tastiera
// in un ciclo infinito
String commande=null;
BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
while(true){
// richiede
System.out.print("Serveur d'impôts>");
// lettura comando
commande=IN.readLine().trim().toLowerCase();
// esecuzione comando
if(commande.equals("start")){
// attivo?
if(actif){
//errore
System.out.println("Le serveur est déjà actif");
// si prosegue
continue;
}//if
// si crea e si avvia il servizio di ascolto
Thread ecoute=new Thread(){
public void run(){
ecoute();
}
};
ecoute.start();
}//if
else if(commande.equals("stop")){
// fine di tutti i thread di esecuzione
System.exit(0);
}//se
else {
// errore
System.out.println("Commande incorrecte. Utilisez (start,stop)");
}//if
}//while
}//admin
Se il comando digitato sulla tastiera è start, viene avviato un thread di ascolto delle richieste dei clienti. Se il comando digitato è stop, tutti i thread vengono arrestati. Il thread di ascolto esegue il metodo ecoute:
public void ecoute(){
// thread di ascolto delle richieste dei clienti
// si crea il servizio di ascolto
ServerSocket ecoute=null;
try{
// si crea il servizio
ecoute=new ServerSocket(portEcoute);
// monitoraggio
System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);
// ciclo di servizio
Socket liaisonClient=null;
while (true){ // ciclo infinito
// attesa di un cliente
liaisonClient=ecoute.accept();
// il servizio è gestito da un'altra attività
new traiteClientImpots(liaisonClient,this).start();
// si torna ad ascoltare le richieste
}// fine while
}catch(Exception ex){
// si segnala l'errore
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
}//catch
}//thread di ascolto
È presente un server TCP classico in ascolto sulla porta portEcoute. Le richieste dei client vengono gestite dal metodo run del thread traiteCientImpots, al cui costruttore vengono passati due parametri:
- l'oggetto Socket liaisonClient che consentirà di raggiungere il client
- l'oggetto impotsJDBC this che consentirà di accedere al metodo this.calculer per il calcolo dell'imposta.
// -------------------------------------------------------
// fornisce il servizio a un cliente del server fiscale
class traiteClientImpots extends Thread{
private Socket liaisonClient; // collegamento con il cliente
private BufferedReader IN; // flusso in entrata
private PrintWriter OUT; // flusso in uscita
private impotsJDBC objImpots; // oggetto Imposta
// costruttore
public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
this.liaisonClient=liaisonClient;
this.objImpots=objImpots;
}//costruttore
Il metodo run gestisce le richieste dei clienti. Si tratta di righe di testo che possono assumere due forme:
- calcolo coniugato (sì/no) nbEnfants salaireAnnuel
- fincalculs
La forma 1 consente il calcolo di un'imposta, mentre la forma 2 chiude la connessione cliente-server.
// metodo di esecuzione
public void run(){
// fornisce il servizio al cliente
try{
// flusso in entrata
IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
// flusso di uscita
OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
// invio di un messaggio di benvenuto al cliente
OUT.println("Bienvenue sur le serveur d'impôts");
// ciclo di lettura della richiesta/scrittura della risposta
String demande=null;
String[] champs=null; // gli elementi della richiesta
String commande=null; // il comando del cliente: calcolo o fine calcolo
while ((demande=IN.readLine())!=null){
// si scompone la richiesta in campi
champs=demande.trim().toLowerCase().split("\\s+");
// due richieste accettate: calcolo e fine calcolo
commande=champs[0];
if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
// errore del cliente
OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
// ordine successivo
continue;
}//if
if(commande.equals("calcul")) calculerImpôt(champs);
if(commande.equals("fincalculs")){
// messaggio di saluto al cliente
OUT.println("Au revoir...");
// liberazione delle risorse
try{ OUT.close();IN.close();liaisonClient.close();}
catch(Exception ex){}
// fine
return;
}//if
//richiesta successiva
}//while
}catch (Exception e){
erreur("L'erreur suivante s'est produite ("+e+")",2);
}// fine try
}// fine Run
Il calcolo dell'imposta viene effettuato tramite il metodo calculerImpôt, che riceve come parametro la tabella dei campi della richiesta effettuata dal cliente. Viene verificata la validità della richiesta e, se del caso, l'imposta viene calcolata e restituita al cliente.
// calcolo delle imposte
public void calculerImpôt(String[] champs){
// elabora la richiesta: calcolo per coniugi nbEnfants salaireAnnuel
// scomposto in campi nella tabella dei campi
String marié=null;
int nbEnfants=0;
int salaireAnnuel=0;
// validità degli argomenti
try{
// sono necessari almeno 4 campi
if(champs.length!=4) throw new Exception();
// sposato
marié=champs[1];
if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
// figli
nbEnfants=Integer.parseInt(champs[2]);
// stipendio
salaireAnnuel=Integer.parseInt(champs[3]);
}catch (Exception ignored){
// errore di formato
OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
// fine
return;
}//if
// è possibile calcolare l'imposta
long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
// si invia la risposta al cliente
OUT.println(""+impot);
}//calcolare
Un programma di test potrebbe essere il seguente:
// chiamata: serveurImpots porta dsnImpots userImpots mdpImpots
import java.io.*;
public class testServeurImpots{
public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";
// programma principale
public static void main (String[] args){
// sono necessari 4 argomenti
if(args.length != 4)
erreur(syntaxe,1);
// la porta deve essere un numero intero >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);
// si crea il server delle imposte
try{
new ServeurImpots(port,args[1],args[2],args[3]);
}catch(Exception ex){
//errore
System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
}//catch
}//Main
// visualizzazione degli errori
public static void erreur(String msg, int exitCode){
// visualizzazione dell'errore
System.err.println(msg);
// arresto con errore
System.exit(exitCode);
}//errore
}// fine classe
Si inseriscono nel programma di test i dati necessari alla creazione di un oggetto ServeurImpots e, a partire da lì, il programma crea tale oggetto.
Proviamo a eseguire il codice 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 ServeurImpots che non è ancora in ascolto delle richieste dei client. È il comando start digitato dalla tastiera che avvia l'ascolto. Il comando stop arresta il server. Utilizziamo ora un client. Useremo il client generico creato in precedenza. Il server è avviato:
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:
Si nota che il client ha correttamente ricevuto il messaggio di benvenuto dal server. Si inviano 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 arrestarlo:
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 - Client TCP generico grafico
8.5.1.1. Presentazione dell’applicazione
Ci proponiamo di creare un programma in grado di interagire su Internet con i principali servizi TCP. Lo chiameremo client TCP generico. Una volta compresa questa applicazione, ci si rende conto che tutti i client TCP sono simili. La finestra del programma è la seguente:

Il significato dei diversi controlli è il seguente:
n. | nome | tipo | ruolo |
1 | TxtRemoteHost | JTextField | nome del computer che offre il servizio desiderato |
2 | TxtPort | JTextField | porta del servizio richiesto |
3 | TxtSend | JTextField | testo del messaggio che verrà inviato al server dal client |
4 | OptRCLF OptLF | JCheckBox | pulsanti che consentono di specificare come terminano le righe nella finestra di dialogo client/server RCLF: ritorno a capo (#13) + nuova riga (#10) LF: nuova riga (#10) |
5 | LstSuivi | JList | visualizza i messaggi relativi allo stato della comunicazione tra client e server |
6 | LstDialogue | JList | visualizza i messaggi scambiati dal client (->) e dal server (<-) |
7 | CmdAnnuler | JButton | nascosto - situato sotto l'elenco delle finestre 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 | sotto-opzioni | ruolo |
Connessione | Connetti | connette il client al server |
Disconnetti | chiude la connessione | |
Esci | Chiude il programma | |
Messaggi | Invia | Invia il messaggio del controllo TxtSend al server |
RazSuivi | Cancella l'elenco LstSuivi | |
RazDialogue | Cancella l'elenco LstDialogue | |
Autore | Visualizza una finestra di copyright |
8.5.1.2. FONCTIONNEMENT DE L’APPLICATION
Quando viene caricato il foglio principale dell’applicazione, avvengono le seguenti operazioni:
- la scheda viene centrata sullo schermo
- sono attive solo le opzioni di menu Connexion/Quitter e Auteur
- il pulsante Annuler è nascosto
- gli elenchi LstSuivi e LstDialogue sono vuoti
Questa opzione è disponibile solo quando i campi "Host remoto" e "Numero di porta" non sono vuoti e non è attualmente attiva alcuna connessione. Cliccando su questa opzione si eseguono le seguenti operazioni:
- viene verificata la validità della porta: deve essere un numero intero maggiore di 0
- viene avviato un thread per garantire la connessione al server
- viene visualizzato il pulsante Annuler per consentire all’utente di interrompere la connessione in corso
- tutte le opzioni di menu vengono disattivate tranne Quitter e Auteur
La connessione può terminare in diversi modi:
- L'utente ha premuto il pulsante Annuler: si interrompe il thread di connessione e si riporta il menu allo stato iniziale. Nel log viene indicato che la connessione è stata chiusa dall'utente.
- La connessione termina con un errore: si procede come descritto in precedenza e, inoltre, nel log si indica la causa dell’errore.
- La connessione si conclude correttamente: si rimuove il pulsante Annuler, si indica nel log che la connessione è stata stabilita, si abilita il menu RazSuivi, si disabilita il menu Connecter, si abilita il menu Déconnecter
Questa opzione è disponibile solo quando è presente una connessione con il server. Quando è attivata, chiude la connessione con il server e riporta il menu allo stato iniziale. Nel log viene indicato che la connessione è stata chiusa dal client.
Questa opzione chiude un’eventuale connessione attiva con il server e termina l’applicazione.
Questa opzione è accessibile solo se sono soddisfatte le seguenti condizioni:
-
è stata stabilita la connessione con il server
-
c'è un messaggio da inviare
Se queste condizioni sono soddisfatte, al server viene inviato il testo presente nel campo TxtSend (3), terminato dalla sequenza RCLF se è stata selezionata l'opzione RCLF, altrimenti dalla sequenza LF. Un eventuale errore durante l’invio viene segnalato nell’elenco di tracciamento.
Svuotano 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 cliente sta effettuando la connessione al server. La connessione potrebbe non andare a buon fine perché il server non risponde o risponde in modo errato. Il pulsante Annuler 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 apertura da parte del cliente
-
la sua chiusura da parte del server o del cliente
-
tutti gli errori che possono verificarsi mentre la connessione è attiva
La lista LstDialogue (6) tiene traccia del dialogo che si instaura 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 monitoraggio 6, mentre quelli relativi al dialogo client/server nell’elenco di dialogo 7. In caso di errore di connessione, il dialogo client/server viene chiuso e il modulo riportato al suo stato iniziale, pronto per una nuova connessione.
8.5.1.3. Da TRAVAIL a FAIRE
Eseguire l’operazione descritta in precedenza in due modalità:
- applicazione autonoma
- applet
8.5.2. Esercizio 2 - Un server di risorse
8.5.2.1. INTRODUCTION
Un'istituzione dispone di diversi potenti server di calcolo accessibili su Internet. Ogni macchina che desideri utilizzare questi servizi di calcolo invia un file di dati alla porta 756 di uno dei server. Questo file contiene diverse informazioni: login, password, comandi che indicano il tipo di calcolo desiderato e i dati su cui eseguire il calcolo. Se il file di dati è corretto, il server di calcolo selezionato lo utilizza e restituisce i risultati al cliente sotto forma di file di testo.
I vantaggi di una tale organizzazione sono molteplici:
- Qualsiasi tipo di client (PC, Mac, Unix,...) può utilizzare questo servizio
- il cliente può trovarsi ovunque su Internet
- le risorse di calcolo sono ottimizzate: sono necessarie solo poche macchine potenti. In questo modo, anche una piccola organizzazione priva di risorse di calcolo può utilizzare questo servizio versando un contributo finanziario calcolato in base al tempo di calcolo utilizzato.
Nonostante la potenza delle macchine, un calcolo può talvolta durare diverse ore: in tal caso il server non è disponibile per altri clienti. Si pone quindi, per un cliente, il problema di trovare un server di calcolo disponibile. A tal fine si ricorre a un «gestore delle risorse di calcolo», denominato di seguito server GRC. Questo servizio è ospitato su un’unica macchina e opera sulla porta 864 in modalità TCP. È a lui che si rivolge un cliente che desidera accedere a un server di calcolo. Il server GRC, che detiene l’elenco completo dei server di calcolo, risponde inviandogli il nome di un server attualmente inattivo. A questo punto, il client non deve fare altro che inviare i propri dati al server che gli è stato indicato.
Ci proponiamo di scrivere il server GRC.
8.5.2.2. L’INTERFACE VISUELLE
L’interfaccia visiva sarà la seguente:

L’interfaccia presenta due elenchi di server:
- a sinistra, l’elenco dei server inattivi e 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 | Chiude l'applicazione | |
Autore | Informazioni sul copyright |
La struttura dei controlli presenti nel modulo è la seguente:
Nome | Tipo | Ruolo |
listLibres | JList | Elenco dei server disponibili |
listOccupés | JList | Elenco dei server occupati |
8.5.2.3. FONCTIONNEMENT DE APPLICATION
All’avvio dell’applicazione, l’elenco listLibres viene popolato con l’elenco dei nomi dei server di calcolo gestiti dal GRC. Questi sono definiti in un file «Server» passato come parametro. Questo file contiene un elenco di nomi di server, uno per riga, e viene quindi utilizzato per compilare l’elenco listLibres. Il menu Avvia è abilitato, mentre il menu Arresta è disabilitato.
Questa opzione
- avvia il servizio di ascolto sulla porta 864 del computer
- disabilita il menu Avvia
- abilita il menu «Arresta»
Questa opzione interrompe il servizio:
- l'elenco dei server occupati viene svuotato
- l'elenco dei server liberi viene popolato con il contenuto del file Server
- il menu Avvia viene abilitato
- il menu «Arresta» viene disabilitato
L'applicazione viene chiusa.
Il dialogo client/server avviene tramite lo scambio di righe di testo terminate dalla sequenza RCLF. Il server GRC riconosce due comandi: getserveur e finservice. Di seguito viene descritto in dettaglio il ruolo di questi due comandi:
- 1-getserveur
Il client chiede se c’è un server di calcolo disponibile per lui.
Il server GRC seleziona quindi il primo server trovato nel proprio elenco di server liberi e ne restituisce il nome al client nel formato:
Inoltre, inserisce il server assegnato al cliente nell’elenco dei server occupati nel formato:
come mostra il seguente esempio, in cui il server calcul1.istia.univ-angers.fr è impegnato a servire il client con indirizzo IP 193.52.43.5:

Un client non può inviare un comando getserveur 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. In tal caso, 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 ogni caso, dopo aver risposto al client, il server GRC chiude la connessione con quest’ultimo per poter servire altri client.
- 2-finservice
Il client comunica di non aver 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, verifica che l’indirizzo IP del client sia presente tra quelli registrati nell’elenco dei server occupati. In caso contrario, il server GRC risponde:
Se il client viene riconosciuto, il server GRC risponde:
e sposta il server di calcolo assegnato a quel cliente nell’elenco dei server liberi. Per riprendere l’esempio precedente, se il cliente invia il comando finservice, la visualizzazione del server GRC diventa:

Dopo l’invio della risposta, qualunque essa sia, il server GRC chiude la connessione.
8.5.2.4. TRAVAIL A FAIRE
Scrivere 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. INTRODUCTION
In questo esercizio desideriamo realizzare un client per il servizio SMTP (SendMail Transfer Protocol) che consenta di inviare la posta. Su Unix o Windows, il programma telnet è un client che opera con il protocollo tcp. Può «comunicare» con qualsiasi servizio tcp che accetti comandi in formato testo terminanti con la sequenza RCLF, ovvero i caratteri con codice ASCII 13 e 10. Ecco un esempio di interazione con il servizio smtp per l’invio di posta:
// risposta del 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ò richiamare qualsiasi servizio utilizzando la sintassi
telnet machine_service port_service
Gli scambi client/server avvengono tramite righe di testo che terminano con la sequenza RCLF.
Le risposte del servizio smtp hanno il seguente formato:
numero-Messaggio oppure
numero-Messaggio
Il server SMTP può inviare più righe di risposta. L’ultima riga della risposta è contrassegnata da un numero seguito da uno spazio, mentre nelle 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 del 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
// commenti ---------
Il comando mail ha la seguente sintassi:
mail from: indirizzo e-mail del mittente del messaggio
// fine commenti
// risposta del server SMTP
// commenti
Il server smtp non verifica la validità dell'indirizzo del mittente: lo accetta così come gli è stato fornito
// fine dei commenti
// commenti ---------
Il comando rcpt ha la seguente sintassi:
rcpt to: indirizzo e-mail del destinatario del messaggio
Se l'indirizzo e-mail è un indirizzo del computer su cui opera il server smtp, il server verifica che esista; in caso contrario, non effettua alcuna verifica. Se è stata effettuata una verifica e è stato rilevato un errore, questo verrà segnalato con un numero >= 500.
È possibile inviare tutti i comandi rcpt to che si desidera: ciò consente di inviare un messaggio a più destinatari.
// fine dei commenti
// risposta del server SMTP
// commenti ---------
Il comando data ha la seguente sintassi:
data
riga1
riga2
...
.
È seguita dalle righe di testo che compongono il messaggio, il quale deve terminare con una riga contenente il solo carattere «punto».
Il messaggio viene quindi inviato al destinatario specificato dal comando rcpt.
// fine dei commenti
// risposta del server SMTP
// testo del messaggio digitato dalla tastiera
subject: essai smtp
essai smtp a partir de telnet
.
// commenti
Nelle righe di testo del comando data è possibile inserire una riga subject: per specificare l'oggetto dell'e-mail. Questa riga deve essere seguita da una riga vuota.
// risposta del server SMTP
// commenti
Il comando quit chiude la connessione al servizio smtp
// fine dei commenti
// risposta del server SMTP
8.5.3.2. L’INTERFACE VISUELLE
Si propone di realizzare un programma con la seguente interfaccia grafica:

I controlli hanno le seguenti funzioni:
Numero | Tipo | Funzione |
1 | JTextField | Sequenza di indirizzi e-mail separati da una virgola |
2 | JTextField | Testo dell’oggetto del messaggio |
3 | JTextField | Sequenza di indirizzi e-mail separati da una virgola |
4 | JTextField | Sequenza di indirizzi e-mail separati da una virgola |
5 | JTextArea | Testo del messaggio |
6 | JList | lista di monitoraggio |
7 | JList | elenco delle conversazioni |
8 | JButton | Pulsante «Annulla» non visualizzato, che appare quando il client richiede la connessione al server SMTP. Consente all’utente di interrompere questa richiesta se il server non risponde. |
8.5.3.3. LES MENUS
La struttura dei menu dell’applicazione è la seguente:
Menu principale | Menu secondario | Ruolo |
Posta | ||
Invia | Invia il messaggio del controllo 5 | |
Esci | Esci dall'applicazione | |
Opzioni | ||
Nascondi tracciamento | Rende invisibile il controllo 6 | |
Reset monitoraggio | Svuota l'elenco di monitoraggio 6 | |
Nascondi finestra di dialogo | Nascondi elenco dialoghi 7 | |
Reset dialogo | Svuota l'elenco 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. FONCTIONNEMENT DE APPLICATION
Questo menu fa apparire la seguente finestra:

Entrambi i campi devono essere compilati affinché il pulsante OK sia attivo. Entrambe le informazioni devono essere memorizzate in variabili globali per essere disponibili ad altri moduli.
Questa opzione è accessibile solo se sono soddisfatte le seguenti condizioni:
- la configurazione è stata completata
- c'è un messaggio da inviare
- è presente 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 messo in uno stato in cui tutte le azioni che potrebbero interferire con il dialogo client/server vengono disabilitate
- viene stabilita una connessione sulla porta 25 del server specificato nella configurazione
- il client comunica quindi con il server SMTP secondo il protocollo descritto sopra
- il campo «Mail From:» utilizza l’indirizzo e-mail del mittente specificato nella configurazione
- il campo «rcpt to:» viene utilizzato per ciascuno degli indirizzi e-mail presenti nei campi 1, 3 e 4
- nelle righe inviate dopo il comando data, si troveranno i seguenti testi:
- una riga Subject:: testo dell’oggetto del controllo 2
- una riga Cc: indirizzi del controllo 3
- una riga Bcc: indirizzi del controllo 4
- il testo del messaggio del controllo 5
- il punto terminale
Questo pulsante, situato nella parte inferiore del modulo, appare solo quando il cliente sta effettuando la connessione al server smtp. La connessione potrebbe non andare a buon fine perché il server smtp non risponde o risponde in modo errato. Il pulsante Annuler offre quindi all’utente la possibilità di interrompere la richiesta di connessione.
L'elenco (6) tiene traccia della connessione. Indica i momenti chiave della connessione:
- la sua apertura da parte del client
- la sua chiusura da parte del server o del cliente
- tutti gli errori di connessione
La lista (7) tiene traccia del dialogo smtp che si instaura tra il client e il server.
Questi due elenchi sono associati alle opzioni del menu:
Nascondi elenco di monitoraggio | Rende invisibile l'elenco di monitoraggio 6 e la didascalia che si trova sopra di esso. Se l'altezza occupata da questi due controlli è H, tutti i controlli situati sotto vengono spostati verso l'alto di un'altezza pari a H e la dimensione totale del modulo viene ridotta di H. Inoltre, l’opzione “Nascondi tracciamento” rende invisibile l’opzione RazSuivi sottostante. |
Azzeramento tracciamento | Svuota l’elenco di monitoraggio 6 |
Nascondi finestra di dialogo | Rende invisibile l’elenco di dialogo 7, l’etichetta che si trova sopra di esso e l’opzione di menu RazDialogue sottostante. Come per "Nascondi Monitoraggio", la posizione dei controlli sottostanti (ad esempio il pulsante Annuler) viene ricalcolata e la dimensione della finestra ridotta. |
Reset finestra di dialogo | Svuota l’elenco di dialogo 7 |
Questo menu apre una finestra denominata «Copyright»:

Gli errori di connessione vengono segnalati nell’elenco di monitoraggio 6, mentre quelli relativi alla comunicazione client/server nell’elenco di dialogo 7. In caso di 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, il dialogo client/server viene chiuso e il modulo riportato allo stato iniziale.
8.5.3.5. GESTION da UN FICHIER DE CONFIGURATION
È auspicabile che l’utente non debba riconfigurare il software ogni volta che lo utilizza. A tal fine, se l’opzione «Opzioni/Salva la configurazione all’uscita» è selezionata, alla chiusura del programma vengono salvate sia le informazioni acquisite tramite l’opzione Options/Configurer sia lo stato dei due elenchi di monitoraggio 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 riportano le due informazioni acquisite tramite l'opzione Opzioni/Configura. Le righe Suivi e Dialogue indicano lo stato delle liste di Monitoraggio e di Dialogo: 1 (presente), 0 (assente).
All’avvio del programma, il file sendmail.ini viene letto, se presente, e il modulo viene configurato di conseguenza. Se il file sendmail.ini non esiste, si procede come se fosse presente:
Se il file sendmail.ini esiste ma è incompleto (righe mancanti), la riga mancante viene sostituita dalla riga corrispondente sopra indicata. Pertanto, poiché manca la riga Suivi=..., si procede come se si avesse 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 in minuscolo: non fa differenza.
Nell’opzione Options/Configurer vengono visualizzati i valori SmtpServer e ReplyAddress attualmente in uso. L’utente può quindi modificarli se lo desidera.
8.5.3.6. Da TRAVAIL a FAIRE
Eseguire l’operazione descritta in precedenza. Si consiglia di occuparsi della gestione del file di configurazione per ultima.
8.5.4. Esercizio 4 - client POPPASS
8.5.4.1. Introduction
Si intende creare un client TCP in grado di comunicare con il server POPPASSD che opera sulla porta 106. Questo servizio consente di modificare la propria password su una macchina UNIX. Il protocollo di comunicazione client/server è il seguente:
1 - Le comunicazioni avvengono tramite lo scambio di messaggi che terminano con la sequenza RCLF
2 - Il client invia comandi al server
- Il server risponde con messaggi che iniziano con numeri a 3 cifre: XXX. Se XXX=200, il comando è stato eseguito correttamente, altrimenti si è verificato un errore.
3 - La cronologia degli scambi è la seguente:
- il server risponde con un messaggio di benvenuto
- il server risponde richiedendo la password se il login viene accettato, altrimenti 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 del client

Il significato dei diversi controlli è il seguente:
n. | nome | tipo | funzione |
1 | txtRemoteHost | JTextField | nome del server |
2 | txtLogin | JTextField | nome utente |
3 | txtMdp | JTextField | Password utente |
4 | txtNewMdp | JTextField | Nuova password utente |
5 | txtConfirmation | JTextField | Conferma della nuova password |
6 | lstSuivi | JList | Messaggi relativi al monitoraggio della connessione |
7 | lstDialogue | JList | Messaggi della finestra di dialogo Client/Server |
10 | cmdAnnuler | JButton | non raffigurato - Pulsante che appare quando è in corso la connessione al server. Consente di interromperla. |
8.5.4.3. I menu
Titolo | Nome del controllo | Funzione |
Connessione | mnuconnexion | |
Connetti | mnuconnetter | avvia la connessione al server |
Esci | mnuQuitter | chiude l'applicazione |
Messaggi | mnuMessages | |
RazSuivi | mnuRazSuivi | cancella l'elenco lstSuivi |
RazDialogue | mnuRazDialogue | cancella l'elenco lstDialogue |
Autore | mnuAuteur | visualizza la finestra del copyright |
8.5.4.4. Funzionamento dell'applicazione
Quando viene caricata la schermata principale dell'applicazione, avvengono le seguenti operazioni:
- la schermata viene centrata sullo schermo
- sono attive solo le opzioni di menu Connexion/Quitter e Auteur
- il pulsante Annuler è nascosto
- gli elenchi LstSuivi e LstDialogue sono vuoti
Questa opzione è disponibile solo se i campi da 1 a 5 sono stati compilati. Cliccando su questa opzione vengono eseguite le seguenti operazioni:
- viene avviato un thread per garantire la connessione al server
- viene visualizzato il pulsante Annuler per consentire all’utente di interrompere la connessione in corso
- tutte le opzioni del menu vengono disattivate tranne Quitter e Auteur
La sequenza degli eventi è quindi la seguente:
- L'utente ha premuto il pulsante Annuler: il thread di connessione viene interrotto e il menu viene riportato allo stato iniziale. Nel log viene indicato che la connessione è stata chiusa dall'utente.
- La richiesta di connessione viene accettata dal server. Si avvia quindi il dialogo con il server per modificare la password. Gli scambi di questo dialogo vengono registrati nell’elenco LstDialogue. Una volta terminato il dialogo, la connessione con il server viene chiusa e il menu del modulo 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 durante la comunicazione si verifica un errore, la connessione viene chiusa e la causa dell’errore viene visualizzata nell’elenco di tracciamento LstSuivi.
Questa opzione chiude un’eventuale connessione attiva con il server e termina l’applicazione.
Svuotano 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 cliente sta effettuando la connessione o è già connesso al server. Il pulsante Annuler offre all’utente la possibilità di interrompere la comunicazione con il server.
L’elenco LstSuivi (5) tiene traccia della connessione. Indica i momenti chiave della connessione:
-
la sua apertura da parte del client
-
la sua chiusura da parte del server o del cliente
-
tutti gli errori che possono verificarsi mentre la connessione è attiva
La lista LstDialogue (6) tiene traccia del dialogo che si instaura tra il client e il server.
Questo menu apre una finestra denominata «Copyright»:

Gli errori di comunicazione vengono segnalati nell’elenco di monitoraggio 6, mentre quelli relativi al dialogo client/server nell’elenco di dialogo 7. In caso di errore di connessione, il dialogo client/server viene chiuso e il modulo riportato allo stato iniziale, pronto per una nuova connessione.
8.5.4.5. Da TRAVAIL a FAIRE
Realizzare il lavoro descritto in precedenza sotto forma di applicazione autonoma e successivamente di applet.