Skip to content

8. Programmazione TCP-IP

8.1. Informazioni generali

8.1.1. Protocolli Internet

In questa sezione forniamo un'introduzione ai protocolli di comunicazione Internet, noti anche come suite TCP/IP (Transmission Control Protocol / Internet Protocol), dal nome dei due protocolli principali. È consigliabile che il lettore abbia una conoscenza generale del funzionamento delle reti, e in particolare dei protocolli TCP/IP, prima di affrontare lo sviluppo di applicazioni distribuite.

Il testo che segue è una traduzione parziale di un brano tratto da "LAN Workplace for DOS – Administrator’s Guide" di NOVELL, un documento risalente ai primi anni '90.


Il concetto generale di creazione di una rete di computer eterogenei deriva dalla ricerca condotta dalla DARPA (Defense Advanced Research Projects Agency) negli Stati Uniti. La DARPA ha sviluppato la suite di protocolli nota come TCP/IP, che consente a macchine eterogenee di comunicare tra loro. Questi protocolli sono stati testati su una rete chiamata ARPAnet, che in seguito è diventata Internet. I protocolli TCP/IP definiscono formati e regole per la trasmissione e la ricezione che sono indipendenti dall'architettura di rete e dall'hardware utilizzato.

La rete progettata dalla DARPA e gestita dai protocolli TCP/IP è una rete a commutazione di pacchetti. Una rete di questo tipo trasmette le informazioni attraverso la rete in piccoli frammenti chiamati pacchetti. Pertanto, se un computer trasmette un file di grandi dimensioni, questo verrà suddiviso in piccoli frammenti che vengono inviati attraverso la rete per essere ricomposti a destinazione. Il TCP/IP definisce il formato di questi pacchetti, ovvero:

  • origine del pacchetto
  • destinazione
  • lunghezza
  • tipo

8.1.2. Il modello OSI

I protocolli TCP/IP seguono in linea di massima il modello di rete aperto noto come OSI (Open Systems Interconnection Reference Model) definito dall'ISO (International Standards Organization). Questo modello descrive una rete ideale in cui la comunicazione tra macchine può essere rappresentata da un modello a sette livelli:

Image

Ogni livello riceve servizi dal livello sottostante e fornisce i propri servizi al livello sovrastante. Supponiamo che due applicazioni situate su macchine diverse A e B vogliano comunicare: lo fanno a livello di Applicazione. Non hanno bisogno di conoscere tutti i dettagli di come funziona la rete: ogni applicazione passa le informazioni che desidera trasmettere al livello sottostante: il livello di Presentazione. L'applicazione deve quindi conoscere solo le regole per interfacciarsi con il livello di Presentazione.

Una volta che le informazioni si trovano nel livello di Presentazione, vengono passate secondo altre regole al livello di Sessione, e così via, fino a quando le informazioni raggiungono il mezzo fisico e vengono trasmesse fisicamente alla macchina di destinazione. Lì, subiranno il processo inverso di quello che hanno subito sulla macchina di invio.

Ad ogni livello, il processo mittente responsabile dell'invio delle informazioni le invia a un processo ricevente sull'altra macchina appartenente allo stesso livello. Lo fa secondo determinate regole note come protocollo di livello. Abbiamo quindi il seguente diagramma di comunicazione finale:

Image

I ruoli dei diversi livelli sono i seguenti:

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

8.1.3. Il modello TCP/IP

Il modello OSI è un modello ideale che non è mai stato pienamente realizzato. La suite di protocolli TCP/IP lo approssima nel modo seguente:

Image

Livello fisico

Nelle reti locali, si utilizzano generalmente le tecnologie Ethernet o Token Ring. In questa sede ci concentreremo esclusivamente sulla tecnologia Ethernet.

Ethernet

Questo è il nome dato a una tecnologia di rete locale a commutazione di pacchetti inventata presso il Xerox PARC nei primi anni '70 e standardizzata da Xerox, Intel e Digital Equipment nel 1978. La rete è costituita fisicamente da un cavo coassiale di circa 1,27 cm di diametro e lungo fino a 500 m. Può essere estesa utilizzando ripetitori, con non più di due ripetitori che separano due macchine qualsiasi. Il cavo è passivo: tutti i componenti attivi si trovano sulle macchine collegate al cavo. Ogni macchina è collegata al cavo tramite una scheda di accesso alla rete che comprende:

  • un ricetrasmettitore che rileva la presenza di segnali sul cavo e converte i segnali analogici in segnali digitali e viceversa.
  • un accoppiatore che riceve i segnali digitali dal ricetrasmettitore e li trasmette al computer per l'elaborazione, o viceversa.

Le caratteristiche principali della tecnologia Ethernet sono le seguenti:

  • Capacità di 10 megabit al secondo.
  • Topologia a bus: tutte le macchine sono collegate allo stesso cavo

Image

  • Rete broadcast: una macchina trasmittente invia informazioni sul cavo indicando l’indirizzo della macchina ricevente. Tutte le macchine collegate ricevono quindi queste informazioni, ma solo il destinatario previsto le conserva.
  • Il metodo di accesso è il seguente: il trasmettitore che desidera trasmettere ascolta il cavo — quindi rileva se è presente o meno un'onda portante, la cui presenza indicherebbe che è in corso una trasmissione. Questa è la tecnica CSMA (Carrier Sense Multiple Access). In assenza di una portante, un trasmettitore può decidere di trasmettere a sua volta. Diversi trasmettitori possono prendere questa decisione. I segnali trasmessi si sovrappongono: questo fenomeno è chiamato collisione. Il trasmettitore rileva questa situazione: mentre trasmette sul cavo, ascolta anche ciò che effettivamente lo attraversa. Se rileva che le informazioni che viaggiano sul cavo non sono quelle che ha trasmesso, conclude che si è verificata una collisione e interrompe la trasmissione. Gli altri trasmettitori che stavano trasmettendo faranno lo stesso. Ciascuno riprenderà la trasmissione dopo un ritardo casuale che dipende dal singolo trasmettitore. Questa tecnica è chiamata CD (Collision Detect). Il metodo di accesso è quindi chiamato CSMA/CD.
  • Indirizzamento a 48 bit. Ogni macchina ha un indirizzo, qui indicato come indirizzo fisico, che è scritto sulla scheda che la collega al cavo. Questo indirizzo è chiamato indirizzo Ethernet della macchina.

Livello di rete

A questo livello, troviamo i protocolli IP, ICMP, ARP e RARP.

IP (Internet Protocol)
Trasmette i pacchetti tra due nodi di rete
ICMP
(Internet Control Message Protocol)
L'ICMP facilita la comunicazione tra il programma del protocollo IP su una macchina e quello su un'altra macchina. Si tratta quindi di un protocollo di scambio di messaggi all'interno del protocollo IP stesso.
ARP
(Address Resolution Protocol)
mappa l'indirizzo IP di un computer al suo indirizzo fisico
RARP
(Reverse Address Resolution Protocol)
mappa l'indirizzo fisico di una macchina al suo indirizzo Internet

Livelli di trasporto/sessione

Questo livello include i seguenti protocolli:

TCP (Transmission Control Protocol)
Garantisce la consegna affidabile delle informazioni tra due client
UDP (User Datagram Protocol)
Garantisce la trasmissione non affidabile di informazioni tra due client

Livelli Applicazione/Presentazione/Sessione

Qui si trovano vari protocolli:

TELNET
Un emulatore di terminale che permette alla macchina A di connettersi alla macchina B come terminale
FTP (File Transfer Protocol)
Consente il trasferimento di file
TFTP (Trivial File Transfer Protocol)
consente il trasferimento di file
SMTP (Simple Mail Transfer Protocol)
consente lo scambio di messaggi tra gli utenti della rete
DNS (Domain Name System)
converte il nome di un computer nel suo indirizzo Internet
XDR (eXternal Data Representation)
Creato da Sun Microsystems, specifica uno standard di rappresentazione dei dati indipendente dalla macchina
RPC (Remote Procedure Call)
definito anch'esso da Sun, è un protocollo di comunicazione tra applicazioni remote, indipendente dal livello di trasporto. Questo protocollo è importante: solleva il programmatore dalla necessità di conoscere i dettagli del livello di trasporto e rende le applicazioni portabili. Questo protocollo si basa sul protocollo XDR
NFS (Network File System)
Definito anch'esso da Sun, questo protocollo permette a una macchina di "vedere" il file system di un'altra macchina. Si basa sul protocollo RPC menzionato sopra

8.1.4. Come funzionano i protocolli Internet

Le applicazioni sviluppate nell'ambiente TCP/IP utilizzano generalmente diversi protocolli presenti in questo ambiente. Un programma applicativo comunica con il livello più alto dei protocolli. Questo livello trasmette le informazioni al livello sottostante e così via, fino a raggiungere il supporto fisico. Qui, le informazioni vengono trasferite fisicamente alla macchina di destinazione, dove attraverseranno nuovamente gli stessi livelli, questa volta in direzione opposta, fino a raggiungere l'applicazione destinata a ricevere le informazioni inviate. Il diagramma seguente mostra il percorso delle informazioni:

Image

Facciamo un esempio: l'applicazione FTP, definita a livello di applicazione, che consente il trasferimento di file tra macchine.

  • L'applicazione invia una sequenza di byte da trasmettere al livello di trasporto.
  • Il livello di trasporto divide questa sequenza di byte in segmenti TCP e aggiunge il numero di segmento all'inizio di ciascun segmento. I segmenti vengono passati al livello di rete, che è regolato dal protocollo IP.
  • Il livello IP crea un pacchetto che incapsula il segmento TCP ricevuto. Nell'intestazione di questo pacchetto, inserisce gli indirizzi Internet delle macchine di origine e di destinazione. Determina inoltre l'indirizzo fisico della macchina di destinazione. L'intero pacchetto viene passato al livello Data Link & Physical, ovvero alla scheda di rete che collega la macchina alla rete fisica.
  • Lì, il pacchetto IP viene a sua volta incapsulato in un frame fisico e inviato al destinatario tramite il cavo.
  • Sulla macchina di destinazione, il livello Data Link & Physical fa l'opposto: decapsula il pacchetto IP dal frame fisico e lo passa al livello IP.
  • Il livello IP verifica che il pacchetto sia corretto: calcola un checksum basato sui bit ricevuti, che deve trovare nell'intestazione del pacchetto. In caso contrario, il pacchetto viene rifiutato.
  • Se il pacchetto viene ritenuto valido, il livello IP decapsula il segmento TCP in esso contenuto e lo passa al livello di trasporto.
  • Il livello di trasporto — nel nostro esempio il livello TCP — esamina il numero del segmento per assicurarsi che i segmenti siano nell'ordine corretto.
  • Calcola inoltre un checksum per il segmento TCP. Se risulta corretto, il livello TCP invia un riconoscimento alla macchina di origine; in caso contrario, il segmento TCP viene rifiutato.
  • Tutto ciò che resta da fare al livello TCP è trasmettere la parte di dati del segmento all'applicazione destinata a riceverla nel livello superiore.

8.1.5. Affrontare i problemi su Internet

Un nodo di rete può essere un computer, una stampante intelligente, un file server — qualsiasi cosa, in effetti, che possa comunicare utilizzando i protocolli TCP/IP. Ogni nodo ha un indirizzo fisico con un formato che dipende dal tipo di rete. Su una rete Ethernet, l'indirizzo fisico è codificato su 6 byte. Un indirizzo di rete X.25 è un numero di 14 cifre.

L'indirizzo Internet di un nodo è un indirizzo logico: è indipendente dall'hardware e dalla rete utilizzati. Si tratta di un indirizzo di 4 byte che identifica sia una rete locale che un nodo su quella rete. L'indirizzo Internet è solitamente rappresentato da quattro numeri — i valori dei quattro byte — separati da un punto. Pertanto, l'indirizzo del computer Lagaffe presso la Facoltà di Scienze di Angers è 193.49.144.1, mentre quello del computer Liny è 193.49.144.9. Da ciò si deduce che l'indirizzo Internet della rete locale è 193.49.144.0. Su questa rete possono esserci fino a 254 nodi.

Poiché gli indirizzi Internet, o indirizzi IP, sono indipendenti dalla rete, un computer sulla rete A può comunicare con un computer sulla rete B senza preoccuparsi del tipo di rete su cui si trova: deve semplicemente conoscere il suo indirizzo IP. Il protocollo IP di ciascuna rete gestisce la conversione tra indirizzo IP e indirizzo fisico, in entrambe le direzioni.

Tutti gli indirizzi IP devono essere univoci. Le organizzazioni ufficiali sono responsabili della loro assegnazione. Infatti, queste organizzazioni assegnano un indirizzo alle reti locali, ad esempio 193.49.144.0 per la rete della Facoltà di Scienze di Angers. L'amministratore di questa rete può quindi assegnare gli indirizzi IP da 193.49.144.1 a 193.49.144.254 a sua discrezione. Questo indirizzo è generalmente memorizzato in un file specifico su ogni macchina collegata alla rete.

8.1.5.1. Classi di indirizzi IP

Un indirizzo IP è una sequenza di 4 byte, spesso scritta come I1.I2.I3.I4, che in realtà contiene due indirizzi:

  • l'indirizzo di rete
  • l'indirizzo di un nodo su quella rete

A seconda delle dimensioni di questi due campi, gli indirizzi IP sono suddivisi in 3 classi: classi A, B e C.

Classe A

L'indirizzo IP: I1.I2.I3.I4 ha la forma R1.N1.N2.N3 dove

R1 è l'indirizzo di rete

N1.N2.N3 è l'indirizzo di una macchina su quella rete

Più precisamente, il formato di un indirizzo IP di Classe A è il seguente:

Image

L'indirizzo di rete è lungo 7 bit e l'indirizzo host è lungo 24 bit. Possono quindi esserci 127 reti di Classe A, ciascuna contenente fino a 2²⁴ host.

Classe B

In questo caso, l'indirizzo IP: I1.I2.I3.I4 ha la forma R1.R2.N1.N2 dove

R1.R2 è l'indirizzo di rete

N1.N2 è l'indirizzo di una macchina su quella rete

Più precisamente, il formato di un indirizzo IP di Classe B è il seguente:

Image

L'indirizzo di rete è di 2 byte (14 bit, per l'esattezza), così come l'indirizzo del nodo. Ciò significa che possono esserci 2¹⁴ reti di Classe B, ciascuna contenente fino a 2¹⁶ nodi.

Classe C

In questa classe, l'indirizzo IP: I1.I2.I3.I4 ha la forma R1.R2.R3.N1 dove

R1.R2.R3 è l'indirizzo di rete

N1 è l'indirizzo di una macchina su quella rete

Più precisamente, il formato di un indirizzo IP di Classe C è il seguente:

Image

L'indirizzo di rete occupa 3 byte (meno 3 bit) e l'indirizzo host occupa 1 byte. È quindi possibile avere 2²¹ reti di Classe C, ciascuna delle quali può contenere fino a 256 host.

Poiché l'indirizzo della macchina Lagaffe presso la Facoltà di Scienze di Angers è 193.49.144.1, vediamo che il byte più significativo è 193, che in binario è 11000001. Possiamo dedurre che la rete è una rete di Classe C.

Indirizzi riservati

. Alcuni indirizzi IP sono indirizzi di rete piuttosto che indirizzi di nodo all'interno della rete. Si tratta di quelli in cui l'indirizzo del nodo è impostato su 0. Pertanto, l'indirizzo 193.49.144.0 è l'indirizzo IP della rete della Facoltà di Scienze di Angers. Di conseguenza, nessun nodo su una rete può avere l'indirizzo zero.

. Quando l'indirizzo del nodo in un indirizzo IP è composto interamente da 1, si tratta di un indirizzo di broadcast: questo indirizzo si riferisce a tutti i nodi della rete.

. In una rete di Classe C, che teoricamente consente 2⁸ = 256 nodi, se eliminiamo i due indirizzi proibiti, ci rimangono solo 254 indirizzi autorizzati.

8.1.5.2. Protocolli di conversione tra indirizzi Internet e indirizzi fisici

Abbiamo visto che quando i dati vengono trasmessi da una macchina all'altra, vengono incapsulati in pacchetti mentre attraversano il livello IP. Questi pacchetti hanno la seguente forma:

Image

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

Image

Nel frame finale sono presenti gli indirizzi fisici delle macchine di origine e di destinazione. Come vengono ottenuti?

Il computer mittente, conoscendo l’indirizzo IP del computer con cui desidera comunicare, ottiene l’indirizzo fisico di quest’ultimo utilizzando un protocollo specifico chiamato ARP (Address Resolution Protocol).

  • Invia un tipo speciale di pacchetto chiamato pacchetto ARP contenente l’indirizzo IP del computer di cui si sta cercando l’indirizzo fisico. Nel pacchetto include anche il proprio indirizzo IP e il proprio indirizzo fisico.
  • Questo pacchetto viene inviato a tutti i nodi della rete.
  • Questi nodi riconoscono la natura speciale del pacchetto. Il nodo che riconosce il proprio indirizzo IP nel pacchetto risponde inviando il proprio indirizzo fisico al mittente del pacchetto. Come può farlo? Ha trovato gli indirizzi IP e fisici del mittente nel pacchetto.
  • Il mittente riceve così l'indirizzo fisico che stava cercando. Lo memorizza in modo da poterlo utilizzare in seguito se altri pacchetti devono essere inviati allo stesso destinatario.

L'indirizzo IP di una macchina è normalmente memorizzato in uno dei suoi file di configurazione, che può consultare per recuperarlo. Questo indirizzo può essere modificato: basta modificare il file. L'indirizzo fisico, invece, è memorizzato nella memoria della scheda di rete e non può essere modificato.

Quando un amministratore desidera riorganizzare la rete, potrebbe dover modificare gli indirizzi IP di tutti i nodi e quindi modificare i vari file di configurazione per ciascun nodo. Ciò può risultare noioso e soggetto a errori se le macchine sono numerose. Un metodo consiste nel non assegnare un indirizzo IP alle macchine: invece, viene inserito un codice speciale nel file in cui la macchina troverebbe normalmente il proprio indirizzo IP. Una volta scoperto di non avere un indirizzo IP, la macchina ne richiede uno utilizzando un protocollo chiamato RARP (Reverse Address Resolution Protocol). Invia quindi un pacchetto speciale chiamato pacchetto RARP sulla rete — simile al precedente pacchetto ARP — contenente il proprio indirizzo fisico. Questo pacchetto viene inviato a tutti i nodi che riconoscono un pacchetto RARP. Uno di essi, chiamato server RARP, mantiene un file contenente le mappature indirizzo fisico <--> indirizzo IP per tutti i nodi. Risponde quindi al mittente del pacchetto RARP inviando il proprio indirizzo IP. Un amministratore che desideri riconfigurare la propria rete deve quindi solo modificare il file di mappatura sul server RARP ( ). Questo server deve normalmente avere un indirizzo IP fisso che deve essere in grado di conoscere senza dover utilizzare il protocollo RARP stesso.

8.1.6. Il livello di rete, noto come livello IP di Internet

L'IP (Internet Protocol) definisce il formato che i pacchetti devono assumere e come devono essere gestiti durante la trasmissione o la ricezione. Questo specifico tipo di pacchetto è chiamato datagramma IP. Ne abbiamo già parlato:

Image

Il punto importante è che, oltre ai dati da trasmettere, il datagramma IP contiene gli indirizzi Internet delle macchine di origine e di destinazione. In questo modo, la macchina ricevente sa chi le sta inviando un messaggio.

A differenza di un frame di rete, la cui lunghezza è determinata dalle caratteristiche fisiche della rete su cui viaggia, la lunghezza del datagramma IP è fissata dal software e sarà quindi la stessa su reti fisiche diverse. Abbiamo visto che, scendendo dal livello di rete al livello fisico, il datagramma IP viene incapsulato all'interno di un frame fisico. Abbiamo fatto l'esempio del frame fisico di una rete Ethernet:

Image

I frame fisici viaggiano da nodo a nodo verso la loro destinazione, che potrebbe non trovarsi sulla stessa rete fisica del computer mittente. Il pacchetto IP può quindi essere incapsulato in successione in diversi frame fisici nei nodi che collegano due reti di tipo diverso. È anche possibile che il pacchetto IP sia troppo grande per essere incapsulato in un singolo frame fisico. Il software IP sul nodo in cui si verifica questo problema suddivide quindi il pacchetto IP in frammenti secondo regole specifiche, ciascuno dei quali viene poi inviato sulla rete fisica. Essi verranno riassemblati solo alla loro destinazione finale.

8.1.6.1. Routing

Il routing è il metodo utilizzato per indirizzare i pacchetti IP verso la loro destinazione. Esistono due metodi: il routing diretto e il routing indiretto.

Routing diretto

Il routing diretto si riferisce alla trasmissione di un pacchetto IP direttamente dal mittente al destinatario all'interno della stessa rete:

  • Il dispositivo che invia un datagramma IP possiede l'indirizzo IP del destinatario.
  • Ottiene l'indirizzo fisico del destinatario tramite il protocollo ARP o dalle proprie tabelle, se tale indirizzo è già stato ottenuto.
  • Invia il pacchetto sulla rete a quell'indirizzo fisico.

Routing indiretto

Il routing indiretto si riferisce alla trasmissione di un pacchetto IP verso una destinazione situata su una rete diversa da quella a cui appartiene il mittente. In questo caso, le parti relative all’indirizzo di rete degli indirizzi IP delle macchine di origine e di destinazione sono diverse. La macchina di origine lo riconosce. Invia quindi il pacchetto a un nodo speciale chiamato router, un nodo che collega una rete locale ad altre reti e il cui indirizzo IP trova nelle proprie tabelle: un indirizzo inizialmente ottenuto da un file, dalla memoria permanente o tramite informazioni che circolano sulla rete.

Un router è collegato a due reti e possiede un indirizzo IP all'interno di entrambe queste reti.

Image

Nel nostro esempio sopra:

  • La rete n. 1 ha l'indirizzo Internet 193.49.144.0 e la rete n. 2 ha l'indirizzo 193.49.145.0.
  • All'interno della rete n. 1, il router ha l'indirizzo 193.49.144.6 e l'indirizzo 193.49.145.3 all'interno della rete n. 2.

Il ruolo del router è quello di prendere il pacchetto IP che riceve, contenuto in un frame fisico tipico della rete n. 1, e inserirlo in un frame fisico in grado di viaggiare sulla rete n. 2. Se l'indirizzo IP della destinazione del pacchetto si trova all'interno della rete n. 2, il router invierà il pacchetto direttamente a essa; altrimenti, lo invierà a un altro router, collegando la rete n. 2 alla rete n. 3, e così via.

8.1.6.2. Messaggi di errore e di controllo

Sempre all'interno del livello di rete, allo stesso livello del protocollo IP, si trova l'ICMP (Internet Control Message Protocol). Viene utilizzato per inviare messaggi relativi al funzionamento interno della rete: nodi guasti, congestione a un router, ecc. I messaggi ICMP sono incapsulati in pacchetti IP e inviati sulla rete. I livelli IP dei vari nodi intraprendono le azioni appropriate in base ai messaggi ICMP che ricevono. Pertanto, un'applicazione non vede mai questi problemi specifici della rete.

Un nodo utilizzerà le informazioni ICMP per aggiornare le proprie tabelle di routing.

8.1.7. Il livello di trasporto: i protocolli UDP e TCP

8.1.7.1. Il protocollo UDP: User Datagram Protocol

Il protocollo UDP consente uno scambio di dati inaffidabile tra due punti, il che significa che la consegna di un pacchetto a destinazione non è garantita. L'applicazione, se lo desidera, può gestire questo aspetto autonomamente, ad esempio attendendo un riconoscimento dopo l'invio di un messaggio prima di inviare quello successivo.

Finora, a livello di rete, abbiamo parlato degli indirizzi IP delle macchine. Tuttavia, su una singola macchina possono coesistere contemporaneamente diversi processi, che possono comunicare tra loro. Pertanto, quando si invia un messaggio, è necessario specificare non solo l'indirizzo IP del computer di destinazione, ma anche il "nome" del processo di destinazione. Questo nome è in realtà un numero, chiamato numero di porta. Alcuni numeri sono riservati alle applicazioni standard: la porta 69 per l'applicazione TFTP (Trivial File Transfer Protocol), ad esempio.

I pacchetti gestiti dal protocollo UDP sono chiamati anche datagrammi. Hanno la seguente forma:

Image

Questi datagrammi sono incapsulati in pacchetti IP e poi in frame fisici.

8.1.7.2. Il protocollo TCP: Transmission Control Protocol

Per comunicazioni sicure, il protocollo UDP è insufficiente: lo sviluppatore dell'applicazione deve creare un protocollo autonomamente per garantire che i pacchetti vengano instradati correttamente.

Il TCP (Transmission Control Protocol) evita questi problemi. Le sue caratteristiche sono le seguenti:

  • Il processo che desidera trasmettere stabilisce innanzitutto una connessione con il processo che riceve le informazioni che sta per trasmettere. Questa connessione viene stabilita tra una porta sulla macchina mittente e una porta sulla macchina ricevente. Viene così creato un percorso virtuale tra le due porte, che sarà riservato esclusivamente ai due processi che hanno stabilito la connessione.
  • Tutti i pacchetti inviati dal processo di origine seguono questo percorso virtuale e arrivano nell'ordine in cui sono stati inviati, cosa che non era garantita nel protocollo UDP poiché i pacchetti potevano seguire percorsi diversi.
  • I dati trasmessi appaiono continui. Il processo mittente invia i dati al proprio ritmo. Questi dati non vengono necessariamente inviati immediatamente: il protocollo TCP attende di averne una quantità sufficiente da inviare. Essi vengono memorizzati in una struttura denominata segmento TCP. Una volta che questo segmento è pieno, viene trasmesso al livello IP, dove viene incapsulato in un pacchetto IP.
  • Ogni segmento inviato dal protocollo TCP è numerato. Il protocollo TCP ricevente verifica di ricevere i segmenti in sequenza. Per ogni segmento ricevuto correttamente, invia un riconoscimento al mittente.
  • Quando il destinatario lo riceve, ne dà notifica al mittente. Il mittente può così confermare che un segmento è stato consegnato con successo, cosa che non era possibile con il protocollo UDP.
  • Se, trascorso un certo periodo di tempo, il protocollo TCP che ha inviato un segmento non riceve un riconoscimento, esso ritrasmette il segmento in questione, garantendo così la qualità del servizio di trasmissione delle informazioni.
  • Il circuito virtuale stabilito tra i due processi in comunicazione è full-duplex: ciò significa che le informazioni possono fluire in entrambe le direzioni. Pertanto, il processo di destinazione può inviare conferme anche mentre il processo di origine continua a inviare informazioni. Ciò consente, ad esempio, al protocollo TCP di origine di inviare più segmenti senza attendere una conferma. Se, dopo un certo periodo di tempo, si rende conto di non aver ricevuto un riconoscimento per un segmento specifico n. n, riprenderà l'invio dei segmenti da quel punto.

8.1.8. Il livello applicativo

Al di sopra dei protocolli UDP e TCP, esistono vari protocolli standard:

TELNET

Questo protocollo consente a un utente sulla macchina A in rete di connettersi alla macchina B (spesso chiamata macchina host). TELNET emula un cosiddetto terminale universale sulla macchina A. L&#x27;utente si comporta quindi come se avesse un terminale collegato alla macchina B. Telnet si basa sul protocollo TCP.

FTP: (File Transfer Protocol)

Questo protocollo consente lo scambio di file tra due macchine remote, nonché operazioni sui file come, ad esempio, la creazione di directory. Si basa sul protocollo TCP.

TFTP: (Trivial File Transfer Protocol)

Questo protocollo è una variante dell&#x27;FTP. Si basa sul protocollo UDP ed è meno sofisticato dell&#x27;FTP.

DNS: (Domain Name System)

Quando un utente desidera scambiare file con una macchina remota, ad esempio tramite FTP, deve conoscere l&#x27;indirizzo Internet di quella macchina. Ad esempio, per utilizzare FTP sulla macchina Lagaffe dell&#x27;Università di Angers, è necessario eseguire FTP come segue: FTP 193.49.144.1

Ciò richiede una directory che associ le macchine agli indirizzi IP. In questa directory, le macchine sarebbero probabilmente designate da nomi simbolici come:

Macchina DPX2/320 presso l&#x27;Università di Angers

Macchina Sun presso l&#x27;ISERPA ad Angers

È chiaro che sarebbe più comodo riferirsi a una macchina con un nome piuttosto che con il suo indirizzo IP. Ciò solleva la questione dell&#x27;unicità dei nomi: ci sono milioni di macchine interconnesse. Si potrebbe immaginare un organismo centralizzato che assegni i nomi. Ciò sarebbe senza dubbio piuttosto macchinoso. Il controllo sui nomi è stato infatti distribuito tra **i domini**. Ogni dominio è gestito da un&#x27;organizzazione generalmente molto snella che ha completa libertà nella scelta dei nomi delle macchine. Pertanto, le macchine in Francia appartengono al dominio **fr**, gestito dall&#x27;Inria di Parigi. Per semplificare ulteriormente le cose, il controllo è distribuito ancora di più: all&#x27;interno del dominio **fr** vengono creati dei sottodomini. Pertanto, l&#x27;Università di Angers appartiene al dominio **univ-Angers**. Il dipartimento che gestisce questo dominio ha completa libertà di denominare le macchine sulla rete dell&#x27;Università di Angers. Per ora, questo dominio non è stato suddiviso. Ma in una grande università con molte macchine collegate in rete, potrebbe esserlo.

La macchina DPX2/320 dell&#x27;Università di Angers è stata denominata *Lagaffe*, mentre un PC 486DX50 è stato denominato *liny*. Come si fa a fare riferimento a queste macchine dall&#x27;esterno? Specificando la gerarchia dei domini a cui appartengono. Pertanto, il nome completo della macchina Lagaffe sarà:
        Lagaffe.univ-Angers.fr
All&#x27;interno dei domini è possibile utilizzare nomi relativi. Pertanto, all&#x27;interno del dominio **fr** e all&#x27;esterno del dominio **univ-Angers**, è possibile fare riferimento alla macchina Lagaffe come
        Lagaffe.univ-Angers
Infine, all&#x27;interno del dominio *univ-Angers*, è possibile fare riferimento ad essa semplicemente come
        Lagaffe
Un&#x27;applicazione può quindi fare riferimento a una macchina tramite il suo nome. In definitiva, però, è comunque necessario ottenere l&#x27;indirizzo IP della macchina. Come si fa? Supponiamo di voler comunicare dalla macchina A alla macchina B.
  • Se la macchina B appartiene allo stesso dominio della macchina A, il suo indirizzo IP si troverà probabilmente in un file presente sulla macchina A.
  • Altrimenti, la macchina A troverà, in un altro file o nello stesso di prima, un elenco di diversi server dei nomi con i relativi indirizzi IP. Un server dei nomi è responsabile della mappatura del nome di una macchina al suo indirizzo IP. La macchina A invierà una richiesta speciale al primo server dei nomi del suo elenco, nota come query DNS, che include il nome della macchina ricercata. Se il server interrogato ha quel nome nei propri record, invierà l'indirizzo IP corrispondente al computer A. Altrimenti, il server troverà nei propri file un elenco di server dei nomi che può interrogare. Lo farà quindi. In questo modo, verranno interrogati diversi server dei nomi, non a caso ma in modo da ridurre al minimo il numero di richieste. Se il computer viene finalmente trovato, la risposta verrà inviata al computer A.

XDR: (eXternal Data Representation)

Creato da Sun Microsystems, questo protocollo definisce uno standard di rappresentazione dei dati indipendente dalla piattaforma.

RPC: (Remote Procedure Call)

Definito anch&#x27;esso da Sun, è un protocollo di comunicazione tra applicazioni remote, indipendente dal livello di trasporto. Questo protocollo è importante: solleva il programmatore dalla necessità di conoscere i dettagli del livello di trasporto e rende le applicazioni portabili. Questo protocollo si basa sul protocollo XDR

NFS: Network File System

Definito anch&#x27;esso da Sun, questo protocollo permette a una macchina di &quot;vedere&quot; il file system di un&#x27;altra macchina. Si basa sul protocollo RPC descritto sopra.

8.1.9. Conclusione

In questa introduzione abbiamo presentato alcune delle caratteristiche principali dei protocolli Internet. Per approfondire questo argomento, i lettori possono consultare l'eccellente libro di Douglas Comer:

Titolo: TCP/IP: Architettura, protocolli, applicazioni.

Autore: Douglas COMER

Editore: InterEditions

8.2. Gestione degli indirizzi di rete in Java

8.2.1. Definizione

Ogni macchina su Internet è identificata da un indirizzo o un nome univoco. Queste due entità sono gestite in Java dalla classe InetAddress, che include i seguenti metodi:

byte [] getAddress()
restituisce i 4 byte dell'indirizzo IP dell'istanza InetAddress corrente
String getHostAddress()
restituisce l'indirizzo IP dell'istanza InetAddress corrente
String getHostName()
restituisce il nome Internet dell'istanza InetAddress corrente
String toString()
restituisce l'indirizzo IP/il nome Internet dell'istanza InetAddress corrente
InetAddress getByName(String Host)
crea l'istanza InetAddress per la macchina specificata da Host. Genera un'eccezione se Host è sconosciuto. Host può essere il nome Internet di una macchina o il suo indirizzo IP nella forma I1.I2.I3.I4
InetAddress getLocalHost()
crea l'istanza InetAddress del computer su cui è in esecuzione il programma contenente questa istruzione.

8.2.2. Alcuni esempi

8.2.2.1. Identifica la macchina locale


import java.net.*;
 
public class localhost{
  public static void main (String arg[]){
    try{
      InetAddress adresse=InetAddress.getLocalHost();
    byte[] IP=adresse.getAddress();
    System.out.print("IP=");
    int i;
    for(i=0;i<IP.length-1;i++) System.out.print(IP[i]+".");
    System.out.println(IP[i]);
      System.out.println("adresse="+adresse.getHostAddress());
    System.out.println("nom="+adresse.getHostName());
    System.out.println("identité="+adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getLocalHost : "+e);
    }// fin try
  }// fine hand
}// fin class

I risultati dell'esecuzione sono i seguenti:

IP=127.0.0.1
adresse=127.0.0.1
nom=tahe
identité=tahe/127.0.0.1

Ogni macchina ha un indirizzo IP interno, che è 127.0.0.1. Quando un programma utilizza questo indirizzo di rete, si riferisce alla macchina su cui è in esecuzione. Il vantaggio di questo indirizzo è che non richiede una scheda di rete. Ciò significa che è possibile testare i programmi di rete senza essere connessi a una rete. Un altro modo per riferirsi alla macchina locale è utilizzare il nome localhost.

8.2.2.2. Identificazione di qualsiasi macchina


import java.net.*;
 
public class getbyname{
  public static void main (String arg[]){
    String nomMachine;
    // we retrieve the argument
    if(arg.length==0) 
      nomMachine="localhost";
    else nomMachine=arg[0];
    // we try to obtain the machine address
    try{
      InetAddress adresse=InetAddress.getByName(nomMachine);
      System.out.println("IP : "+  adresse.getHostAddress());
      System.out.println("nom : "+ adresse.getHostName());
      System.out.println("identité : "+ adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getByName : "+e);
    }// fin try
  }// fine hand
}// fin class

Con la chiamata Java getByName, otteniamo i seguenti risultati:

IP : 127.0.0.1
nom : localhost
identité : localhost/127.0.0.1

Con la chiamata Java getbyname shiva.istia.univ-angers.fr, otteniamo:

IP : 193.52.43.5
nom : shiva.istia.univ-angers.fr
identité : shiva.istia.univ-angers.fr/193.52.43.5

Con la chiamata Java **getbyname www.ibm.com**, otteniamo:

IP : 204.146.18.33
nom : www.ibm.com
identité : www.ibm.com/204.146.18.33

8.3. Programmazione TCP-IP

8.3.1. Informazioni generali

Image

Quando un'applicazione AppA sul computer A vuole comunicare con un'applicazione AppB sul computer B su Internet, deve conoscere diverse cose:

  • l'indirizzo IP o il nome host del computer B
  • il numero di porta utilizzato dall'applicazione AppB. Questo perché il computer B potrebbe ospitare molte applicazioni che operano su Internet. Quando riceve informazioni dalla rete, deve sapere a quale applicazione sono destinate. Le applicazioni sul computer B accedono alla rete tramite interfacce note anche come porte di comunicazione. Queste informazioni sono contenute nel pacchetto ricevuto dal computer B in modo che possa essere consegnato all'applicazione corretta.
  • I protocolli di comunicazione compresi dal computer B. Nel nostro studio, useremo solo i protocolli TCP-IP.
  • Il protocollo di comunicazione accettato dall'applicazione AppB. Infatti, le macchine A e B "parleranno" tra loro. Ciò che diranno sarà incapsulato all'interno dei protocolli TCP/IP. Tuttavia, quando, alla fine della catena, l'applicazione AppB riceve le informazioni inviate dall'applicazione AppA, deve essere in grado di interpretarle. Ciò è analogo alla situazione in cui due persone, A e B, comunicano per telefono: la loro conversazione è veicolata dal telefono. Il parlato viene codificato come segnali dal telefono A, trasmesso sulle linee telefoniche e ricevuto dal telefono B, dove viene decodificato. La persona B sente quindi il parlato. È qui che entra in gioco il concetto di protocollo di comunicazione: se A parla francese e B non capisce quella lingua, A e B non saranno in grado di comunicare efficacemente.

Pertanto, le due applicazioni che comunicano devono concordare il tipo di dialogo che useranno. Ad esempio, il dialogo con un servizio FTP non è lo stesso di quello con un servizio POP: questi due servizi non accettano gli stessi comandi. Hanno un protocollo di dialogo diverso.

8.3.2. Caratteristiche del protocollo TCP

Qui esamineremo solo le comunicazioni di rete che utilizzano il protocollo di trasporto TCP. Rivediamo le caratteristiche di questo protocollo:

  • Il processo che desidera trasmettere dati stabilisce innanzitutto una connessione con il processo che riceverà le informazioni che sta per trasmettere. Questa connessione viene stabilita tra una porta sulla macchina mittente e una porta sulla macchina ricevente. Viene così creato un percorso virtuale tra le due porte, che sarà riservato esclusivamente ai due processi che hanno stabilito la connessione.
  • Tutti i pacchetti inviati dal processo di origine seguono questo percorso virtuale e arrivano nell'ordine in cui sono stati inviati
  • I dati trasmessi appaiono continui. Il processo mittente invia i dati al proprio ritmo. Questi dati non vengono necessariamente inviati immediatamente: il protocollo TCP attende di averne una quantità sufficiente da inviare. Essi vengono memorizzati in una struttura chiamata segmento TCP. Una volta che questo segmento è pieno, viene trasmesso al livello IP, dove viene incapsulato in un pacchetto IP.
  • Ogni segmento inviato dal protocollo TCP è numerato. Il protocollo TCP ricevente verifica di ricevere i segmenti in sequenza. Per ogni segmento ricevuto correttamente, invia un riconoscimento al mittente.
  • Quando il mittente riceve questo riconoscimento, ne informa il processo di invio. Il processo di invio può così confermare che un segmento è arrivato a destinazione.
  • Se, dopo un certo periodo di tempo, il protocollo TCP che ha inviato un segmento non riceve un riconoscimento, ritrasmette il segmento in questione, garantendo così la qualità del servizio di consegna delle informazioni.
  • Il circuito virtuale stabilito tra i due processi in comunicazione è full-duplex: ciò significa che le informazioni possono fluire in entrambe le direzioni. Pertanto, il processo di destinazione può inviare conferme anche mentre il processo di origine continua a inviare informazioni. Ciò consente, ad esempio, al protocollo TCP di origine di inviare più segmenti senza attendere una conferma. Se, dopo un certo periodo di tempo, si rende conto di non aver ricevuto un riconoscimento per un segmento specifico n. n, riprenderà l’invio dei segmenti da quel punto.

8.3.3. Il rapporto client-server

La comunicazione su Internet è spesso asimmetrica: la macchina A avvia una connessione per richiedere un servizio alla macchina B, specificando che desidera aprire una connessione con il servizio SB1 sulla macchina B. La macchina B accetta o rifiuta. Se accetta, la macchina A può inviare le proprie richieste al servizio SB1. Queste richieste devono essere conformi al protocollo di comunicazione compreso dal servizio SB1. Si instaura così un dialogo richiesta-risposta tra la macchina A, detta macchina client, e la macchina B, detta macchina server. Uno dei due partner chiuderà la connessione.

8.3.4. Architettura client

L'architettura di un programma di rete che richiede i servizi di un'applicazione server sarà la seguente:

ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
        tant que ce n'est pas fini
            préparer une demande
            l'émettre vers la machine B
            attendre et récupérer la réponse
            la traiter
        fin tant que
finsi

8.3.5. Architettura del server

L'architettura di un programma che offre servizi sarà la seguente:

ouvrir le service sur la machine locale
tant que le service est ouvert
        se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
        lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que

Il programma server gestisce la richiesta di connessione iniziale di un client in modo diverso dalle sue successive richieste di servizio. Il programma non fornisce il servizio stesso. Se lo facesse, non sarebbe più in ascolto delle richieste di connessione mentre il servizio è in corso e i client non verrebbero serviti. Proceda quindi in modo diverso: non appena una richiesta di connessione viene ricevuta sulla porta di ascolto e quindi accettata, il server crea un'attività responsabile di fornire il servizio richiesto dal client. Questo servizio viene fornito su un'altra porta della macchina server chiamata porta di servizio. Ciò consente di servire più client contemporaneamente.

Un'attività di servizio avrà la seguente struttura:

tant que le service n'a pas été rendu totalement
        attendre une demande sur le port de service
        lorsqu'il y en a une, élaborer la réponse
        transmettre la réponse via le port de service
fin tant que
libérer le port de service

8.3.6. La classe Socket

8.3.6.1. Definizione

Lo strumento fondamentale utilizzato dai programmi che comunicano via Internet è il socket. Questo termine inglese significa "presa elettrica"; in questo contesto, il suo significato è esteso a "presa di rete". Affinché un'applicazione possa inviare e ricevere informazioni su Internet, ha bisogno di una presa di rete, un socket. Questo strumento è stato originariamente creato nelle versioni Berkeley di Unix. Da allora è stato portato su tutti i sistemi Unix e sull'ambiente Windows. Esiste anche sulle macchine virtuali Java in due forme: la classe Socket per le applicazioni client e la classe ServerSocket per le applicazioni server. Qui spieghiamo alcuni dei costruttori e dei metodi della classe Socket:

public Socket(String host, int port)
apre una connessione remota alla porta port sulla macchina host
public int getLocalPort()
restituisce il numero di porta locale utilizzato dal socket
  
public int getPort()
restituisce il numero della porta remota a cui è connesso il socket
  
public InetAddress getLocalAddress()
restituisce l'InetAddress locale a cui è associato il socket
  
public InetAddress getInetAddress()
restituisce l'InetAddress remoto a cui è associato il socket
  
public InputStream getInputStream()
restituisce un flusso di input utilizzato per leggere i dati inviati dal partner remoto
  
public OutputStream getOutputStream()
restituisce un flusso di output utilizzato per inviare dati al partner remoto
  
public void shutdownInput()
chiude il flusso di input del socket
  
public void shutdownOutput()
chiude il flusso di output del socket
  
public void close()
chiude il socket e i suoi flussi di I/O
  
public String toString()
restituisce una stringa che "rappresenta" il socket
 

8.3.6.2. Stabilire una connessione con un server

Abbiamo visto che affinché la macchina A possa stabilire una connessione con un servizio sulla macchina B, sono necessarie due informazioni:

  • l'indirizzo IP o il nome host della macchina B
  • il numero di porta su cui è in esecuzione il servizio desiderato

Il costruttore

    public Socket(String  host, int  port);

crea un socket e lo connette al computer host sulla porta port. Questo costruttore genera un'eccezione in diversi casi:

  • indirizzo errato
  • porta errata
  • richiesta rifiutata

Dobbiamo gestire questa eccezione:


    Socket  sClient=null;
    try{
        sClient=new Socket(host,port);
    } catch(Exception e){
        // la connexion a échoué - on traite l'erreur
        ….
    }

Se la richiesta di connessione va a buon fine, al client viene assegnata una porta locale per comunicare con il computer B. Una volta stabilita la connessione, è possibile recuperare questa porta utilizzando il metodo:

public int getLocalPort();

Se la connessione va a buon fine, abbiamo visto che, dal suo lato, il server dispone di un altro processo che gestisce il servizio su una cosiddetta porta di servizio. Il numero di questa porta può essere ottenuto utilizzando il metodo:

public int getPort();

8.3.6.3. Invio di informazioni sulla rete

È possibile ottenere un flusso di scrittura sul socket — e quindi sulla rete — utilizzando il metodo:

public OutputStream getOutputStream();

Tutto ciò che viene inviato a questo flusso verrà ricevuto sulla porta di servizio del server. Molte applicazioni utilizzano un'interfaccia testuale costituita da righe di testo seguite da un carattere di nuova riga. Il metodo println è quindi molto utile in questi casi. Convertiamo quindi il flusso di output OutputStream in un flusso PrintWriter, che fornisce il metodo println. La scrittura può generare un'eccezione.

8.3.6.4. Lettura di informazioni dalla rete

È possibile ottenere un flusso di lettura per i dati in arrivo sul socket utilizzando il metodo:

public InputStream getInputStream();

Tutto ciò che viene letto da questo flusso proviene dalla porta di servizio del server. Per le applicazioni con una finestra di dialogo composta da righe di testo terminate da un carattere di nuova riga, è consigliabile utilizzare il metodo readLine. A tal fine, si converte l'InputStream in un BufferedReader, che dispone del metodo readLine(). La lettura potrebbe generare un'eccezione.

8.3.6.5. Chiusura della connessione

Questo viene fatto utilizzando il metodo:

public void close();

Questo metodo può generare un'eccezione. Le risorse utilizzate, in particolare la porta di rete, vengono liberate.

8.3.6.6. Architettura client

Ora disponiamo degli elementi necessari per descrivere l'architettura di base di un client Internet:


    Socket  sClient=null;
    try{
            // connect to the service running on port P of machine M
        sClient=new Socket(M,P);
 
        // create client socket I/O streams
        BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
        PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);
 
        // request-response loop
        boolean  fini=false;
        String demande;
        String réponse;
        while (! fini){
            // preparing the application
            demande=…
            // we send it
            out.println(demande);
            // we read the answer
            réponse=in.readLine();
            // the answer is processed

        }
        // it's over
        sClient.close();
    } catch(Exception e){
        // we handle the exception
        ….
    }

Per mantenere l'esempio semplice, non abbiamo cercato di gestire i vari tipi di eccezioni generate dal costruttore Socket o dai metodi readLine, getInputStream, getOutputStream e close. Tutto è stato consolidato in un'unica eccezione.

8.3.7. La classe ServerSocket

8.3.7.1. Definizione

Questa classe è destinata alla gestione dei socket sul lato server. Di seguito illustriamo alcuni dei costruttori e dei metodi di questa classe:

public ServerSocket(int port)
crea un socket in ascolto sulla porta port
public ServerSocket(int port, int count)
Come sopra, ma imposta la dimensione della coda su count, ovvero il numero massimo di connessioni client che verranno messe in coda se il server è occupato quando arriva la connessione del client.
public int getLocalPort()
restituisce il numero della porta di ascolto utilizzata dal socket
public InetAddress getInetAddress()
Restituisce l'InetAddress locale a cui è associato il socket
public Socket accept()
Mette il server in uno stato di attesa per una connessione (operazione di blocco). All'arrivo di una connessione client, restituisce un socket attraverso il quale verrà fornito il servizio al client.
public void close()
Chiude il socket e i relativi flussi di I/O
public String toString()
Restituisce una stringa che "rappresenta" il socket
public void close()
chiude il socket del servizio e libera le risorse ad esso associate

8.3.7.2. Apertura del servizio

Ciò avviene utilizzando i due costruttori:

public ServerSocket(int  port);    
public ServerSocket(int  port, int  count);

port è la porta di ascolto del servizio: quella a cui i client inviano le loro richieste di connessione. count è la dimensione massima della coda del servizio (50 per impostazione predefinita), che memorizza le richieste di connessione dei client a cui il server non ha ancora risposto. Quando la coda è piena, le richieste di connessione in arrivo vengono rifiutate. Entrambi i costruttori generano un'eccezione.

8.3.7.3. Accettare una richiesta di connessione

Quando un client invia una richiesta di connessione alla porta di ascolto del servizio, il servizio la accetta utilizzando il metodo:

    public Socket accept();

Questo metodo restituisce un'istanza Socket: si tratta del socket del servizio, attraverso il quale verrà fornito il servizio, il più delle volte da un altro thread. Il metodo può generare un'eccezione.

8.3.7.4. Lettura/scrittura tramite il socket del servizio

Poiché il socket di servizio è un'istanza della classe Socket, fare riferimento alle sezioni precedenti in cui è stato trattato questo argomento.

8.3.7.5. Identificazione del client

Una volta ottenuto il socket di servizio, è possibile identificare il client utilizzando il metodo

    public InetAddress getInetAddress()

della classe Socket. Ciò consente di accedere all'indirizzo IP e al nome del client.

8.3.7.6. Chiusura del servizio

Questo viene effettuato utilizzando il metodo

    public void close();

della classe ServerSocket. Questo libera le risorse in uso, in particolare la porta di ascolto. Il metodo potrebbe generare un'eccezione.

8.3.7.7. Architettura di base del server

Sulla base di quanto detto, possiamo scrivere la struttura di base di un server:


SocketServer sEcoute=null;
try{
    // ouverture du service
    int portEcoute=…
    int maxConnexions=…
    sEcoute=new ServerSocket(portEcoute,maxConnexions);
 
    // traitement des demandes de connexion
    boolean fini=false;
    Socket sService=null;
    while( ! fini){
        // attente et acceptation d'une demande
        sService=sEcoute.accept();
 
        // le service est rendu par une autre tâche à laquelle on passe la socket de service
        new Service(sService).start();
 
        // on se remet en attente des demandes de connexion
    }
    // c'est fini - on clôt le service
    sEcoute.close();
} catch (Exception e){
    // on traite l'exception

}

La classe Service è un thread che potrebbe presentarsi in questo modo:


public class Service extends Thread{
 
    Socket sService;        // service socket
 
    // manufacturer
    public Service(Socket S){
        sService=S;
    }
 
// run
public void run(){
    try{
        // create input-output flows
    BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
    PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);
 
    // request-response loop
    boolean  fini=false;
    String demande;
    String réponse;
    while (! fini){
        // we read the request
        demande=in.readLine();
 
        // we treat it 

 
        // we prepare the answer
        réponse=…

        // we send it
        out.println(réponse);
    }
    // it's over
    sService.close();
    } catch(Exception e){
    // we handle the exception
    ….
    }// try
} // run

8.4. Applicazioni

8.4.1. Server Echo

Proponiamo di scrivere un server echo che verrà avviato da una finestra DOS utilizzando il comando:

    java serveurEcho port

Il server funziona sulla porta passata come parametro. Si limita a rinviare al client la richiesta che il client gli ha inviato, insieme all'identità del client (IP+nome). Accetta 2 connessioni nella sua coda. Qui abbiamo tutti i componenti di un server TCP. Il programma è il seguente:

// call: serveurEcho port
// echo server
// returns the line sent to the customer


import java.net.*;
import java.io.*;

public class serveurEcho{
    public final static String syntaxe="Syntaxe : serveurEcho port";
    public final static int nbConnexions=2;

    // main program
    public static void main (String arg[]){

     // is there an argument
     if(arg.length != 1)
        erreur(syntaxe,1);

     // this argument must be integer >0
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[0]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // create the listening socket
     ServerSocket ecoute=null;
     try{
        ecoute=new ServerSocket(port,nbConnexions);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket d'écoute ("+e+")",3);
     }

     // follow-up
     System.out.println("Serveur d'écho lancé sur le port " + port);

     // service loop
     boolean serviceFini=false;
     Socket service=null;
     while (! serviceFini){
         // waiting for a customer
        try{
            service=ecoute.accept();
        } catch (IOException e){
                erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
        }

         // we identify the link
        try{
            System.out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }


         // the service is provided by another task
        new traiteClientEcho(service).start();
     }// end while
    }// fine hand

// error display
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // identifies
    private static String identifie(InetAddress Host){
        // host identification
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// end class


// provides service to an echo server client

class traiteClientEcho extends Thread{

    private Socket service;            // service socket
    private BufferedReader in;        // iNPUTS
    private PrintWriter out;            // output flow

     // manufacturer
    public traiteClientEcho(Socket service){
        this.service=service;
    }

     // run method
    public void run(){

         // creation of input and output flows
        try{
            in=new BufferedReader(new InputStreamReader(service.getInputStream()));
        } catch (IOException e){
                erreur("Erreur lors de la création du flux déentrée de la socket de service ("+e+")",1);
        }// fin try
        try{
            out=new PrintWriter(service.getOutputStream(),true);
        } catch (IOException e){
                erreur("Erreur lors de la création du flux de sortie de la socket de service ("+e+")",1);
        }// fin try

         // link identification is sent to the customer
        try{
            out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }

         // loop read request/write response
        String demande,reponse;
        try{
             // the service stops when the client sends an end-of-file marker
            while ((demande=in.readLine())!=null){
                // echo of demand
                reponse="["+demande+"]";
                out.println(reponse);
                 // service stops when client sends "end
                if(demande.trim().toLowerCase().equals("fin")) break;
            }// end while
        } catch (IOException e){
                erreur("Erreur lors des échanges client/serveur ("+e+")",3);
        }// fin try

         // close the socket
        try{
            service.close();
        } catch (IOException e){
            erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
        }// fin try
    }// end run

     // error display
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }// end error

     // identifies
    private String identifie(InetAddress Host){
         // host identification
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// fin class

Le due classi necessarie per il servizio sono state unite in un unico file sorgente. Solo una di esse, quella contenente la funzione main, ha l'attributo public. La struttura del server è conforme all'architettura generale dei server TCP. È stato aggiunto un metodo (identify) per identificare la connessione tra il server e un client. Ecco alcuni risultati:

Il server viene avviato dal comando

    java serveurEcho 187

Viene quindi visualizzato il seguente messaggio nella finestra della console:

Serveur d'écho lancé sur le port 187

Per testare questo server, utilizziamo il programma telnet, disponibile sia su Unix che su Windows. Telnet è un client TCP universale adatto a tutti i server che accettano righe di testo terminate da un carattere di fine riga nella loro comunicazione. Questo è il caso del nostro server echo. Avviamo un primo client telnet su Windows (2000 in questo esempio) digitando telnet in una finestra DOS:


DOS>telnet
Microsoft (R) Windows 2000 (TM) version 5.00 (numéro 2195)
Client Telnet Microsoft
Client Telnet numéro 5.00.99203.1
 
Le caractère d'escapement is 'CTRL+$'
 
Microsoft Telnet> help
 
Les commandes peuvent être abrégées. Les commandes prises en charge sont :
 
close           ferme la connexion en cours
display         affiche les paramètres d'operation
open            ouvre une connexion à un site
quit            quitte telnet
set             définit les options (entrez 'set ?' to display the list)
status          affiche les informations d'status
unset           annule les options (entrez 'unset ?' to display the list)
? ou help       affiche des informations d'help
 
Microsoft Telnet> set ?
NTLM            Active l'authentication NTLM.
LOCAL_ECHO      Active l'local echo.
TERM x          (où x est ANSI, VT100, VT52 ou VTNT))
CRLF            Envoi de CR et de LF
 
Microsoft Telnet> set local_echo
 
Microsoft Telnet> open localhost 187

Per impostazione predefinita, il programma Telnet non ripete i comandi digitati sulla tastiera. Per abilitare questa funzione, immettere il comando:

Microsoft Telnet> set local_echo

Per aprire una connessione al server, specificando la porta del servizio echo (187) e l'indirizzo del computer su cui è in esecuzione (localhost), digitare il seguente comando:

Microsoft Telnet> open localhost 187

Nella finestra DOS del client, verrà quindi visualizzato il messaggio:

Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

Nella finestra del server appare il seguente messaggio:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

In questo caso, tahe e localhost si riferiscono alla stessa macchina. Nella finestra del client Telnet è possibile digitare righe di testo. Il server le ripete:

Client [127.0.0.1,tahe,1059] connectÚ au serveur [127.0.0.1,tahe,187]
je suis là
[je suis là]
au revoir
[au revoir]

Si noti che la porta client (1059) viene rilevata correttamente, ma la porta del servizio (187) è identica alla porta di ascolto (187), il che è inaspettato. Ci si aspetterebbe infatti di ottenere la porta del socket del servizio piuttosto che la porta di ascolto. Dovremmo verificare se otteniamo gli stessi risultati su Unix. Ora, avviamo un secondo client telnet. La finestra del server diventa:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]

Nella finestra del secondo client, è anche possibile digitare righe di testo:

Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]

Ciò dimostra che il server echo può servire più client contemporaneamente. I client Telnet possono essere chiusi chiudendo la finestra DOS in cui sono in esecuzione.

8.4.2. Un client Java per il server echo

Nella sezione precedente abbiamo utilizzato un client Telnet per testare il servizio echo. Ora scriveremo il nostro client:

// call: clientEcho machine port
// echo server client
// sends lines to the server, which echoes them back to the server

import java.net.*;
import java.io.*;

public class clientEcho{
    public final static String syntaxe="Syntaxe : clientEcho machine port";

    // main program    
    public static void main (String arg[]){

     // are there two arguments
     if(arg.length != 2)
        erreur(syntaxe,1);

     // the first argument must be the name of an existing machine
    String machine=arg[0];
    InetAddress serveurAddress=null;
    try{
        serveurAddress=InetAddress.getByName(machine);
    } catch (Exception e){
        erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
    }

     // port must be integer >0
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[1]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\nPort incorrect ("+E+")",3);

     // connect to the server
     Socket sClient=null;
     try{
        sClient=new Socket(machine,port);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket de communication ("+e+")",4);
     }

     // we identify the link
    try{
        System.out.println("Client : Client ["+identifie(InetAddress.getLocalHost())+","+
        sClient.getLocalPort()+"] connecté au serveur [" + identifie (sClient.getInetAddress())
        + "," + sClient.getPort() + "]");
    } catch (Exception e) {
        erreur("identification liaison ("+e+")",5);
    }

     // creation of a flow for reading lines typed on the keyboard
    BufferedReader IN=null;
    try{
        IN=new BufferedReader(new InputStreamReader(System.in));
    } catch (Exception e){
        erreur("Création du flux d'entrée clavier ("+e+")",6);
    }
     // creation of the input stream associated with the client socket
    BufferedReader in=null;
    try{
        in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
    } catch (Exception e){
        erreur("Création du flux d'entrée de la socket client("+e+")",7);
    }
     // creation of the output stream associated with the client socket
    PrintWriter out=null;
    try{
        out=new PrintWriter(sClient.getOutputStream(),true);
    } catch (Exception e){
        erreur("Création du flux de sortie de la socket ("+e+")",8);
    }

     // request-response loops
    boolean serviceFini=false;
    String demande=null;
    String reponse=null;

    // we read the message sent by the server just after connection 
    try{
        reponse=in.readLine();
    } catch (IOException e){
            erreur("Lecture réponse ("+e+")",4);
    }        

     // response display
    System.out.println("Serveur : " +reponse);

    while (! serviceFini){
         // read a line typed on the keyboard
        System.out.print("Client : ");
        try{
            demande=IN.readLine();
        } catch (Exception e){
            erreur("Lecture ligne ("+e+")",9);
        }
         // sending demand on the network
        try{
            out.println(demande);
        } catch (Exception e){
            erreur("Envoi demande ("+e+")",10);
        }
         // wait/read answer
        try{
            reponse=in.readLine();
        } catch (IOException e){
                erreur("Lecture réponse ("+e+")",4);
        }
         // response display
        System.out.println("Serveur : " +reponse);
         // is it over?
        if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
    }
     // it's over
    try{
        sClient.close();
    } catch(Exception e){
        erreur("Fermeture socket ("+e+")",11);
    }
}// hand

// error display
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // identifies
    private static String identifie(InetAddress Host){
        // host identification
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// fin class

La struttura di questo client è conforme all'architettura generale dei client TCP. In questo caso, abbiamo gestito una per una le varie eccezioni possibili, il che appesantisce il programma. Ecco i risultati ottenuti durante il test di questo client:

Client : Client [127.0.0.1,tahe,1045] connecté au serveur [127.0.0.1,localhost,187]
Serveur : Client [127.0.0.1,localhost,1045] connectÚ au serveur [127.0.0.1,tahe,187]
Client : 123
Serveur : [123]
Client : abcd
Serveur : [abcd]
Client : je suis là
Serveur : [je suis là]
Client : fin
Serveur : [fin]

Le righe che iniziano con Client sono quelle inviate dal client, mentre quelle che iniziano con Server sono quelle restituite dal server.

8.4.3. Un client TCP generico

Molti servizi creati agli albori di Internet funzionano secondo il modello echo server discusso in precedenza: la comunicazione client-server consiste nello scambio di righe di testo. Scriveremo un client TCP generico che verrà avviato come segue: java cltTCPgenerique server port

Questo client TCP si connetterà alla porta port sul server server. Una volta connesso, creerà due thread:

  1. un thread responsabile della lettura dei comandi digitati sulla tastiera e del loro invio al server
  2. un thread responsabile della lettura delle risposte del server e della loro visualizzazione sullo schermo

Perché due thread quando non era necessario nell'applicazione precedente? In quell'applicazione, il protocollo di comunicazione era fisso: il client inviava una singola riga e il server rispondeva con una singola riga. Ogni servizio ha il proprio protocollo specifico e ci imbattiamo anche nelle seguenti situazioni:

  • il client deve inviare diverse righe di testo prima di ricevere una risposta
  • la risposta di un server può contenere più righe di testo

Pertanto, il ciclo che invia una singola riga al server e riceve una singola riga dal server non è sempre adatto. Creeremo quindi due cicli separati:

  • un ciclo per leggere i comandi digitati sulla tastiera da inviare al server. L'utente segnalerà la fine dei comandi con la parola chiave "fin".
  • un ciclo per ricevere e visualizzare le risposte del server. Si tratterà di un ciclo infinito che verrà interrotto solo dalla chiusura della connessione di rete da parte del server o dalla digitazione del comando "end" da parte dell'utente sulla tastiera.

Per avere questi due cicli separati, abbiamo bisogno di due thread indipendenti. Vediamo un esempio di esecuzione in cui il nostro client TCP generico si connette a un servizio SMTP (Simple Mail Transfer Protocol). Questo servizio è responsabile dell'inoltro delle e-mail ai destinatari. Opera sulla porta 25 e utilizza un protocollo di scambio basato su testo.


Dos>java clientTCPgenerique istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test

ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Commentiamo questi scambi client-server:

  • Il servizio SMTP invia un messaggio di benvenuto quando un client si connette ad esso:
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
  • Alcuni servizi dispongono di un comando "help" che fornisce informazioni sui comandi disponibili per quel servizio. In questo caso non è così. I comandi SMTP utilizzati nell'esempio sono i seguenti:
    • mail from: mittente, per specificare l'indirizzo e-mail del mittente
    • rcpt to: destinatario, per specificare l'indirizzo e-mail del destinatario del messaggio. Se ci sono più destinatari, il comando rcpt to: viene ripetuto tante volte quante necessarie per ciascun destinatario.
    • data, che segnala al server SMTP che il messaggio sta per essere inviato. Come indicato nella risposta del server, questo consiste in una serie di righe che terminano con una riga contenente solo un punto. Un messaggio può avere intestazioni separate dal corpo del messaggio da una riga vuota. Nel nostro esempio, abbiamo incluso un oggetto utilizzando la parola chiave Subject:
  • Una volta inviato il messaggio, possiamo comunicare al server che abbiamo terminato utilizzando il comando quit. Il server chiude quindi la connessione di rete. Il thread di lettura può rilevare questo evento e arrestarsi.
  • L'utente digita quindi "end" sulla tastiera per arrestare anche il thread che legge i comandi digitati sulla tastiera.

Se controlliamo l'e-mail ricevuta, vediamo quanto segue (Outlook):

Image

Si noti che il servizio SMTP non è in grado di rilevare se un mittente sia valido o meno. Pertanto, non ci si può mai fidare del campo "Da" di un messaggio. In questo caso, il mittente machin@univ-angers.fr non esisteva.

Questo client TCP generico ci permette di scoprire il protocollo di comunicazione dei servizi Internet e, da lì, creare classi specializzate per i client di questi servizi. Esploriamo il protocollo di comunicazione del servizio POP (Post Office Protocol), che ci permette di recuperare le email memorizzate su un server. Opera sulla porta 110.


Dos> java clientTCPgenerique istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<--     by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<--     Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<--     by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<--     Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2                  Tl.  : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 ,  Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

I comandi principali sono i seguenti:

  • user login, dove si inserisce il proprio login sulla macchina che ospita le proprie e-mail
  • pass password, dove si inserisce la password associata al login precedente
  • list, per ottenere un elenco dei messaggi nel formato numero, dimensione in byte
  • retr i, per leggere il messaggio numero i
  • quit, per terminare la sessione.

Esploriamo ora il protocollo di comunicazione tra un client e un server web, che in genere funziona sulla porta 80:


Dos> java clientTCPgenerique istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0
 
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
<--
<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise  jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<--
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Un client web invia i propri comandi al server secondo il seguente schema:

commande1
commande2
...
commanden
[ligne vide]

Il server web risponde solo dopo aver ricevuto la riga vuota. In questo esempio, abbiamo utilizzato un solo comando:

GET /index.html HTTP/1.0

che richiede l'URL /index.html dal server e indica che sta utilizzando la versione 1.0 di HTTP. La versione più recente di questo protocollo è la 1.1. L'esempio mostra che il server ha risposto inviando il contenuto del file index.html e poi ha chiuso la connessione, come possiamo vedere dalla lettura della risposta "thread terminating". Prima di inviare il contenuto del file index.html, il server web ha inviato una serie di intestazioni seguite da una riga vuota:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

La riga <html> è la prima riga del file /index.html. Il testo che la precede è chiamato intestazioni HTTP (HyperText Transfer Protocol). Non entreremo nei dettagli su queste intestazioni in questa sede, ma tieni presente che il nostro client generico fornisce l'accesso ad esse, il che può essere utile per comprenderle. Ad esempio, la prima riga:

<-- HTTP/1.1 200 OK

indica che il server web contattato supporta il protocollo HTTP/1.1 e che ha trovato con successo il file richiesto (200 OK), dove 200 è un codice di risposta HTTP. Le righe

<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html

comunicano al client che riceverà 11.251 byte di testo HTML (HyperText Markup Language) e che la connessione verrà chiusa una volta inviati i dati.

Ecco quindi un client TCP molto pratico. Certamente fa meno del programma telnet che abbiamo usato in precedenza, ma è stato interessante scriverlo da soli. Il programma client TCP generico è il seguente:

// imported packages
import java.io.*;
import java.net.*;

public class clientTCPgenerique{

    // receives the characteristics of a service as a parameter in the form
     // server port
     // connects to the service
     // creates a thread to read keyboard commands
     // these will be sent to the
     // creates a thread to read server responses
     // these will be displayed on the screen
     // the whole thing ends with the command end typed on the keyboard

   // instance variable
  private static Socket client;

    public static void main(String[] args){

         // syntax
        final String syntaxe="pg serveur port";

         // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the server name
        String serveur=args[0];

         // port must be integer >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[1]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

        client=null;
         // there may be problems
        try{
             // connect to the service
            client=new Socket(serveur,port);
        }catch(Exception ex){
             // error
            erreur("Impossible de se connecter au service ("+ serveur
                +","+port+"), erreur : "+ex.getMessage(),3);
             // end
            return;
        }//catch

         // create read/write threads
    new ClientSend(client).start();
    new ClientReceive(client).start();

        // end thread main
        return;
    }// hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class  

class ClientSend extends Thread {
    // class for reading keyboard commands
     // and send them to a server via a tcp client passed in parameter

    private Socket client;    // tcp client

     // manufacturer
    public ClientSend(Socket client){
         // we note the tcp client
        this.client=client;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        PrintWriter OUT=null;            // network write streams
    BufferedReader IN=null;        // keyboard flow
        String commande=null;            // command read from keyboard

         // error management
        try{
             // network write stream creation
            OUT=new PrintWriter(client.getOutputStream(),true);
      // keyboard input stream creation
      IN=new BufferedReader(new InputStreamReader(System.in));
            // order entry-send loop
            System.out.println("Commandes : ");
            while(true){
                 // read command typed on keyboard
                commande=IN.readLine().trim();
                // finished?
                if (commande.toLowerCase().equals("fin")) break;
                // send order to server
                OUT.println(commande);
                 // next order
            }//while
        }catch(Exception ex){
             // error
            System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
    }//run
}//class

class ClientReceive extends Thread{
    // class responsible for reading lines of text intended for a 
     // tcp client passed as parameter

    private Socket client;    // tcp client

     // manufacturer
    public ClientReceive(Socket client){
         // we note the tcp client
        this.client=client;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        BufferedReader IN=null;        // network read stream
        String réponse=null;        // server response

         // error management
        try{
             // create network read stream
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // loop read text lines from IN stream
            while(true){
                 // network streaming
                réponse=IN.readLine();
                 // closed flow?
                if(réponse==null) break;
                // display
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // error
            System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
    }//run
}//class

8.4.4. Un server TCP generico

Ora vedremo un server

  • che visualizza sullo schermo i comandi inviati dai propri client
  • e invia loro, in risposta, le righe di testo digitate sulla tastiera da un utente. È quindi l’utente a fungere da server.

Il programma viene avviato con: java genericTCPserver listeningPort, dove listeningPort è la porta a cui i client devono connettersi. Il servizio client sarà gestito da due thread:

  • un thread dedicato esclusivamente alla lettura delle righe di testo inviate dal client
  • un thread dedicato esclusivamente alla lettura delle risposte digitate dall'utente. Questo thread segnalerà, utilizzando il comando `fin`, che sta chiudendo la connessione con il client.

Il server crea due thread per ogni client. Se ci sono n client, ci saranno 2n thread attivi contemporaneamente. Il server stesso non si ferma mai a meno che l'utente non prema Ctrl-C sulla tastiera. Vediamo alcuni esempi.

Il server è in esecuzione sulla porta 100 e utilizziamo il client generico per comunicare con esso. La finestra del client ha questo aspetto:


E:\data\serge\MSNET\c#\réseau\client tcp générique> java clientTCPgenerique localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

Le righe che iniziano con <-- sono quelle inviate dal server al client; le altre provengono dal client al server. La finestra del server è la seguente:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]

Le righe che iniziano con <-- sono quelle inviate dal cliente al server. Le righe N: sono quelle inviate dal server al cliente N. Il server sopra indicato è ancora attivo anche se il cliente 1 ha terminato. Avviamo un secondo cliente per lo stesso server:


Dos> java clientTCPgenerique localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

La finestra del server appare quindi così:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- commande 3 du client 2
réponse 3 au client 2
2 : [fin du Thread de lecture des demandes du client 2]
fin
[fin du Thread de lecture des réponses du serveur au client 2]
^C

Ora simuliamo un server web avviando il nostro server generico sulla porta 88:


Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88

Ora apriamo un browser e richiediamo l'URL http://localhost:88/exemple.html. Il browser si connetterà quindi alla porta 88 sul computer localhost e richiederà la pagina /example.html:

Image

Ora diamo un'occhiata alla finestra del nostro server:

Dos>java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Questo mostra le intestazioni HTTP inviate dal browser. Ciò ci permette di imparare gradualmente il protocollo HTTP. In un esempio precedente, abbiamo creato un client web che inviava solo il comando GET. Era sufficiente. Qui vediamo che il browser invia ulteriori informazioni al server. Queste informazioni hanno lo scopo di comunicare al server con quale tipo di client ha a che fare. Notiamo inoltre che le intestazioni HTTP terminano con una riga vuota.

Creiamo una risposta per il nostro client. L'utente alla tastiera è il vero e proprio server in questo caso e può creare manualmente una risposta. Ricordiamo la risposta inviata da un server web in un esempio precedente:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

Proviamo a fornire una risposta simile:

...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]

Le righe che iniziano con 2: vengono inviate dal server al client n. 2. Il comando end chiude la connessione dal server al client. Nella nostra risposta, ci siamo limitati alle seguenti intestazioni HTTP:

HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :

Non specifichiamo la dimensione del file che stiamo inviando (Content-Length), ma indichiamo semplicemente che chiuderemo la connessione (Connection: close) dopo l'invio. Questo è sufficiente per il browser. Non appena rileva la chiusura della connessione, capirà che la risposta del server è completa e visualizzerà la pagina HTML che gli è stata inviata. La pagina è la seguente:

2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>

L'utente chiude quindi la connessione al client digitando il comando "fin". Il browser capisce così che la risposta del server è completa e può visualizzarla:

Image

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

Image

cioè esattamente ciò che è stato inviato dal server generico.

Il codice per il server TCP generico è il seguente:

// packages
import java.io.*;
import java.net.*;

public class serveurTCPgenerique{

    // main program
    public static void main (String[] args){

    // receives the port of listening to customer requests
     // creates a thread to read client requests
     // these will be displayed on the screen
     // creates a thread to read keyboard commands
     // these will be sent as a reply to the customer
     // the whole thing ends with the command end typed on the keyboard

    final String syntaxe="Syntaxe : pg port";
   // instance variable
         // is there an argument
     if(args.length != 1)
        erreur(syntaxe,1);

         // port must be integer >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // we create the listening service
    ServerSocket ecoute=null;
    int nbClients=0;    // no. of customers handled
        try{
             // create the service
            ecoute=new ServerSocket(port);
             // follow-up
            System.out.println("Serveur générique lancé sur le port " + port);

             // customer service loop
            Socket client=null;
            while (true){ // infinite loop - will be stopped by Ctrl-C
                 // waiting for a customer
                client=ecoute.accept();

                 // the service is provided by separate threads
                nbClients++;

                 // create read/write threads
        new ServeurSend(client,nbClients).start();
        new ServeurReceive(client,nbClients).start();

                // back to listening to requests
            }// end while
        }catch(Exception ex){
             // we report the error
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }// fine hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

class ServeurSend extends Thread{
    // class responsible for reading typed responses
     // and send them to a client via a tcp client passed to the

    Socket client;    // tcp client
    int numClient;        // customer no

     // manufacturer
    public ServeurSend(Socket client, int numClient){
         // we note the tcp client
        this.client=client;
         // and its
        this.numClient=numClient;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        PrintWriter OUT=null;        // network write streams
        String réponse=null;        // answer read from keyboard
    BufferedReader IN=null;    // keyboard flow

        // follow-up
        System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
         // error management
        try{
             // network write stream creation
            OUT=new PrintWriter(client.getOutputStream(),true);
      // keyboard flow creation
      IN=new BufferedReader(new InputStreamReader(System.in));
            // order entry-send loop
            while(true){
                 // customer identification
                System.out.print("--> " + numClient + " : ");
                 // read response typed on keyboard
                réponse=IN.readLine().trim();
                // finished?
                if (réponse.toLowerCase().equals("fin")) break;
                // send response to server
                OUT.println(réponse);
                 // following response
            }//while
        }catch(Exception ex){
             // error
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
    }//run
}//class

class ServeurReceive extends Thread{
    // class responsible for reading text lines sent to the server 
     // via a tcp client passed to the builder

    Socket client;    // tcp client
    int numClient;        // customer no

     // manufacturer
    public ServeurReceive(Socket client, int numClient){
         // we note the tcp client
        this.client=client;
         // and its
        this.numClient=numClient;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        BufferedReader IN=null;        // network read stream
        String réponse=null;        // server response

         // follow-up
        System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
         // error management
        try{
             // create network read stream
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // loop read text lines from IN stream
            while(true){
                 // network streaming
                réponse=IN.readLine();
                 // closed flow?
                if(réponse==null) break;
                // display
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // error
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
    }//run
}//class

8.4.5. Un client Web

Nell'esempio precedente abbiamo visto alcune delle intestazioni HTTP inviate da un browser:

<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Scriveremo un client web che accetta un URL come parametro e visualizza il contenuto di quell'URL sullo schermo. Supporremo che il server web contattato per l'URL supporti il protocollo HTTP 1.1. Tra le intestazioni elencate sopra, useremo solo le seguenti:

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • La prima intestazione indica quale pagina vogliamo
  • il secondo specifica quale server stiamo interrogando
  • La terza indica che vogliamo che il server chiuda la connessione dopo averci risposto.

Se nell'esempio sopra riportato sostituiamo GET con HEAD, il server ci invierà solo le intestazioni HTTP e non la pagina HTML.

Il nostro client web verrà chiamato nel modo seguente: java clientweb URL cmd, dove URL è l'URL desiderato e cmd è una delle due parole chiave GET o HEAD per indicare se vogliamo solo le intestazioni (HEAD) o anche il contenuto della pagina (GET). Vediamo un primo esempio. Avviamo il server IIS e poi il client web sulla stessa macchina:


dos>java clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private

La risposta

HTTP/1.1 302 Object moved

indica che la pagina richiesta è stata spostata (ovvero, il suo URL è cambiato). Il nuovo URL è fornito dall'intestazione Location:

Location: /IISSamples/Default/welcome.htm

Se utilizziamo GET invece di HEAD nella chiamata del client Web:


dos>java clientweb http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:33:36 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=IMFNCCMDAKPNNGMGMFIHENFE; path=/
Cache-control: private
 
<head><title>L'objet a changé d'emplacement</title></head>
<body><h1>L'objet a changé d'emplacement</h1>Cet objet peut être trouvé <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

Otteniamo lo stesso risultato di HEAD, più il corpo della pagina HTML. Il programma è il seguente:

// imported packages
import java.io.*;
import java.net.*;

public class clientweb{

    // requests a URL
     // displays its contents on the screen

    public static void main(String[] args){
        // syntax
        final String syntaxe="pg URI GET/HEAD";

        // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the URI required
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // URI validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // order verification
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // incorrect order
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // extract useful information from URL
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
        try{
             // connect to the server
            client=new Socket(host,port);

            // create customer input/output flows TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // request URL - send HTTP headers
            OUT.println(commande + " " + path + query + " HTTP/1.1");
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // we read the answer
            while((réponse=IN.readLine())!=null){
                 // the answer is processed
                System.out.println(réponse);
            }//while
             // it's over
            client.close();
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

L'unica novità di questo programma è l'uso della classe URL. Il programma riceve un URL (Uniform Resource Locator) o URI (Uniform Resource Identifier) nella forma http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... La classe URL ci permette di scomporre la stringa URL nei suoi vari componenti. Un oggetto URL viene costruito dalla stringa URL ricevuta come parametro:

        // vérification validité de l'URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
            // URI incorrecte
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch

Se la stringa URL ricevuta come parametro non è un URL valido (protocollo mancante, server, ecc.), viene generata un'eccezione. Questo ci permette di verificare la validità del parametro ricevuto. Una volta costruito l'oggetto URL, abbiamo accesso ai suoi vari elementi. Pertanto, se l'oggetto URL del codice precedente fosse stato costruito dalla stringa

http://serveur:port/cheminPageHTML?param1=val1;param2=val2;... 

avremo:

url.getHost() = server

url.getPort()=porta o -1 se la porta non è specificata

url.getPath() = HTMLPagePath o una stringa vuota se non c'è alcun percorso

url.getQuery() = param1=val1;param2=val2;... oppure null se non c'è query

uri.getProtocol() = http

8.4.6. Gestione dei reindirizzamenti da parte del client Web

Il client web precedente non gestisce alcun reindirizzamento dell'URL richiesto. Il client seguente lo gestisce.

  1. Legge la prima riga delle intestazioni HTTP inviate dal server per verificare se contiene la stringa "302 Object moved", che indica un reindirizzamento
  2. 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.
  3. Visualizza il resto della risposta del server. Se c'è un reindirizzamento, i passaggi da 1 a 3 vengono ripetuti con il nuovo URL. Il programma non accetta più di un reindirizzamento. Questo limite è definito da una costante che può essere modificata.

Ecco un esempio:

Dos>java clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private

<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781

<html>

<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
</html>

Il programma è il seguente:

// imported packages
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientweb2{

    // requests a URL
     // displays its contents on the screen

    public static void main(String[] args){
        // syntax
        final String syntaxe="pg URL GET/HEAD";

        // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the URI required
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // URI validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // order verification
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // incorrect order
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
        final int nbRedirsMax=1;                // no more than one redirection accepted
        int nbRedirs=0;                                    // number of redirects in progress
        String premièreLigne;                        // 1st line of the answer
        boolean redir=false;                        // indicates redirection or not
        String locationString="";                // the URL string of a possible redirection

         // regular expression to find a URL redirect
        Pattern location=Pattern.compile("^Location: (.+?)$");

         // error management
        try{
             // you may have several URL to request if there are redirections
            while(nbRedirs<=nbRedirsMax){

                // extract useful information from URL
            String protocol=url.getProtocol();
            String path=url.getPath();
            if(path.equals("")) path="/";
            String query=url.getQuery();
            if(query!=null) query="?"+query; else query="";
            String host=url.getHost();
            int port=url.getPort();
            if(port==-1) port=url.getDefaultPort();

                 // connect to the server
                client=new Socket(host,port);

                // create customer input/output flows TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // request URL - send HTTP headers
                OUT.println(commande + " " + path + query + " HTTP/1.1");    
                OUT.println("Host: " + host + ":" + port);
                OUT.println("Connection: close");
                OUT.println();

                 // read the first line of the answer
                premièreLigne=IN.readLine();
                 // screen echo
                System.out.println(premièreLigne);

                // redirection?
        if(premièreLigne.endsWith("302 Object moved")){     
                    // there is a redirection
                    redir=true;
                    nbRedirs++;
                }//if

                // next HTTP headers until you find the empty line signalling the end of the headers
                boolean locationFound=false;
                while(!(réponse=IN.readLine()).equals("")){
                    // the answer is displayed
                    System.out.println(réponse);
                     // if there is a redirection, we search for the Location header
                    if(redir && ! locationFound){
                        // compare the line with the relational expression location
                        Matcher résultat=location.matcher(réponse);
                        if(résultat.find()){
                            // if found, note the URL of redirection
                            locationString=résultat.group(1);             
                             // we note that we found
                            locationFound=true;
                        }//if
                    }//if
                    // next header
                }//while

                 // following lines of the answer
                System.out.println(réponse);
                while((réponse=IN.readLine())!=null){
                     // the answer is displayed
                    System.out.println(réponse);
                }//while
                 // close the connection
                client.close();
                 // are we done?
                if ( ! locationFound || nbRedirs>nbRedirsMax)
                    break;
                 // there is a redirection to be made - the new URL is built
                URLString=protocol +"://"+host+":"+port+locationString;
                url=new URL(URLString);
                // follow-up
                System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
            }//while
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

8.4.7. Server di calcolo delle imposte

Stiamo riesaminando l'esercizio TAXES, che è già stato trattato in varie forme. Rivediamo l'ultima versione:

È stata creata una classe base denominata **impots**. I suoi attributi sono tre array di numeri:

public class impots{

  // data required for tax calculation
   // come from an external source

  protected double[] limites=null;
  protected double[] coeffR=null;
  protected double[] coeffN=null;

   // empty builder
  protected impots(){}

   // manufacturer
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{

La classe impots ha due costruttori:

  • un costruttore che accetta i tre array di dati necessari per calcolare l'imposta
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
  • un costruttore senza parametri utilizzabile solo dalle classi figlie
  protected impots(){}

La classe **impotsJDBC** è stata derivata da questa classe, consentendo di popolare i tre array limites*, *coeffR* e coeffN* con i contenuti di un database:

public class impotsJDBC extends impots{
  // addition of a constructor for building
   // limit tables, coeffr, coeffn from table
   // database taxes
  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

    // dsnIMPOTS: DSN database name
     // userIMPOTS, mdpIMPOTS: database login/password

Era stata scritta un'applicazione grafica. L'applicazione utilizzava un oggetto della classe impotsJDBC. L'applicazione e questo oggetto si trovavano sulla stessa macchina. Proponiamo di collocare il programma di test e l'oggetto impotsJDBC su macchine diverse. Avremo un'applicazione client-server in cui l'oggetto impotsJDBC remoto fungerà da server. La nuova classe si chiama TaxServer ed è derivata dalla classe impotsJDBC:

// imported packages
import java.net.*;
import java.io.*;
import java.sql.*;

public class ServeurImpots extends impotsJDBC {

    // attributes
    int portEcoute;                // the ability to listen to customer requests
    boolean actif;                // server status

     // manufacturer
    public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
      throws IOException, SQLException, ClassNotFoundException {
       // parent construction
        super(DSNimpots, USERimpots, MDPimpots);
         // we note the listening port
        this.portEcoute=portEcoute;
         // currently inactive
        actif=false;
         // creates and launches a thread for reading keyboard commands
         // the server will be managed using these commands
        Thread admin=new Thread(){
        public void run(){
          try{
            admin();
        }catch (Exception ignored){}
      }
    };
    admin.start();
    }//ServeurImpots

L'unico nuovo parametro nel costruttore è la porta utilizzata per ascoltare le richieste dei client. Gli altri parametri vengono passati direttamente alla classe base impotsJDBC. Il server fiscale è controllato da comandi immessi tramite la tastiera. Pertanto, creiamo un thread per leggere questi comandi. Ci saranno due comandi possibili: start per avviare il servizio e stop per arrestarlo definitivamente. Il metodo admin che gestisce questi comandi è il seguente:

    public void admin() throws IOException{
        // reads server administration commands typed from the keyboard
         // in an endless loop
        String commande=null;
    BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
        while(true){
             // invite
            System.out.print("Serveur d'impôts>");
             // read command
            commande=IN.readLine().trim().toLowerCase();
             // order execution
            if(commande.equals("start")){
                // active?
                if(actif){
                     //error
                    System.out.println("Le serveur est déjà actif");
                     // we continue
                    continue;
                }//if
                 // create and launch the listening service
                Thread ecoute=new Thread(){
                public void run(){
                  ecoute();
              }
            };
            ecoute.start();
            }//if
            else if(commande.equals("stop")){
                // end of all execution threads
                System.exit(0);
            }//if
            else {
                // error
                System.out.println("Commande incorrecte. Utilisez (start,stop)");
            }//if
        }//while
    }//admin

Se il comando immesso tramite la tastiera è start, viene avviato un thread che ascolta le richieste dei client. Se il comando immesso è stop, tutti i thread vengono arrestati. Il thread di ascolto esegue il metodo listen:

    public void ecoute(){
         // thread for listening to customer requests
         // we create the listening service
        ServerSocket ecoute=null;
        try{
            // create the service
            ecoute=new ServerSocket(portEcoute);
             // follow-up
            System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);

             // service loop
            Socket liaisonClient=null;
            while (true){ // infinite loop
                 // waiting for a customer
                liaisonClient=ecoute.accept();

                 // the service is provided by another task
                new traiteClientImpots(liaisonClient,this).start();

                 // back to listening to requests
            }// end while
        }catch(Exception ex){
             // we report the error
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }//listening thread

Qui abbiamo un classico server TCP in ascolto sulla porta portEcoute. Le richieste dei client vengono gestite dal metodo run del thread traiteCientImpots, al quale vengono passati due parametri al momento della creazione:

  1. l'oggetto Socket liaisonClient, che consentirà l'accesso al client
  2. l'oggetto impotsJDBC, che fornisce l'accesso al metodo this.calculate per il calcolo dell'imposta.
// -------------------------------------------------------
// provides service to a tax server client

class traiteClientImpots extends Thread{

    private Socket liaisonClient;            // customer liaison
    private BufferedReader IN;                // iNPUTS
    private PrintWriter OUT;                    // output flow
    private impotsJDBC objImpots;            // object Tax

         // manufacturer
    public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
        this.liaisonClient=liaisonClient;
        this.objImpots=objImpots;
    }//manufacturer

Il metodo run elabora le richieste dei clienti. Si tratta di righe di testo che possono assumere due forme:

  1. sposato(sì/no) numeroFigli stipendioAnnuale
  2. endCalculation

Il modulo 1 calcola l'imposta, mentre il modulo 2 chiude la connessione client-server.

     // run method
    public void run(){
         // renders service to the customer
        try{
             // iNPUTS
            IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
             // output flow
            OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
             // send a welcome message to the customer
            OUT.println("Bienvenue sur le serveur d'impôts");

             // loop read request/write response
            String demande=null;
            String[] champs=null;    // elements of the request
            String commande=null;    // customer order: calculation or fincalculs
            while ((demande=IN.readLine())!=null){
                 // demand is broken down into fields
                champs=demande.trim().toLowerCase().split("\\s+");
                 // two successful applications: calcul and fincalculs
                commande=champs[0];
                if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
                     // customer error
                    OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
                     // next order
                    continue;
                }//if
                if(commande.equals("calcul")) calculerImpôt(champs);
                if(commande.equals("fincalculs")){
                     // good-bye message to customer
                    OUT.println("Au revoir...");
                     // freeing up resources
                    try{ OUT.close();IN.close();liaisonClient.close();}
                    catch(Exception ex){}
                     // end
                    return;
                }//if
                 //following request
            }//while
        }catch (Exception e){
            erreur("L'erreur suivante s'est produite ("+e+")",2);
        }// fin try
    }// end Run

Il calcolo dell'imposta viene eseguito dal metodo calculateTax, che accetta come parametro l'array dei campi della richiesta del cliente. Viene verificata la validità della richiesta e, se valida, l'imposta viene calcolata e restituita al cliente.

     // tax calculation
    public void calculerImpôt(String[] champs){
         // processing the application: calculation married nbEnfants salaireAnnuel
         // broken down into fields in the fields table

        String marié=null;
        int nbEnfants=0;
        int salaireAnnuel=0;

         // validity of arguments
        try{
             // at least 4 fields are required
            if(champs.length!=4) throw new Exception();
            // married
            marié=champs[1];
            if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
            // children
            nbEnfants=Integer.parseInt(champs[2]);
             // salary
            salaireAnnuel=Integer.parseInt(champs[3]);
        }catch (Exception ignored){
            // format error
            OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
             // finish
            return;
        }//if
         // tax can be calculated
        long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
         // we send the response to the customer
        OUT.println(""+impot);
    }//calculate

Un programma di prova potrebbe essere simile a questo:

// call: serveurImpots port dsnImpots userImpots mdpImpots

import java.io.*;

public class testServeurImpots{
    public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";

    // main program
    public static void main (String[] args){

        // you need 4 arguments
        if(args.length != 4)
            erreur(syntaxe,1);

         // port must be integer >0
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

         // we create the tax server
        try{
            new ServeurImpots(port,args[1],args[2],args[3]);
        }catch(Exception ex){
             //error
            System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
        }//catch
    }//Main

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}// end class

Passiamo al programma di test i dati necessari per costruire un oggetto TaxServer, e da lì il programma crea questo oggetto.

Proviamo a eseguirlo per la prima volta:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

Il comando

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots

crea un oggetto TaxServer che non è ancora in ascolto delle richieste dei client. È il comando start digitato sulla tastiera che avvia questo processo di ascolto. Il comando stop arresta il server. Utilizziamo ora un client. Useremo il client generico creato in precedenza. Il server è in esecuzione:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124

Il client generico viene avviato in un'altra finestra DOS:

Dos>java clientTCPgenerique localhost 124
Commandes :
<-- Bienvenue sur le serveur d'impôts

Possiamo vedere che il client ha ricevuto correttamente il messaggio di benvenuto dal server. Inviamo altri comandi:

x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<--  syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Torniamo alla finestra del server per interromperlo:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

8.5. Esercizi

8.5.1. Esercizio 1 - Grafico generico del client TCP

8.5.1.1. Panoramica dell'applicazione

Proponiamo di creare un programma in grado di comunicare via Internet con i principali servizi TCP. Lo chiameremo client TCP generico. Una volta compresa questa applicazione, vi renderete conto che tutti i client TCP sono simili. La finestra del programma ha questo aspetto:

Image

I significati dei vari controlli sono i seguenti:

N.
Nome
Tipo
ruolo
1
TxtRemoteHost
JTextField
nome del computer che fornisce il servizio richiesto
2
TxtPort
JTextField
porta del servizio richiesto
3
TxtSend
JTextField
testo del messaggio da inviare al server da parte del client
4
OptRCLF
OptLF
JCheckBox
pulsanti utilizzati per specificare come terminano le righe nella finestra di dialogo client/server
RCLF: ritorno a capo (#13) + avanzamento riga (#10)
LF: avanzamento riga (#10)
5
LstSuivi
JList
visualizza i messaggi relativi allo stato della comunicazione tra il client e il server
6
LstDialogue
JList
visualizza i messaggi scambiati dal client (->) e dal server (<-)
7
CmdCancel
JButton
nascosto - situato sotto l'elenco della finestra di dialogo - Appare quando la connessione è in corso e consente di interromperla se il server non risponde

Le opzioni di menu disponibili sono le seguenti:

opzione
sottoopzioni
ruolo
Connessione
Connetti
collega il client al server
 
Disconnetti
Chiude la connessione
 
Esci
Esce dal programma
Messaggi
Invia
Invia il messaggio dal controllo TxtSend al server
 
ClearTracking
Cancella l'elenco LstSuivi
 
ClearDialogue
Cancella l'elenco LstDialogue
Autore
 
Visualizza una finestra di copyright

8.5.1.2. COME FUNZIONA L'APPLICAZIONE

Inizializzazione dell'applicazione

Quando viene caricata la finestra principale dell'applicazione, vengono eseguite le seguenti azioni:

  • il foglio viene centrato sullo schermo
  • Sono attive solo le opzioni di menu "Accedi/Esci" e "Autore"
  • Il pulsante Annulla è nascosto
  • Gli elenchi LstSuivi e LstDialogue sono vuoti
Menu Connetti/Connetti

Questa opzione è disponibile solo quando i campi Host remoto e N. porta non sono vuoti e non c'è alcuna connessione attualmente attiva. Facendo clic su questa opzione si eseguono le seguenti operazioni:

  • La porta viene convalidata: deve essere un numero intero maggiore di 0
  • Viene avviato un thread per stabilire la connessione al server
  • Viene visualizzato il pulsante Annulla per consentire all'utente di interrompere la connessione in corso
  • Tutte le opzioni del menu sono disabilitate tranne Esci e Autore

La connessione può terminare in diversi modi:

  1. L'utente ha cliccato sul pulsante Annulla: il thread di connessione viene interrotto e il menu viene riportato allo stato iniziale. Il log indica che l'utente ha chiuso la connessione.
  2. La connessione termina con un errore: si procede come in precedenza e, in aggiunta, nel log viene indicata la causa dell'errore.
  3. La connessione viene completata con successo: il pulsante Annulla viene rimosso, il log indica che la connessione è stata stabilita, il menu ResetLog è abilitato, il menu Connetti è disabilitato e il menu Disconnetti è abilitato
Menu Connetti/Disconnetti

Questa opzione è disponibile solo quando è attiva una connessione al server. Se attivata, chiude la connessione al server e riporta il menu allo stato iniziale. Il registro indica che la connessione è stata chiusa dal client.

Menu Connetti/Esci

Questa opzione chiude qualsiasi connessione attiva al server e chiude l'applicazione.

Menu Messaggi/Invia

Questa opzione è disponibile solo se sono soddisfatte le seguenti condizioni:

  • è stata stabilita una connessione al server

  • è presente un messaggio da inviare

Se queste condizioni sono soddisfatte, il testo nel campo TxtSend (3) viene inviato al server, terminato dalla sequenza RCLF se l'opzione RCLF è stata selezionata, oppure dalla sequenza LF in caso contrario. Eventuali errori di trasmissione vengono segnalati nell'elenco di tracciamento.

Menu RazSuivi e RazDialogue

Cancella rispettivamente gli elenchi LstSuivi e LstDialogue. Queste opzioni sono disabilitate quando gli elenchi corrispondenti sono vuoti.

Il pulsante Annulla

Questo pulsante, situato nella parte inferiore del modulo, appare solo quando il client sta tentando di connettersi al server. La connessione potrebbe non andare a buon fine perché il server non risponde o risponde in modo errato. Il pulsante Annulla offre quindi all'utente la possibilità di interrompere la richiesta di connessione.

Elenchi di tracciamento

L'elenco LstSuivi (5) tiene traccia della connessione. Indica i momenti chiave della connessione:

  • la sua creazione da parte del client

  • la sua chiusura da parte del server o del client

  • eventuali errori che possono verificarsi mentre la connessione è attiva

La lista LstDialogue (6) tiene traccia del dialogo stabilito tra il client e il server. Un thread monitora in background ciò che accade sul socket di comunicazione del client e lo visualizza nella lista 6.

L'opzione Autore

Questo menu apre una finestra denominata Copyright:

Image

Gestione degli errori

Gli errori di connessione vengono segnalati nell'elenco di tracciamento 6, mentre quelli relativi al dialogo client/server vengono segnalati nell'elenco dei dialoghi 7. In caso di errore di connessione, il dialogo client/server viene chiuso e il modulo viene riportato allo stato iniziale, pronto per una nuova connessione.

8.5.1.3. ATTIVITÀ

Implementare il lavoro sopra descritto in due forme:

  • applicazione autonoma
  • applet

8.5.2. Esercizio 2 - Un server di risorse

8.5.2.1. INTRODUZIONE

Un'istituzione dispone di diversi potenti server di calcolo accessibili tramite Internet. Qualsiasi macchina che desideri utilizzare questi servizi di calcolo invia un file di dati alla porta 756 su uno dei server. Questo file contiene varie informazioni: login, password, comandi che specificano il tipo di calcolo desiderato e i dati su cui eseguire il calcolo. Se il file di dati è valido, il server di calcolo selezionato lo elabora e restituisce i risultati al client sotto forma di file di testo.

I vantaggi di questa configurazione sono numerosi:

  • qualsiasi tipo di client (PC, Mac, Unix, ecc.) può utilizzare questo servizio
  • il client può trovarsi ovunque su Internet
  • le risorse di calcolo sono ottimizzate: sono necessarie solo poche macchine potenti. Pertanto, una piccola organizzazione priva di risorse di calcolo può utilizzare questo servizio a un costo calcolato in base al tempo di calcolo utilizzato.

Nonostante la potenza delle macchine, un calcolo può talvolta richiedere diverse ore: il server risulta quindi non disponibile per altri client. Ciò pone il problema per un client di trovare un server di calcolo disponibile. Per risolvere questo problema, viene utilizzato un “gestore delle risorse di calcolo”, di seguito denominato server GRC. Questo servizio gira su una singola macchina e opera sulla porta 864 in modalità TCP. Un cliente che desidera accedere a un server di calcolo contatta questo servizio. Il server GRC, che mantiene un elenco completo dei server di calcolo, risponde inviando al cliente il nome di un server attualmente inattivo. Il cliente invia quindi semplicemente i propri dati al server designato.

Proponiamo di sviluppare il server GRC.

8.5.2.2. L'INTERFACCIA VISIVA

L'interfaccia visiva sarà la seguente:

Image

L'interfaccia mostra due elenchi di server:

  • a sinistra, l'elenco dei server inattivi, che sono quindi disponibili per i calcoli
  • a destra, l'elenco dei server occupati dai calcoli di un cliente.

La struttura del menu è la seguente:

Menu principale
Menu secondario
Ruolo
Servizio
Avvia
Avvia il servizio TCP sulla porta 864
 
Arresta
Arresta il servizio
 
Esci
Esci dall'applicazione
Autore
 
Informazioni sul copyright

La struttura dei controlli nel modulo è la seguente:

Nome
Tipo
Ruolo
listLibres
JList
Elenco dei server disponibili
listBusy
JList
Elenco dei server occupati

8.5.2.3. COME FUNZIONA L'APP

Caricamento dell'applicazione

Quando l'applicazione viene caricata, l'elenco listLibres viene popolato con l'elenco dei nomi dei server di calcolo gestiti dal GRC. Questi sono definiti in un file Servers passato come parametro. Questo file contiene un elenco di nomi di server, uno per riga, e viene quindi utilizzato per popolare l'elenco listLibres. Il menu Start è abilitato; il menu Stop è disabilitato.

Opzione Servizio/Avvia

Questa opzione

  • avvia il servizio di ascolto sulla porta 864 della macchina
  • disabilita il menu Avvia
  • abilita il menu Stop
Opzione Servizio/Arresta

Questa opzione arresta il servizio:

  • l'elenco dei server occupati viene cancellato
  • l'elenco dei server liberi viene popolato con il contenuto del file Servers
  • il menu Avvia viene abilitato
  • il menu Stop viene disabilitato
Opzione Servizio/Esci

L'applicazione si chiude.

Comunicazione client/server

Il dialogo client/server avviene tramite lo scambio di righe di testo terminate dalla sequenza RCLF. Il server GRC riconosce due comandi: getserver e endservice. Di seguito descriviamo in dettaglio il ruolo di questi due comandi:

  • 1-getserver

Il client chiede se è disponibile un server di calcolo per lui.

Il server GRC prende quindi il primo server trovato nel proprio elenco di server disponibili e restituisce il suo nome al client nel seguente formato:
        100-nom du serveur
Inoltre, sposta il server assegnato al client nell&#x27;elenco dei server occupati nel seguente formato:
        serveur (IP du client)
come mostrato nell&#x27;esempio seguente, in cui il server *calcul1.istia.univ-angers.fr* è occupato a servire il client con indirizzo IP *193.52.43.5*:

Image

Un client non può inviare un comando **getserver** se gli è già stato assegnato un server di calcolo. Pertanto, prima di rispondere al client, il server GRC verifica che l&#x27;indirizzo IP del client non sia già presente tra quelli registrati nell&#x27;elenco dei server occupati. Se così fosse, il server GRC risponde:
        501-Vous avez actuellement une demande en cours
Infine, c&#x27;è il caso in cui non sia disponibile alcun server di calcolo: l&#x27;elenco dei server liberi è vuoto. In questo caso, il server GRC risponde:
        502- Il n’y a aucun serveur de calcul disponible
In tutti i casi, dopo aver risposto al client, il server GRC chiude la connessione con il client in modo da poter servire altri client.
  • 2-finservice
Il client indica che non ha più bisogno del server di calcolo che stava utilizzando.

Il server GRC verifica innanzitutto che il cliente sia effettivamente uno di quelli che stava servendo. A tal fine, controlla se l&#x27;indirizzo IP del cliente è tra quelli registrati nell&#x27;elenco dei server occupati. Se così non fosse, il server GRC risponde:
        503-Aucun serveur ne vous a été attribué
Se il client viene riconosciuto, il server GRC risponde:
        101-Fin de service acceptée
e sposta il server di calcolo assegnato a quel client nell&#x27;elenco dei server liberi. Per riprendere l&#x27;esempio precedente, se il client invia il comando **di fine servizio**, il display del server GRC diventa:

Image

Dopo aver inviato la risposta, indipendentemente dal suo contenuto, il server GRC chiude la connessione.

8.5.2.4. ATTIVITÀ

Scrivi l'applicazione come programma autonomo che possa essere testato, ad esempio, con un client telnet o con il client TCP generico dell'esercizio precedente.

8.5.3. Esercizio 3 - Un client SMTP

8.5.3.1. INTRODUZIONE

In questa sede, vogliamo realizzare un client per il servizio SMTP (Simple Mail Transfer Protocol), che consente di inviare e-mail. Su Unix o Windows, il programma telnet è un client che funziona con il protocollo TCP. Può "comunicare" con qualsiasi servizio TCP che accetti comandi testuali terminanti con la sequenza RCLF, ovvero i caratteri ASCII 13 e 10. Ecco un esempio di conversazione con il servizio SMTP per l'invio di e-mail:


$ telnet istia.univ-angers.fr 25        // appel du service smtp

// risposta dal server SMTP


Trying 193.52.43.2...
Connected to istia.univ-angers.fr.
Escape character is '^]'.
220-Istia.Istia.Univ-Angers.fr Sendmail 8.6.10/8.6.9 ready at Tue, 16 Jan 1996 07:53:12 +0100
220 ESMTP spoken here

// commenti --------------

Il programma telnet può connettersi a qualsiasi servizio utilizzando la sintassi

***telnet*** **macchina\_servizio porta\_servizio**

Gli scambi client/server utilizzano righe di testo terminate dalla sequenza RCLF.

Le risposte dal servizio SMTP hanno la forma:

**Numero-messaggio oppure**

**Numero del messaggio**

Il server SMTP può inviare più righe di risposta. L'ultima riga della risposta è indicata da un numero seguito da uno spazio, mentre per le righe precedenti della risposta il numero è seguito da un trattino -.

Un numero maggiore o uguale a 500 indica un messaggio di errore.

// Fine dei commenti

help                        // commande émise au clavier

// risposta dal server SMTP

214-Commands:
214-    HELO    EHLO    MAIL    RCPT    DATA
214-    RSET    NOOP    QUIT    HELP    VRFY
214-    EXPN    VERB
214-For more info use "HELP <topic>".
214-To report bugs in the implementation send email to
214-    sendmail@CS.Berkeley.EDU.
214-For local information send email to Postmaster at your site.
214 End of HELP info

mail from: serge.tahe@istia.univ-angers.fr    // nouvelle commande émise au clavier

// commenti ---------

Il comando mail ha la seguente sintassi:

**mail da: indirizzo e-mail del mittente del messaggio**

// fine dei commenti

// risposta dal server SMTP

250 serge.tahe@istia.univ-angers.fr... Sender ok

// commenti

Il server SMTP non verifica la validità dell'indirizzo del mittente: lo accetta così com'è

// fine dei commenti


rcpt to: user1@istia.univ-angers.fr        // nouvelle commande émise au clavier

// commenti ---------

Il comando rcpt ha la seguente sintassi:

**rcpt a: indirizzo e-mail del destinatario del messaggio**

Se l'indirizzo e-mail è un indirizzo presente sul computer su cui è in esecuzione il server SMTP, ne verifica l'esistenza; in caso contrario, non esegue alcuna verifica. Se è stata eseguita la verifica ed è stato rilevato un errore, questo verrà segnalato con un numero >= 500.

Puoi inserire tutti i comandi "rcpt" che vuoi: questo ti permette di inviare un messaggio a più persone.

// fine dei commenti

// Risposta del server SMTP

250 user1@istia.univ-angers.fr... Recipient ok
data                        // nouvelle commande émise au clavier

// commenti ---------

Il comando data ha la seguente sintassi:

**data**

    **riga1**

    **riga2**

    **...**

    **.**

Seguono le righe di testo che compongono il messaggio, che deve terminare con una riga contenente solo il carattere "punto".

Il messaggio viene quindi inviato al destinatario specificato dal comando rcpt.

// fine dei commenti

// risposta dal server SMTP

354 Enter mail, end with "." on a line by itself

// testo del messaggio digitato sulla tastiera


subject: essai smtp
 
essai smtp a partir de telnet
.

// commenti

Nelle righe di testo del comando data, è possibile includere una riga subject: per specificare l'oggetto dell'e-mail. Questa riga deve essere seguita da una riga vuota.

// risposta dal server SMTP

250 HAA11627 Message accepted for delivery
quit                            // nouvelle commande émise au clavier

// commenti

**Il comando quit chiude la connessione al servizio** ***SMTP***

// fine dei commenti

// risposta dal server SMTP

221 Istia.Istia.Univ-Angers.fr closing connection

8.5.3.2. L'INTERFACCIA VISIVA

Proponiamo di realizzare un programma con la seguente interfaccia visiva:

Image

I controlli hanno le seguenti funzioni:

Numero
Tipo
Ruolo
1
JTextField
Un elenco di indirizzi e-mail separati da virgole
2
JTextField
Testo dell'oggetto del messaggio
3
JTextField
Elenco di indirizzi e-mail separati da virgole
4
JTextField
Un elenco di indirizzi e-mail separati da virgole
5
JTextArea
Testo del messaggio
6
JList
Elenco dei tracciamenti
7
JList
elenco di dialogo
8
JButton
Pulsante "Annulla" non visibile, che appare quando il client richiede una connessione al server SMTP. Consente all'utente di annullare questa richiesta se il server non risponde.

8.5.3.3. MENU

La struttura dei menu dell'applicazione è la seguente:

Menu principale
Menu secondario
Ruolo
Posta
  
 
Invia
Invia il messaggio dal controllo 5
 
Esci
Esci dall'app
Opzioni
  
 
Nascondi tracciamento
Nascondi controllo 6
 
Cancella lista di controllo
Cancella la lista di controllo 6
 
Nascondi finestra di dialogo
Nasconde la finestra di dialogo 7
 
Cancella finestra di dialogo
Cancella l'elenco delle finestre di dialogo 7
 
Configura
Consente all'utente di specificare
- l'indirizzo del server SMTP utilizzato dal programma
- il proprio indirizzo e-mail
 
Salva...
Salva la configurazione precedente in un file .ini
Autore
 
Informazioni sul copyright

8.5.3.4. COME FUNZIONA L'APPLICAZIONE

Menu Opzioni/Configurazione

Questo menu visualizza la seguente finestra:

Image

Entrambi i campi devono essere compilati affinché il pulsante OK sia attivo. Entrambe le informazioni devono essere memorizzate in variabili globali in modo che siano disponibili per altri moduli.

Menu Posta/Invia

Questa opzione è disponibile solo se sono soddisfatte le seguenti condizioni:

  • la configurazione è stata completata
  • c'è un messaggio da inviare
  • c'è un oggetto
  • c'è almeno un destinatario nei campi 1, 3 e 4

Se queste condizioni sono soddisfatte, la sequenza degli eventi è la seguente:

  • il modulo viene impostato in uno stato in cui tutte le azioni che potrebbero interferire con il dialogo client/server sono disabilitate
  • Viene stabilita una connessione sulla porta 25 del server specificato nella configurazione
  • Il client comunica quindi con il server SMTP secondo il protocollo sopra descritto
  • Il campo "Mail From" utilizza l'indirizzo e-mail del mittente specificato nella configurazione
  • Il comando "rcpt to:" viene utilizzato per ciascuno degli indirizzi e-mail presenti nei campi 1, 3 e 4
  • Nelle righe inviate dopo il comando data, apparirà il seguente testo:
    • una riga Subject:: testo dell'oggetto dal controllo 2
    • una riga Cc: indirizzi da test 3
    • una riga Bcc: indirizzi dal test 4
    • il testo del messaggio per il controllo 5
    • il punto finale
Il pulsante Annulla

Questo pulsante, situato nella parte inferiore del modulo, appare solo quando il client sta tentando di connettersi al server SMTP. La connessione potrebbe non andare a buon fine perché il server SMTP non risponde o risponde in modo errato. Il pulsante Annulla consente quindi all'utente di interrompere la richiesta di connessione.

Elenchi di tracciamento

L'elenco (6) tiene traccia della connessione. Indica i momenti chiave della connessione:

  • la sua creazione da parte del client
  • la sua chiusura da parte del server o del client
  • tutti gli errori di connessione

L'elenco (7) tiene traccia del dialogo SMTP stabilito tra il client e il server.

Questi due elenchi sono associati alle opzioni del menu:

Nascondi tracciamento
Nasconde l'elenco di tracciamento 6 e l'etichetta sopra di esso. Se l'altezza occupata da questi due controlli è H, tutti i controlli situati sotto di essi vengono spostati verso l'alto di un'altezza pari a H e la dimensione totale del modulo viene ridotta di H. Inoltre, Nascondi tracciamento nasconde l'opzione Cancella tracciamento sottostante.
Cancella tracciamento
Cancella l'elenco di tracciamento 6
Nascondi finestra di dialogo
Nasconde l'elenco della finestra di dialogo 7, l'etichetta sopra di esso e l'opzione di menu Cancella finestra di dialogo sottostante. Come per Nascondi tracciamento, la posizione dei controlli situati sotto (ad esempio il pulsante Annulla) viene ricalcolata e la dimensione della finestra viene ridotta.
Cancella finestra di dialogo
Cancella l'elenco delle finestre di dialogo 7
L'opzione Autore

Questo menu apre una finestra denominata Copyright:

Image

Gestione degli errori

Gli errori di connessione vengono segnalati nell'elenco di tracciamento 6, mentre quelli relativi alla finestra di dialogo client/server vengono segnalati nell'elenco della finestra di dialogo 7. Quando si verifica un errore, l'utente viene avvisato tramite una finestra di errore e viene visualizzato l'elenco contenente la causa dell'errore, se in precedenza era nascosto. Inoltre, la finestra di dialogo client/server viene chiusa e il modulo viene riportato allo stato iniziale.

8.5.3.5. GESTIONE DI UN FILE DI CONFIGURAZIONE

È auspicabile che l'utente non debba riconfigurare il software ogni volta che lo utilizza. A tal fine, se l'opzione Opzioni/Salva configurazione all'uscita è selezionata, la chiusura del programma salva le due informazioni impostate tramite l'opzione Opzioni/Configura, nonché lo stato dei due elenchi di tracciamento, in un file sendmail.ini situato nella stessa directory del file .exe del programma. Questo file ha il seguente formato:

SmtpServer=shiva.istia.univ-angers.fr
ReplyAddress=serge.tahe@istia.univ-angers.fr
Suivi=0
Dialogue=1

Le righe SmtpServer e ReplyAddress contengono le due informazioni inserite tramite il menu Opzioni/Configura. Le righe Tracking e Dialogue indicano lo stato delle liste di tracciamento e di dialogo: 1 (presente), 0 (assente).

All'avvio del programma, se esiste il file sendmail.ini, questo viene letto e il modulo viene configurato di conseguenza. Se il file sendmail.ini non esiste, il programma agisce come se fosse presente:

SmtpServer=
ReplyAddress=
Suivi=1
Dialogue=1

Se il file sendmail.ini esiste ma è incompleto (mancano delle righe), la riga mancante viene sostituita dalla riga corrispondente sopra riportata. Pertanto, se manca la riga Suivi=..., la trattiamo come se avessimo Suivi=1.

Tutte le righe che non corrispondono al modello:

    mot clé= valeur

vengono ignorate, così come quelle in cui la parola chiave non è valida. La parola chiave può essere scritta in maiuscolo o minuscolo: non ha importanza.

Nel menu Opzioni/Configura vengono visualizzati i valori correnti di SmtpServer e ReplyAddress. L'utente può quindi modificarli se lo desidera.

8.5.3.6. ATTIVITÀ DA SVOLGERE

Completare l'operazione sopra descritta. Si consiglia di occuparsi della gestione dei file di configurazione per ultima.

8.5.4. Esercizio 4 - Client POPPASS

8.5.4.1. Introduzione

Proponiamo di creare un client TCP in grado di comunicare con il server POPPASSD in esecuzione sulla porta 106. Questo servizio consente di modificare la propria password su una macchina UNIX. Il protocollo di comunicazione client/server è il seguente:

1 - La comunicazione avviene tramite lo scambio di messaggi terminati dalla sequenza RCLF

2 - Il client invia i comandi al server

- Il server risponde con messaggi che iniziano con un numero di 3 cifre: XXX. Se XXX=200, il comando è stato eseguito con successo; in caso contrario, si è verificato un errore.

3 - La sequenza degli scambi è la seguente:

A    - le client se connecte
- Il server risponde con un messaggio di benvenuto
B    - le client envoie USER login
- Il server risponde richiedendo la password se il login viene accettato; in caso contrario, restituisce un errore
C    - le client envoie PASS mot_de_passe
- Il server risponde richiedendo la nuova password se la password viene accettata; in caso contrario, restituisce un errore
D    - le client envoie NEWPASS nouveau_mot_de_passe
- Il server risponde confermando che la nuova password è stata accettata; in caso contrario, restituisce un errore
E    - le client envoie la commande QUIT
- Il server invia un messaggio di fine sessione e chiude la connessione

8.5.4.2. Il modulo client

Image

Il significato dei vari controlli è il seguente:

N.
nome
tipo
ruolo
1
txtRemoteHost
JTextField
nome del server
2
txtLogin
JTextField
accesso utente
3
txtPassword
JTextField
Password utente
4
txtNuovaPassword
JTextField
Nuova password dell'utente
5
txtConferma
JTextField
Conferma nuova password
6
lstTracking
JList
Messaggi di tracciamento della connessione
7
lstDialogue
JList
Messaggi di dialogo client/server
10
cmdCancel
JButton
non mostrato - Pulsante che appare quando la connessione al server è in corso. Consente di interromperla.

8.5.4.3. Menu

Titolo
Nome controllo
Funzione
Connessione
loginmenu
 
Connetti
mnuconnect
avvia la connessione al server
Esci
mnuQuitter
chiude l'applicazione
Messaggi
mnuMessaggi
 
Cancella cronologia
mnuClearTracking
cancella l'elenco lstSuivi
ClearDialog
mnuClearDialog
cancella l'elenco lstDialogue
Author
mnuAuthor
visualizza la finestra del copyright

8.5.4.4. Come funziona l'applicazione

Inizializzazione dell'applicazione

Quando viene caricato il foglio principale dell'applicazione, si verificano le seguenti azioni:

  • il foglio viene centrato sullo schermo
  • sono attive solo le opzioni di menu Login/Logout e Autorizza
  • il pulsante Annulla è nascosto
  • gli elenchi LstSuivi e LstDialogue sono vuoti
Menu Login/Connetti

Questa opzione è disponibile solo quando i campi da 1 a 5 sono stati compilati. Facendo clic su questa opzione si attivano le seguenti azioni:

  • Viene avviato un thread per stabilire la connessione al server
  • viene visualizzato il pulsante Annulla per consentire all'utente di interrompere la connessione in corso
  • tutte le opzioni del menu vengono disabilitate tranne Esci e Autore

La sequenza di eventi è quindi la seguente:

  1. L'utente ha cliccato sul pulsante Annulla: il thread di connessione viene interrotto e il menu viene riportato allo stato iniziale. Il log indica che l'utente ha chiuso la connessione.
  2. La richiesta di connessione viene accettata dal server. Avviamo quindi il dialogo con il server per modificare la password. Gli scambi in questo dialogo vengono registrati nell'elenco LstDialogue. Una volta completato il dialogo, la connessione con il server viene chiusa e il menu del modulo viene riportato allo stato iniziale.
  3. Finché il dialogo è attivo, il pulsante Annulla rimane visibile per consentire all'utente di chiudere la connessione se lo desidera.
  4. Se si verifica un errore durante la comunicazione, la connessione viene chiusa e la causa dell'errore viene visualizzata nell'elenco di tracciamento LstSuivi.
Menu Connessione/Esci

Questa opzione chiude tutte le connessioni attive con il server e chiude l'applicazione.

Menu ClearTracking e ClearDialog

Cancella rispettivamente gli elenchi LstSuivi e LstDialogue. Queste opzioni sono disabilitate quando gli elenchi corrispondenti sono vuoti.

Il pulsante Annulla

Questo pulsante, situato nella parte inferiore del modulo, appare solo quando il client si sta connettendo o è connesso al server. Il pulsante Annulla consente all'utente di interrompere la comunicazione con il server.

Elenchi di tracciamento

L'elenco LstSuivi (5) tiene traccia della connessione. Indica i momenti chiave della connessione:

  • la sua instaurazione da parte del client

  • la sua chiusura da parte del server o del client

  • eventuali errori che possono verificarsi mentre la connessione è attiva

La lista LstDialogue (6) tiene traccia del dialogo tra il client e il server.

L'opzione Author

Questo menu apre una finestra denominata Finestra Copyright:

Image

Gestione degli errori

Gli errori di comunicazione vengono segnalati nell'elenco di tracciamento 6, mentre quelli relativi al dialogo client/server vengono segnalati nell'elenco di dialogo 7. In caso di errore di connessione, il dialogo client/server viene chiuso e il modulo viene riportato allo stato iniziale, pronto per una nuova connessione.

8.5.4.5. ATTIVITÀ

Implementare il lavoro descritto sopra come applicazione autonoma e poi come applet.