Skip to content

11. Programmazione Internet

11.1. Généralités

11.1.1. I protocolli di Internet

Forniamo qui un'introduzione ai protocolli di comunicazione di Internet, noti anche come suite di protocolli TCP/IP (Transfer Control Protocol / Internet Protocol), dal nome dei due protocolli principali. Può essere utile che il lettore abbia una comprensione generale del funzionamento delle reti e, in particolare, dei protocolli TCP/IP prima di affrontare la realizzazione di applicazioni distribuite. Il testo che segue è una traduzione parziale di un brano tratto dal documento "Lan Workplace for Dos - Administrator's Guide" di NOVELL, un documento risalente agli inizi degli anni '90.


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

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

  • origine del pacchetto
  • destinazione
  • lunghezza
  • tipo

11.1.2. Il modello OSI

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

Ogni livello riceve servizi dal livello sottostante e fornisce i propri al livello superiore. Supponiamo che due applicazioni situate su macchine diverse, A e B, vogliano comunicare: lo fanno a livello del livello Application. Non hanno bisogno di conoscere tutti i dettagli del funzionamento della rete: ogni applicazione trasmette le informazioni che desidera inviare al livello sottostante, ovvero il livello Présentation. L’applicazione deve quindi conoscere solo le regole di interfaccia con il livello Présentation.

Una volta che le informazioni si trovano nel livello Présentation, vengono trasferite secondo altre regole al livello Session e così via, fino a quando le informazioni non raggiungono il supporto fisico e vengono trasmesse fisicamente al computer di destinazione. A quel punto, subirà il processo inverso rispetto a quello a cui è stata sottoposta sul computer mittente.

Ad ogni livello, il processo mittente incaricato di inviare le informazioni le invia a un processo ricevente sull’altra macchina appartenente allo stesso livello. Lo fa secondo determinate regole che vengono definite protocollo di livello. Si ottiene quindi il seguente schema di comunicazione finale:

Il ruolo dei diversi livelli è il seguente:

Physique
Garantisce la trasmissione dei bit su un supporto fisico. In questo livello si trovano apparecchiature terminali per l’elaborazione dei dati (E.T.T.D.), quali terminali o computer, nonché apparecchiature di terminazione dei circuiti dati (E.T.C.D.), quali modulatori/demodulatori, multiplexer e concentratori. I punti di interesse a questo livello sono:
  • la scelta della codifica delle informazioni (analogica o digitale)
  • la scelta della modalità di trasmissione (sincrona o asincrona).
Liaison de données
Nasconde le caratteristiche fisiche del livello fisico. Rileva e corregge gli errori di trasmissione.
Réseau
Gestisce il percorso che devono seguire le informazioni inviate sulla rete. Questo processo è denominato routage: determinare il percorso che un’informazione deve seguire per arrivare al destinatario.
Transport
Consente la comunicazione tra due applicazioni, mentre i livelli precedenti permettevano solo la comunicazione tra macchine. Un servizio fornito da questo livello può essere il multiplexing: il livello di trasporto potrà utilizzare una stessa connessione di rete (da macchina a macchina) per trasmettere informazioni appartenenti a più applicazioni.
Session
In questo livello si trovano servizi che consentono a un’applicazione di aprire e mantenere una sessione di lavoro su una macchina remota.
Présentation
Il suo scopo è uniformare la rappresentazione dei dati sulle diverse macchine. Pertanto, i dati provenienti da una macchina A saranno “formattati” dal livello Présentation della macchina A secondo un formato standard prima di essere inviati sulla rete. Una volta giunti al livello Présentation del computer destinatario B, che li riconoscerà grazie al loro formato standard, verranno rielaborati in modo diverso affinché l’applicazione del computer B possa riconoscerli.
Application
A questo livello si trovano le applicazioni generalmente vicine all’utente, come la posta elettronica o il trasferimento di file.

11.1.3. Il modello TCP/IP

Il modello OSI è un modello ideale mai realizzato. La suite di protocolli TCP/IP se ne avvicina nella forma seguente:

Livello fisico

Nelle reti locali si utilizzano generalmente le tecnologie Ethernet o Token-Ring. In questa sede ci limiteremo a presentare la tecnologia Ethernet.

Ethernet

È il nome dato a una tecnologia di reti locali a commutazione di pacchetti inventata da PARC Xerox all’inizio degli anni ’70 e standardizzata da Xerox, Intel e Digital Equipment nel 1978. La rete è costituita fisicamente da un cavo coassiale di circa 1,27 cm di diametro e di lunghezza non superiore a 500 m. Può essere estesa tramite répéteurs, ma due dispositivi non possono essere separati da più di due ripetitori. Il cavo è passivo: tutti i componenti attivi si trovano sulle macchine collegate al cavo. Ogni macchina è collegata al cavo tramite una scheda di accesso alla rete che comprende:

  • un trasmettitore (transceiver) che rileva la presenza di segnali sul cavo e converte i segnali analogici in segnali digitali e viceversa.
  • un accoppiatore che riceve i segnali digitali dal trasmettitore e li trasmette al computer per l’elaborazione o viceversa.

Le caratteristiche principali della tecnologia Ethernet sono le seguenti:

  • Capacità di 10 megabit al secondo.
  • Topologia a bus: tutte le macchine sono collegate allo stesso cavo
  • Rete broadcast: un dispositivo che trasmette invia le informazioni sul cavo indicando l’indirizzo del dispositivo destinatario. Tutti i dispositivi collegati ricevono quindi tali informazioni e solo quello a cui sono destinate le conserva.
  • Il metodo di accesso è il seguente: il trasmettitore che desidera trasmettere ascolta il cavo e rileva così la presenza o meno di un’onda portante, la cui presenza indicherebbe che è in corso una trasmissione. Si tratta della tecnica CSMA (Carrier Sense Multiple Access). In assenza di portante, un trasmettitore può decidere di trasmettere a sua volta. Possono essere più trasmettitori a prendere questa decisione. I segnali trasmessi si sovrappongono: si parla allora di collisione. Il trasmettitore rileva questa situazione: mentre trasmette sul cavo, ascolta ciò che effettivamente vi transita. Se rileva che l’informazione che transita sul cavo non è quella che ha trasmesso, ne deduce che vi è una collisione e interromperà la trasmissione. Gli altri trasmettitori che stavano trasmettendo faranno lo stesso. Ciascuno riprenderà la propria trasmissione dopo un tempo casuale che dipende da ciascun trasmettitore. Questa tecnica è denominata CD (Collision Detect). Il metodo di accesso è quindi denominato CSMA/CD.
  • Un indirizzamento a 48 bit. Ogni macchina ha un indirizzo, qui denominato indirizzo fisico, che è riportato sulla scheda che la collega al cavo. Questo indirizzo è chiamato indirizzo Ethernet della macchina.

Livello di rete

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

IP (Internet Protocol)
Trasmette pacchetti tra due nodi della rete
ICMP 
(Internet Control Message Protocol)
ICMP gestisce la comunicazione tra il programma del protocollo IP di una macchina e quello di un'altra macchina. Si tratta quindi di un protocollo di scambio di messaggi all'interno dello stesso protocollo IP.
ARP
(Address Resolution Protocol)
effettua la corrispondenza tra l’indirizzo Internet della macchina e l’indirizzo fisico della macchina
RARP
(Reverse Address Resolution Protocol)
effettua la corrispondenza tra l'indirizzo fisico del computer e l'indirizzo Internet del computer

Livelli di trasporto/sessione

In questo livello si trovano i seguenti protocolli:

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

Livelli Applicazione/Presentazione/Sessione

Qui si trovano diversi protocolli:

TELNET
Emulatore di terminale che consente a una macchina A di connettersi a una 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 un nome di computer in un indirizzo Internet del computer
XDR (eXternal Data Representation)
creato da Sun MicroSystems, specifica una rappresentazione standard dei dati, indipendente dal tipo di macchina
RPC(Remote Procedures 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)
, anch’esso definito da Sun; questo protocollo consente a una macchina di “vedere” il sistema di file di un’altra macchina. Si basa sul precedente protocollo RPC

11.1.4. Funzionamento dei protocolli Internet

Le applicazioni sviluppate nell’ambiente TCP/IP utilizzano generalmente diversi protocolli di tale ambiente. Un programma applicativo comunica con il livello più alto dei protocolli. Quest’ultimo trasmette le informazioni al livello sottostante e così via fino a raggiungere il supporto fisico. A quel punto, le informazioni vengono trasferite fisicamente al dispositivo destinatario, dove attraverseranno nuovamente gli stessi livelli, questa volta in senso inverso, fino a raggiungere l’applicazione destinataria delle informazioni inviate. Lo schema seguente illustra il percorso delle informazioni:

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

  • L’applicazione fornisce una sequenza di byte da trasmettere al livello transport.
  • Il livello transport suddivide questa sequenza di byte in segments e TCP, aggiungendo all’inizio di ciascun segmento il numero che lo identifica. I segmenti vengono passati al livello di rete gestito dal protocollo IP.
  • Il livello IP crea un pacchetto che incapsula il segmento TCP ricevuto. All'inizio di questo pacchetto, inserisce gli indirizzi Internet del computer di origine e di destinazione. Determina inoltre l'indirizzo fisico del computer di destinazione. Il tutto viene trasmesso al livello di collegamento dati e collegamento fisico, ovvero alla scheda di rete che collega il computer alla rete fisica.
  • A questo punto, il pacchetto IP viene a sua volta incapsulato in una trama fisica e inviato al destinatario tramite il cavo.
  • Sul computer di destinazione, il livello "Collegamento dati e collegamento fisico" esegue l'operazione inversa: incapsula il pacchetto IP dalla trama fisica e lo trasmette al livello IP.
  • Il livello IP verifica che il pacchetto sia corretto: calcola una somma in base ai bit ricevuti (checksum), somma che deve ritrovare nell'intestazione del pacchetto. In caso contrario, il pacchetto viene scartato.
  • Se il pacchetto viene dichiarato corretto, il livello IP decapsula il segmento TCP in esso contenuto e lo trasmette al livello superiore transport.
  • Il livello transport, che nel nostro esempio è il livello TCP, esamina il numero del segmento per ripristinare il corretto ordine dei segmenti.
  • Calcola inoltre un checksum per il segmento TCP. Se risulta corretto, il livello TCP invia una conferma di ricezione alla macchina di origine; in caso contrario, il segmento TCP viene rifiutato.
  • Al livello TCP non resta che trasmettere la parte dati del segmento all’applicazione destinataria di tali dati nel livello superiore.

11.1.5. I problemi di indirizzamento in Internet

Un nodo di una rete può essere un computer, una stampante intelligente, un server di file, in pratica qualsiasi dispositivo in grado di comunicare tramite i protocolli TCP/IP. Ogni nodo ha un indirizzo fisico il cui formato dipende dal tipo di rete. Su una rete Ethernet, l’indirizzo fisico è codificato su 6 byte. Un indirizzo di una rete X25 è un numero di 14 cifre.

L’indirizzo Internet di un nodo è un indirizzo logico: è indipendente dall’hardware e dalla rete utilizzata. Si tratta di un indirizzo di 4 byte che identifica sia una rete locale sia un nodo di tale rete. L’indirizzo Internet è solitamente rappresentato sotto forma di 4 numeri, corrispondenti ai valori dei 4 byte, separati da un punto. Pertanto, l’indirizzo del computer Lagaffe della Facoltà di Scienze di Angers è 193.49.144.1 e quello del computer Liny è 193.49.144.9. Ne consegue che l’indirizzo Internet della rete locale è 193.49.144.0. Su questa rete possono esserci fino a 254 nodi.

Poiché gli indirizzi Internet o gli indirizzi IP sono indipendenti dalla rete, un computer di una rete A può comunicare con un computer di una rete B senza preoccuparsi del tipo di rete su cui si trova: è sufficiente che conosca il proprio indirizzo IP. Il protocollo IP di ciascuna rete si occupa di effettuare la conversione tra indirizzo IP e indirizzo fisico, in entrambe le direzioni.

Gli indirizzi IP devono essere tutti diversi. In Francia, è l’INRIA che si occupa di assegnare gli indirizzi IP. Infatti, questo ente assegna un indirizzo alla rete locale, ad esempio 193.49.144.0 per la rete della Facoltà di Scienze di Angers. L’amministratore di questa rete può quindi assegnare gli indirizzi IP da 193.49.144.1 a 193.49.144.254 a sua discrezione. Questo indirizzo è generalmente registrato in un file specifico su ogni macchina collegata alla rete.

11.1.5.1. Le classi di indirizzi IP

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

  • l'indirizzo della rete
  • l'indirizzo di un nodo di tale rete

A seconda della dimensione di questi due campi, gli indirizzi IP si dividono in 3 classi: classi A, B e C.

Classe A

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

R1 è l'indirizzo di rete

N1.N2.N3 è l'indirizzo di un computer in tale rete

Più precisamente, la forma di un indirizzo di classe A IP è la seguente:

L'indirizzo di rete è a 7 bit e l'indirizzo del nodo a 24 bit. Si possono quindi avere 127 reti di classe A, ciascuna delle quali può contenere fino a 224 nodi.

Classe B

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

R1.R2 è l’indirizzo della rete

N1.N2 è l'indirizzo di un computer in questa rete

Più precisamente, la forma di un indirizzo di classe B IP è la seguente:

L'indirizzo di rete è composto da 2 byte (14 bit esatti), così come quello del nodo. Si possono quindi avere 2¹⁴ reti di classe B, ciascuna delle quali può contenere fino a 2¹⁶ nodi.

Classe C

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

R1.R2.R3 è l’indirizzo della rete

N1 è l'indirizzo di un computer in questa rete

Più precisamente, la forma di un indirizzo di classe C IP è la seguente:

L'indirizzo di rete è composto da 3 byte (meno 3 bit) e l'indirizzo del nodo da 1 byte. Si possono quindi avere 221 reti di classe C contenenti fino a 256 nodi.

Poiché l’indirizzo del computer Lagaffe della Facoltà di Scienze di Angers è 193.49.144.1, si nota che l’occhetto di peso maggiore è pari a 193, ovvero 11000001 in binario. Ne consegue che la rete è di classe C.

Indirizzi riservati

  • Alcuni indirizzi IP sono indirizzi di rete piuttosto che indirizzi di nodi all’interno della rete. Si tratta di quelli in cui l’indirizzo del nodo è impostato a 0. Pertanto, l’indirizzo 193.49.144.0 è l’indirizzo IP della rete della Facoltà di Scienze di Angers. Di conseguenza, nessun nodo di una rete può avere l’indirizzo zero.
  • Quando in un indirizzo IP l’indirizzo del nodo è composto esclusivamente da 1, si ha un indirizzo di broadcast: tale indirizzo indica tutti i nodi della rete.
  • In una rete di classe C, che teoricamente consente 2⁸ = 256 nodi, se si escludono i due indirizzi non consentiti, rimangono solo 254 indirizzi consentiti.

11.1.5.2. I protocolli di conversione Indirizzo Internet <--> Indirizzo fisico

Abbiamo visto che durante la trasmissione di dati da una macchina all’altra, questi, attraversando il livello IP, venivano incapsulati in pacchetti. Questi hanno la seguente forma:

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

Nella trama finale sono presenti gli indirizzi fisici dei computer di origine e di destinazione. Come vengono ottenuti?

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

  • Invia un pacchetto di un tipo speciale denominato pacchetto ARP contenente l’indirizzo IP del computer di cui si sta cercando l’indirizzo fisico. Ha inoltre provveduto a inserirvi il proprio indirizzo IP e il proprio indirizzo fisico.
  • Questo pacchetto viene inviato a tutti i nodi della rete.
  • Questi ultimi riconoscono la natura speciale del pacchetto. Il nodo che riconosce il proprio indirizzo IP nel pacchetto risponde inviando al mittente del pacchetto il proprio indirizzo fisico. Come è possibile? Ha trovato nel pacchetto gli indirizzi IP e l’indirizzo fisico del mittente.
  • Il mittente riceve quindi l’indirizzo fisico che stava cercando. Lo memorizza in modo da poterlo utilizzare in seguito, qualora debbano essere inviati altri pacchetti allo stesso destinatario.

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

Quando un amministratore desidera riorganizzare la propria rete, può trovarsi nella necessità di modificare gli indirizzi IP di tutti i nodi e quindi di modificare i diversi file di configurazione dei vari nodi. Ciò può risultare noioso e fonte di errori se il numero di macchine è elevato. Un metodo consiste nel non assegnare alcun indirizzo IP alle macchine: si inserisce quindi un codice speciale nel file in cui la macchina dovrebbe trovare il proprio indirizzo IP. Scoprendo di non avere un indirizzo IP, la macchina lo richiede secondo un protocollo denominato RARP (Reverse Address Resolution Protocol). Invia quindi sulla rete un pacchetto speciale chiamato pacchetto RARP, analogo al precedente pacchetto ARP, nel quale inserisce il proprio indirizzo fisico. Questo pacchetto viene inviato a tutti i nodi, che riconoscono quindi un pacchetto RARP. Uno di essi, denominato server RARP, possiede un file che riporta la corrispondenza indirizzo fisico <--> indirizzo IP di tutti i nodi. Risponde quindi al mittente del pacchetto RARP, rinviandogli il proprio indirizzo IP. Un amministratore che desideri riconfigurare la propria rete deve quindi semplicemente modificare il file di corrispondenze del server RARP. Quest’ultimo deve normalmente avere un indirizzo fisso IP che deve poter conoscere senza dover utilizzare lui stesso il protocollo RARP.

11.1.6. Il livello di rete denominato livello IP di Internet

Il protocollo IP (Internet Protocol) definisce la forma che i pacchetti devono assumere e il modo in cui devono essere gestiti durante la loro trasmissione o ricezione. Questo particolare tipo di pacchetto è chiamato datagramma IP. Lo abbiamo già presentato:

L’aspetto importante è che, oltre ai dati da trasmettere, il datagramma IP contiene gli indirizzi Internet del computer di origine e di destinazione. In questo modo il computer destinatario sa chi gli sta inviando un messaggio.

A differenza di una trama di rete, la cui lunghezza è determinata dalle caratteristiche fisiche della rete su cui transita, la lunghezza del datagramma IP è fissata dal software e sarà quindi la stessa su reti fisiche diverse. Abbiamo visto che, scendendo dal livello di rete al livello fisico, il datagramma IP era incapsulato in una trama fisica. Abbiamo fornito l’esempio della trama fisica di una rete Ethernet:

Le frame fisiche circolano da nodo a nodo verso la loro destinazione, che potrebbe non trovarsi sulla stessa rete fisica del dispositivo mittente. Il pacchetto IP può quindi essere incapsulato in successione in diverse trame fisiche a livello dei nodi che fungono da collegamento tra due reti di tipo diverso. È anche possibile che il pacchetto IP sia troppo grande per essere incapsulato in una trama fisica. Il software IP del nodo in cui si verifica questo problema scompone quindi il pacchetto IP in fragments secondo regole precise, e ciascuno di essi viene poi inviato sulla rete fisica. Verranno ricomposti solo alla loro destinazione finale.

11.1.6.1. Il routing

L’instradamento è il metodo utilizzato per convogliare i pacchetti IP verso la loro destinazione. Esistono due metodi: l’instradamento diretto e l’instradamento indiretto.

Routing diretto

Il routing diretto indica l’inoltro di un pacchetto IP direttamente dal mittente al destinatario all’interno della stessa rete:

  • Il computer mittente di un datagramma IP dispone dell’indirizzo IP del destinatario.
  • Ottiene l'indirizzo fisico di quest'ultimo tramite il protocollo ARP o dalle proprie tabelle, se tale indirizzo è già stato acquisito.
  • Invia il pacchetto sulla rete a tale indirizzo fisico.

Routing indiretto

Il routing indiretto indica l’inoltro di un pacchetto IP verso una destinazione situata su una rete diversa da quella a cui appartiene il mittente. In questo caso, le parti relative all’indirizzo di rete degli indirizzi IP delle macchine di origine e di destinazione sono diverse. La macchina di origine ne prende atto. Invia quindi il pacchetto a un nodo speciale chiamato router (router), nodo che collega una rete locale alle altre reti e di cui trova l’indirizzo IP nelle proprie tabelle, indirizzo ottenuto inizialmente da un file, da una memoria permanente o tramite informazioni che circolano sulla rete.

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

Nel nostro esempio sopra riportato:

. La rete n. 1 ha l'indirizzo Internet 193.49.144.0 e la rete n. 2 l'indirizzo 193.49.145.0.

. All'interno della rete n. 1, il router ha l'indirizzo 193.49.144.6, mentre all'interno della rete n. 2 ha l'indirizzo 193.49.145.3.

Il router ha il compito di convertire il pacchetto IP che riceve, contenuto in una trama fisica tipica della rete n. 1, in una trama fisica in grado di circolare sulla rete n. 2. Se l'indirizzo IP del destinatario del pacchetto si trova nella rete n. 2, il router gli invierà il pacchetto direttamente; in caso contrario, lo invierà a un altro router, collegando la rete n. 2 a una rete n. 3 e così via.

11.1.6.2. Messaggi di errore e di controllo

Sempre nel livello di rete, quindi allo stesso livello del protocollo IP, esiste il protocollo ICMP (Internet Control Message Protocol). Serve a inviare messaggi sul funzionamento interno della rete: nodi in avaria, congestione a livello di un router, ecc... I messaggi ICMP vengono incapsulati in pacchetti IP e inviati sulla rete. I livelli IP dei diversi nodi intraprendono le azioni appropriate in base ai messaggi ICMP che ricevono. In questo modo, un'applicazione non si accorge mai di questi problemi specifici della rete.

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

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

11.1.7.1. Il protocollo UDP: User Datagram Protocol

Il protocollo UDP consente uno scambio non affidabile di dati tra due punti, ovvero non garantisce il corretto instradamento di un pacchetto alla sua destinazione. L’applicazione, se lo desidera, può gestire autonomamente questa situazione, ad esempio attendendo, dopo l’invio di un messaggio, una conferma di ricezione prima di inviare quello successivo.

Finora, a livello di rete, abbiamo parlato degli indirizzi IP dei computer. Tuttavia, su un singolo computer possono coesistere contemporaneamente diversi processi, tutti in grado di comunicare tra loro. È quindi necessario indicare, al momento dell’invio di un messaggio, non solo l’indirizzo IP del computer destinatario, ma anche il “nome” del processo destinatario. Questo nome è in realtà un numero, chiamato numero di porta. Alcuni numeri sono riservati alle applicazioni standard: la porta 69 per l’applicazione tftp (trivial file transfer protocol), ad esempio.

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

Questi datagrammi saranno incapsulati in pacchetti IP, quindi in frame fisici.

11.1.7.2. Il protocollo TCP: Transfer Control Protocol

Per garantire comunicazioni sicure, il protocollo UDP non è sufficiente: lo sviluppatore di applicazioni deve elaborare autonomamente un protocollo che gli consenta di verificare il corretto instradamento dei pacchetti. Il protocollo TCP (Transfer Control Protocol) evita questi problemi. Le sue caratteristiche sono le seguenti:

  • Il processo che intende trasmettere stabilisce innanzitutto una connessione con il processo destinatario delle informazioni che sta per inviare. Tale connessione avviene tra una porta del computer mittente e una porta del computer ricevente. Tra le due porte viene così creato un percorso virtuale che sarà riservato esclusivamente ai due processi che hanno stabilito la connessione.
  • Tutti i pacchetti inviati dal processo sorgente seguono questo percorso virtuale e arrivano nell’ordine in cui sono stati inviati, cosa che non era garantita nel protocollo UDP poiché i pacchetti potevano seguire percorsi diversi.
  • Le informazioni trasmesse hanno un carattere continuo. Il processo mittente invia le informazioni al proprio ritmo. Queste non vengono necessariamente inviate immediatamente: il protocollo TCP attende di averne una quantità sufficiente per inviarle. Vengono memorizzate in una struttura denominata segmento TCP. Una volta riempito, questo segmento verrà trasmesso al livello IP, dove verrà incapsulato in un pacchetto IP.
  • Ogni segmento inviato dal protocollo TCP è numerato. Il protocollo TCP destinatario verifica di ricevere correttamente i segmenti in sequenza. Per ogni segmento ricevuto correttamente, invia una conferma di ricezione al mittente.
  • Quando quest’ultimo lo riceve, lo comunica al processo mittente. Quest’ultimo può quindi sapere che un segmento è arrivato a destinazione, cosa che non era possibile con il protocollo UDP.
  • Se, trascorso un certo tempo, il protocollo TCP che ha trasmesso un segmento non riceve una conferma di ricezione, ritrasmette il segmento in questione, garantendo così la qualità del servizio di inoltro delle informazioni.
  • Il circuito virtuale stabilito tra i due processi che comunicano è full-duplex: ciò significa che le informazioni possono transitare in entrambe le direzioni. In questo modo il processo di destinazione può inviare conferme di ricezione anche mentre il processo di origine continua a inviare informazioni. Ciò consente, ad esempio, al protocollo sorgente TCP di inviare più segmenti senza attendere la conferma di ricezione. Se dopo un certo tempo si accorge di non aver ricevuto la conferma di ricezione di un determinato segmento n. n, riprenderà la trasmissione dei segmenti da quel punto.

11.1.8. Il livello Applicazioni

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

TELNET

Questo protocollo consente a un utente di una macchina A della rete di connettersi a una macchina B (spesso denominata macchina host). TELNET emula sulla macchina A un terminale cosiddetto universale. L'utente si comporta quindi come se disponesse di un terminale collegato al computer B. Telnet si basa sul protocollo TCP.

FTP: (File Transfer Protocol)

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

TFTP: (Trivial File Transfer Control)

Questo protocollo è una variante di FTP. Si basa sul protocollo UDP ed è meno sofisticato rispetto a FTP.

DNS: (Domain Name System)

Quando un utente desidera scambiare file con un computer remoto, ad esempio tramite FTP, deve conoscere l’indirizzo Internet di tale computer. Ad esempio, per eseguire FTP sul computer Lagaffe dell’Università di Angers, occorrerebbe avviare FTP come segue: FTP 193.49.144.1

Ciò richiede la presenza di una directory che metta in corrispondenza macchina <--> indirizzo IP. Probabilmente in questa directory le macchine sarebbero designate con nomi simbolici quali:

macchina DPX2/320 dell’Università di Angers

macchina Sun dell’Università di Angers ISERPA

È evidente che sarebbe più comodo identificare un computer con un nome piuttosto che con il suo indirizzo IP. Si pone quindi il problema dell’unicità del nome: ci sono milioni di computer interconnessi. Si potrebbe immaginare che un organismo centralizzato attribuisca i nomi. Sarebbe senza dubbio piuttosto complicato. Il controllo dei nomi è stato infatti distribuito in domini. Ogni dominio è gestito da un organismo generalmente molto snello che gode di piena libertà nella scelta dei nomi dei computer. Così, i computer in Francia appartengono al dominio «fr», gestito dall’Inria di Parigi. Per semplificare ulteriormente le cose, il controllo viene ulteriormente distribuito: all’interno del dominio «fr» vengono creati altri domini. Ad esempio, l’Università di Angers appartiene al dominio «univ-Angers». Il servizio che gestisce questo dominio ha piena libertà di denominare i computer della rete dell’Università di Angers. Per il momento questo dominio non è stato suddiviso. Ma in una grande università con molti computer in rete, potrebbe esserlo.

Il computer DPX2/320 dell’Università di Angers è stata denominata Lagaffe, mentre una PC e una 486DX50 sono state denominate liny. Come si fa a fare riferimento a queste macchine dall’esterno? Specificando la gerarchia dei domini a cui appartengono. Pertanto, il nome completo della macchina Lagaffe sarà:

Lagaffe.univ-Angers.fr

All’interno dei domini è possibile utilizzare nomi relativi. Pertanto, all’interno del dominio fr e al di fuori del dominio univ-Angers, la macchina Lagaffe potrà essere indicata come

Lagaffe.univ-Angers

Infine, all’interno del dominio univ-Angers, potrà essere indicata semplicemente come

Lagaffe

Un'applicazione può quindi fare riferimento a un computer tramite il suo nome. In definitiva, è comunque necessario ottenere l'indirizzo Internet di quel computer. Come si ottiene? Supponiamo di voler comunicare da un computer A a un computer B.

  • Se il computer B appartiene allo stesso dominio del computer A, probabilmente il suo indirizzo IP si troverà in un file del computer A.
  • Altrimenti, il computer A troverà in un altro file, o nello stesso di prima, un elenco di alcuni server dei nomi con i relativi indirizzi IP. Un server dei nomi ha il compito di stabilire la corrispondenza tra il nome di un computer e il suo indirizzo IP. Il computer A invierà una richiesta speciale al primo server dei nomi della sua lista, denominata richiesta DNS, che include quindi il nome del computer ricercato. Se il server interpellato ha quel nome nei propri registri, invierà al computer A l’indirizzo IP corrispondente. In caso contrario, anche il server troverà nei propri file un elenco di server dei nomi che può interpellare. Lo farà quindi. In questo modo verrà interpellato un certo numero di server dei nomi, non in modo casuale ma in modo da ridurre al minimo le richieste. Se la macchina viene infine individuata, la risposta tornerà alla macchina A.

XDR: (Rappresentazione dei dati eXternal)

Creato da Sun MicroSystems, questo protocollo specifica una rappresentazione standard dei dati, indipendente dalle macchine.

RPC: (Remote Procedure Call)

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

NFS: Network File System

Sempre definito da Sun, questo protocollo consente a una macchina di «vedere» il sistema di file di un’altra macchina. Si basa sul precedente protocollo RPC.

11.1.9. Conclusione

In questa introduzione abbiamo presentato alcune linee generali dei protocolli Internet. Per approfondire questo argomento, si consiglia di leggere l’eccellente libro di Douglas Comer:

Titolo TCP/IP: Architettura, protocolli, applicazioni.

Autore Douglas COMER

Editore InterEditions

11.2. Le classi .NET della gestione degli indirizzi IP

Un computer connesso a Internet è identificato in modo univoco da un indirizzo IP (Internet Protocol) che può assumere due forme:

  • IPv4: codificato su 32 bit e rappresentato da una stringa della forma "I1.I2.I3.I4", dove In è un numero compreso tra 1 e 254. Questi sono attualmente gli indirizzi IP più diffusi.
  • IPv6: codificato su 128 bit e rappresentato da una stringa della forma "[I1.I2.I3.I4.I5.I6.I7.I8]", dove In è una stringa di 4 cifre esadecimali. In questo documento non utilizzeremo gli indirizzi IPv6.

Una macchina può essere definita anche da un nome, anch’esso univoco. Questo nome non è obbligatorio, poiché le applicazioni utilizzano sempre, in definitiva, gli indirizzi IP delle macchine. Sono presenti per facilitare la vita degli utenti. Pertanto, con un browser, è più semplice richiedere l’indirizzo http://www.ibm.com (URL) piuttosto che URL http://129.42.17.99, sebbene entrambi i metodi siano possibili.

Un dispositivo può avere più indirizzi IP se è fisicamente connesso a più reti contemporaneamente. In tal caso, avrà un indirizzo IP su ciascuna rete.

Un indirizzo IP può essere rappresentato in due modi in .NET:

  • sotto forma di stringa di caratteri "I1.I2.I3.I4" o "[I1.I2.I3.I4.I5.I6.I7.I8]"
  • sotto forma di un oggetto di tipo IPAddress

La classe IPAddress

Tra i metodi M, le proprietà P e le costanti C della classe IPAddress figurano i seguenti:

AddressFamily AddressFamily
P
famiglia dell'indirizzo IP. Il tipo AddressFamily è un'enumerazione. I due valori comuni sono:
AddressFamily.InterNetwork: per un indirizzo IPv4
AddressFamily.InterNetworkV6: per un indirizzo IPv6
IPAddress Any
C
l'indirizzo IP "0.0.0.0". Quando un servizio è associato a questo indirizzo, significa che accetta client su tutti gli indirizzi IP della macchina su cui opera.
IPAddress LoopBack
C
l'indirizzo IP "127.0.0.1". Chiamato "indirizzo di loopback". Quando un servizio è associato a questo indirizzo, significa che accetta solo i client che si trovano sulla stessa macchina su cui opera.
IPAdress None
C
l'indirizzo IP "255.255.255.255". Quando un servizio è associato a questo indirizzo, significa che non accetta alcun client.
bool TryParse(string ipString, out IPAddress address)
M
tenta di convertire l'indirizzo IP ipString nella forma "I1.I2.I3.I4" sotto forma di oggetto IPAddress address. Restituisce true se l’operazione ha avuto esito positivo.
bool IsLoopBack
M
restituisce true se l'indirizzo IP è "127.0.0.1"
string ToString()
M
converte l'indirizzo IP nella forma "I1.I2.I3.I4" o "[I1.I2.I3.I4.I5.I6.I7.I8]"

L'associazione tra l'indirizzo IP e nomMachine è garantita da un servizio distribuito su Internet denominato DNS (Domain Name System). I metodi statici della classe Dns consentono di effettuare l'associazione tra gli indirizzi IP <--> nomMachine:

GetHostEntry (string hostNameOrdAddress)
restituisce l’indirizzo IPHostEntry a partire da un indirizzo IP sotto forma di stringa o da un nome di macchina. Genera un’eccezione se la macchina non viene trovata.
GetHostEntry (IPAddress ip)
restituisce un indirizzo IPHostEntry a partire da un indirizzo IP di tipo IPAddress. Genera un'eccezione se la macchina non viene trovata.
string GetHostName()
restituisce il nome del computer su cui è in esecuzione il programma che esegue questa istruzione
IPAddress[] GetHostAddresses(string hostNameOrdAddress)
restituisce gli indirizzi IP della macchina identificata dal suo nome o da uno dei suoi indirizzi IP.

Un'istanza IPHostEntry incapsula gli indirizzi IP, gli alias e il nome di una macchina. Il tipo IPHostEntry è il seguente:

IPAddress[] AddressList
P
tabella degli indirizzi IP del computer
String[] Aliases
P
gli alias DNS della macchina. Questi sono i nomi corrispondenti ai diversi indirizzi IP della macchina.
string HostName
P
il nome host principale del computer

Consideriamo il seguente programma che visualizza il nome del computer su cui viene eseguito e poi, in modo interattivo, fornisce le corrispondenze tra indirizzo IP e nome del computer:


using System;
using System.Net;

namespace Chap9 {
    class Program {
        static void Main(string[] args) {
            // visualizza il nome del computer locale
            // fornisce quindi informazioni interattive sui computer in rete
            // identificate da un nome o da un indirizzo IP

            // macchina locale
            Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());

            // domande e risposte interattive
            string machine;
            IPHostEntry ipHostEntry;
            while (true) {
                // inserimento del nome o dell'indirizzo IP del dispositivo ricercato
                Console.Write("Machine recherchée (rien pour arrêter) : ");
                machine = Console.ReadLine().Trim().ToLower();
                // finito?
                if (machine == "") return;
                // gestione delle eccezioni
                try {
                    // ricerca macchina
                    ipHostEntry = Dns.GetHostEntry(machine);
                    // nome della macchina
                    Console.WriteLine("Machine : " + ipHostEntry.HostName);
                    // gli indirizzi IP del computer
                    Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
                    for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
                        Console.Write(", {0}" , ipHostEntry.AddressList[i]);
                    }
                    Console.WriteLine();
                    // alias del computer
                    if (ipHostEntry.Aliases.Length != 0) {
                        Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
                        for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
                            Console.Write(", {0}" , ipHostEntry.Aliases[i]);
                        }
                        Console.WriteLine();
                    }
                } catch {
                    // il computer non esiste
                    Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
                }
            }
        }
    }
}

L'esecuzione fornisce i seguenti risultati:

Machine Locale= LISA-AUTO2005A
Machine recherchée (rien pour arrêter) : localhost
Machine : LISA-AUTO2005A
Adresses IP : 127.0.0.1
Machine recherchée (rien pour arrêter) : 127.0.0.1
Machine : LISA-AUTO2005A
Adresses IP : 127.0.0.1
Machine recherchée (rien pour arrêter) : istia.univ-angers.fr
Machine : istia.univ-angers.fr
Adresses IP : 193.49.146.171
Machine recherchée (rien pour arrêter) : 193.49.146.171
Machine : istia.istia.univ-angers.fr
Adresses IP : 193.49.146.171
Machine recherchée (rien pour arrêter) : xx
Impossible de trouver la machine [xx]

11.3. Nozioni di base sulla programmazione web

11.3.1. Informazioni generali

Consideriamo la comunicazione tra due macchine remote A e B:

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

  • l'indirizzo IP o il nome del computer B
  • il numero della porta con cui opera l'applicazione AppB. Infatti, la macchina B può supportare numerose applicazioni che operano su Internet. Quando riceve informazioni provenienti dalla rete, deve sapere a quale applicazione sono destinate tali informazioni. Le applicazioni del computer B accedono alla rete tramite interfacce denominate anche porte di comunicazione. Questa informazione è contenuta nel pacchetto ricevuto dal computer B affinché possa essere consegnato all’applicazione corretta.
  • I protocolli di comunicazione supportati dal computer B. Nel nostro studio utilizzeremo esclusivamente i protocolli TCP-IP.
  • il protocollo di dialogo accettato dall’applicazione AppB. Infatti, i computer A e B «comunicheranno» tra loro. Ciò che si scambieranno sarà incapsulato nei protocolli TCP-IP. Tuttavia, quando, alla fine della catena, l’applicazione AppB riceverà le informazioni inviate dall’applicazione AppA, dovrà essere in grado di interpretarle. Ciò è analogo alla situazione in cui due persone, A e B, comunicano per telefono: il loro dialogo viene trasportato dal telefono. La voce viene codificata sotto forma di segnali dal telefono A, trasportata attraverso le linee telefoniche, arriva al telefono B per essere decodificata. La persona B sente quindi le parole. È qui che entra in gioco il concetto di protocollo di dialogo: se A parla francese e B non capisce questa lingua, A e B non potranno dialogare in modo efficace.

Pertanto, le due applicazioni che comunicano tra loro devono concordare sul tipo di dialogo che adotteranno. Ad esempio, il dialogo con un servizio ftp non è lo stesso di quello con un servizio pop: questi due servizi non accettano gli stessi comandi. Hanno un protocollo di dialogo diverso.

11.3.2. Le caratteristiche del protocollo TCP

In questa sede esamineremo solo le comunicazioni di rete che utilizzano il protocollo di trasporto TCP. Ricordiamo qui le caratteristiche di quest’ultimo:

  • Il processo che intende trasmettere stabilisce innanzitutto una connessione con il processo destinatario delle informazioni che sta per inviare. Questa connessione avviene tra una porta del computer mittente e una porta del computer ricevente. Tra le due porte viene così creato un percorso virtuale che sarà riservato esclusivamente ai due processi che hanno stabilito la connessione.
  • Tutti i pacchetti inviati dal processo sorgente seguono questo percorso virtuale e arrivano nell’ordine in cui sono stati inviati
  • L’informazione trasmessa ha un carattere continuo. Il processo mittente invia le informazioni al proprio ritmo. Queste non vengono necessariamente inviate immediatamente: il protocollo TCP attende di averne una quantità sufficiente per inviarle. Vengono memorizzate in una struttura denominata segmento TCP. Una volta riempito, questo segmento verrà trasmesso al livello IP, dove verrà incapsulato in un pacchetto IP.
  • Ogni segmento inviato dal protocollo TCP è numerato. Il protocollo TCP destinatario verifica di ricevere correttamente i segmenti in sequenza. Per ogni segmento ricevuto correttamente, invia una conferma di ricezione al mittente.
  • Quando quest’ultimo lo riceve, lo comunica al processo mittente. Quest’ultimo può quindi sapere che un segmento è arrivato a destinazione.
  • Se, trascorso un certo tempo, il protocollo TCP che ha trasmesso un segmento non riceve una conferma di ricezione, ritrasmette il segmento in questione, garantendo così la qualità del servizio di inoltro delle informazioni.
  • Il circuito virtuale stabilito tra i due processi che comunicano è full-duplex: ciò significa che le informazioni possono transitare in entrambe le direzioni. In questo modo il processo di destinazione può inviare conferme di ricezione anche mentre il processo di origine continua a inviare informazioni. Ciò consente, ad esempio, al protocollo sorgente TCP di inviare più segmenti senza attendere la conferma di ricezione. Se dopo un certo tempo si accorge di non aver ricevuto la conferma di ricezione di un determinato segmento n. n, riprenderà la trasmissione dei segmenti da quel punto.

11.3.3. Il rapporto client-server

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

11.3.4. Architettura di un client

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

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

11.3.5. Architettura di un server

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

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

Il programma server gestisce in modo diverso la richiesta di connessione iniziale di un client rispetto alle sue successive richieste volte a ottenere un servizio. Il programma non fornisce direttamente il servizio. Se lo facesse, durante la durata del servizio non sarebbe più in ascolto delle richieste di connessione e i client non verrebbero quindi serviti. Proceda quindi in altro modo: non appena una richiesta di connessione viene ricevuta sulla porta di ascolto e poi accettata, il server crea un’attività incaricata di fornire il servizio richiesto dal cliente. Tale servizio viene fornito su un’altra porta del server, denominata porta di servizio. In questo modo è possibile servire più clienti contemporaneamente.

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

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

11.4. Scopri i protocolli di comunicazione di Internet

11.4.1. Introduzione

Quando un client si connette a un server, si instaura un dialogo tra i due. La natura di tale dialogo costituisce ciò che viene definito protocollo di comunicazione del server. Tra i protocolli più diffusi su Internet figurano i seguenti:

  • HTTP: HyperText Transfer Protocol - il protocollo di comunicazione con un server web (server HTTP)
  • SMTP: Simple Mail Transfer Protocol – il protocollo di comunicazione con un server di invio della posta elettronica (server SMTP)
  • POP: Post Office Protocol - il protocollo di comunicazione con un server di archiviazione della posta elettronica (server POP). In questo caso si tratta di recuperare i messaggi di posta elettronica ricevuti e non di inviarne.
  • FTP: File Transfer Protocol - il protocollo di comunicazione con un server di archiviazione file (server FTP).

Tutti questi protocolli hanno la particolarità di essere protocolli basati su righe di testo: il client e il server si scambiano righe di testo. Se si dispone di un client in grado di:

  • creare una connessione con un server TCP
  • visualizzare sulla console le righe di testo che il server gli invia
  • inviare al server le righe di testo che un utente digiterebbe

allora si è in grado di dialogare con un server TCP che utilizza un protocollo basato su righe di testo, purché si conoscano le regole di tale protocollo.

Il programma telnet, presente sui sistemi Unix o Windows, è un client di questo tipo. Sui sistemi Windows è disponibile anche uno strumento chiamato putty ed è proprio questo che useremo in questa sede. putty è scaricabile all’indirizzo [http://www.putty.org/]. Si tratta di un eseguibile (.exe) pronto all’uso. Lo configureremo nel modo seguente:

  • [1]: l’indirizzo IP del server TCP a cui ci si vuole connettere o il suo nome
  • [2]: la porta di ascolto del server TCP
  • [3]: utilizzare la modalità Raw, che indica una connessione TCP grezza.
  • [4]: impostare la modalità Never per impedire che la finestra del client putty si chiuda se il server interrompe la connessione.
  • [6,7]: numero di colonne/righe della console
  • [5]: il numero massimo di righe conservate in memoria. Un server HTTP può inviare molte righe. È necessario poterle "scorrere".
  • [8,9]: per conservare le impostazioni precedenti, assegnare un nome alla configurazione [8] e salvarla [9].
  • [11,12]: per recuperare una configurazione salvata, selezionarla ([11]) e caricarla ([12]).

Con questo strumento così configurato, scopriamo alcuni protocolli TCP.

11.4.2. Il protocollo HTTP (HyperText Transfer Protocol)

Colleghiamo il nostro client [1] al server web del computer istia.univ-angers.fr [2], porta 80 [3]:

Nella console di putty, creiamo la finestra di dialogo HTTP come segue:

GET / HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: close

HTTP/1.1 200 OK
Date: Sat, 03 May 2008 07:53:47 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=0d2e64b317; path=/
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1

693f
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
....
         </html>
0
  • le righe da 1 a 4 rappresentano la richiesta del cliente, digitata tramite tastiera
  • le righe da 5 a 19 rappresentano la risposta del server
  • riga 1: sintassi GET UrlDocument HTTP/1.1 - richiediamo l'URL /, c.a.d. la radice del sito web [istia.univ-angers.fr].
  • riga 2: sintassi Host: macchina:porta
  • riga 3: sintassi Connection: [mode de la connexion]. La modalità [close] indica al server di chiudere la connessione una volta inviata la risposta. La modalità [Keep-Alive] richiede di lasciarla aperta.
  • riga 4: riga vuota. Le righe da 1 a 3 sono denominate intestazioni HTTP. Possono essercene altre oltre a quelle qui presentate. La fine delle intestazioni HTTP è segnalata da una riga vuota.
  • righe 5-13: le intestazioni HTTP della risposta del server - terminano anch'esse con una riga vuota.
  • righe 14-19: il documento inviato dal server, in questo caso un documento HTML
  • riga 5: sintassi HTTP/1.1 codice msg - il codice 200 indica che il documento richiesto è stato trovato.
  • riga 6: data e ora del server
  • riga 7: identificazione del software che gestisce il servizio web – in questo caso un server Apache su Linux/Debian
  • riga 8: il documento è stato generato dinamicamente da PHP
  • riga 9: cookie di identificazione del client - se quest’ultimo desidera essere riconosciuto al prossimo accesso, dovrà inviare nuovamente questo cookie nelle sue intestazioni HTTP.
  • riga 10: indica che, dopo aver fornito il documento richiesto, il server chiuderà la connessione
  • riga 11: il documento verrà trasmesso a pezzi (chunked) e non in un unico blocco.
  • riga 12: tipo di documento: in questo caso un documento HTML
  • riga 13: la riga vuota che segnala la fine delle intestazioni HTTP del server
  • riga 14: numero esadecimale che indica il numero di caratteri del primo blocco del documento. Quando questo numero sarà pari a 0 (riga 19), il client saprà di aver ricevuto l'intero documento.
  • righe 15-18: parte del documento ricevuto.

La connessione è stata chiusa e il client putty è inattivo. Riconnettiamoci a [1] e cancelliamo dallo schermo le visualizzazioni precedenti [2,3]:

La finestra di dialogo questa volta è la seguente:

GET /inconnu HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: Close

HTTP/1.1 404 Not Found
Date: Sat, 03 May 2008 08:16:02 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=iso-8859-1

11a
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
                                                  <HTML><HEAD>
                                                              <TITLE>404 Not Found</TITLE>
                                                                                          </HEAD><BODY>
                                                                                                       <H1>Not Found</H1>
 The requested URL /inconnu was not found on this server.<P>
                                                            <HR>
                                                                <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
                   </BODY></HTML>

0
  • riga 1: è stato richiesto un documento inesistente
  • riga 5: il server HTTP ha risposto con il codice 404, a indicare che il documento richiesto non è stato trovato.

Se si richiede questo documento con il browser Firefox:

Image

Se si richiede di visualizzare il codice sorgente [Affichage/Code source]:

1
2
3
4
5
6
7
8
9
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>404 Not Found</TITLE>
</HEAD><BODY>
<H1>Not Found</H1>
The requested URL /inconnu was not found on this server.<P>
<HR>
<ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
</BODY></HTML>

Otteniamo le righe 13-22 ricevute dal nostro client putty. L’utilità di quest’ultimo è quella di mostrarci, in aggiunta, le intestazioni HTTP della risposta. È possibile ottenerle anche con Firefox.

11.4.3. Il protocollo SMTP (Simple Mail Transfer Protocol)

I server SMTP operano generalmente sulla porta 25 [2]. Ci si connette al server [1]. In questo caso, di solito è necessario scegliere un server

appartenente allo stesso dominio IP del computer, poiché molto spesso i server SMTP sono configurati per accettare solo le richieste provenienti da computer appartenenti allo stesso dominio. Inoltre, capita abbastanza spesso che i firewall o gli antivirus dei computer personali siano configurati in modo da non accettare connessioni alla porta 25 da un computer esterno. Potrebbe quindi essere necessario riconfigurare [3] tale firewall o antivirus.

La finestra di dialogo SMTP nella finestra del client putty è la seguente:

220 neuf-infra-smtp-out-sp604001av.neufgp.fr neuf telecom Service relais mail ready
HELO istia.univ-angers.fr
250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
mail from: @expéditeur
250 2.1.0 <@expéditeur> sender ok
rcpt to: @destinataire
250 2.1.5 <@destinataire> destinataire ok
data
354 enter mail, end with "." on a line by itself
ligne1
ligne2
.
250 2.0.0 LwiU1Z00V4AoCxw0200000 message ok
quit
221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection

Di seguito (D) è una richiesta del client, (R) una risposta del server.

  • riga 1: (R) messaggio di benvenuto del server SMTP
  • riga 2: (D) comando HELO per salutare
  • riga 3: (R) risposta del server
  • riga 4: (D) indirizzo del mittente, ad esempio mail from: someone@gmail.com
  • riga 5: (R) risposta del server
  • riga 6: (D) indirizzo del destinatario, ad esempio rcpt to: someoneelse@gmail.com
  • riga 7: (R) risposta del server
  • riga 8: (D) indica l'inizio del messaggio
  • riga 9: (R) risposta del server
  • righe 10-12: (D) il messaggio da inviare, terminato da una riga contenente solo un punto.
  • riga 13: (R) risposta del server
  • riga 14: (D) il client segnala di aver terminato
  • riga 15: (R) risposta del server, che successivamente chiude la connessione

11.4.4. Il protocollo POP (Post Office Protocol)

I server POP operano generalmente sulla porta 110 [2]. Ci si connette al server [1]. Il dialogo POP nella finestra del client putty è il seguente:

+OK Hello there.
user xx
+OK Password required.
pass yy
+OK logged in.
list
+OK POP3 clients that break here, they violate STD53.
1 10105
2 55875
...
64 1717
.
retr 64
+OK 1717 octets follow.
Return-Path: <xx@neuf.fr>
X-Original-To: xx@univ-angers.fr
Delivered-To: xx@univ-angers.fr
....
Date: Sat,  3 May 2008 10:59:25 +0200 (CEST)
From: xx@neuf.fr
To: undisclosed-recipients:;

ligne1
ligne2
.
quit
+OK Bye-bye.
  • riga 1: (R) messaggio di benvenuto del server POP
  • riga 2: (D) il client fornisce il proprio identificativo POP, c.a.d. L'account con cui legge la propria posta
  • riga 3: (R) la risposta del server
  • riga 4: (D) la password del cliente
  • riga 5: (R) la risposta del server
  • riga 6: (D) il client richiede l'elenco dei propri messaggi
  • righe 7-12: (R) l'elenco dei messaggi presenti nella casella di posta del cliente, nel formato [N° du message taille en octets du message]
  • riga 13: (D) si richiede il messaggio n. 64
  • righe 14-25: (R) il messaggio n. 64 con le righe 15-22 che contengono le intestazioni del messaggio e le righe 23-24 che contengono il corpo del messaggio.
  • riga 26: (D) il client indica di aver terminato
  • riga 27: (R) risposta del server che provvederà quindi a chiudere la connessione.

11.4.5. Il protocollo FTP (File Transfer Protocol)

Il protocollo FTP è più complesso di quelli presentati in precedenza. Per scoprire le righe di testo scambiate tra il client e il server, è possibile utilizzare uno strumento come FileZilla [http://www.filezilla.fr/].

FileZilla è un client FTP che offre un'interfaccia Windows per il trasferimento di file. Le azioni dell’utente sull’interfaccia di Windows vengono tradotte in comandi FTP che vengono registrati in [1]. È un buon modo per scoprire i comandi del protocollo FTP.

11.5. Le classi .NET della programmazione web

11.5.1. Scegliere la classe adatta

Il framework .NET offre diverse classi per lavorare con la rete:

  • La classe Socket è quella che opera più a livello di rete. Consente una gestione dettagliata della connessione di rete. Il termine socket indica una presa di corrente. Il termine è stato esteso per indicare una presa di rete software. In una comunicazione TCP-IP tra due macchine A e B, sono due sockets a comunicare tra loro. Un’applicazione può operare direttamente con i sockets. È il caso dell’applicazione A sopra citata. Un socket può essere un socket client o serveur.
  • Se si desidera operare a un livello meno dettagliato rispetto a quello della classe Socket, è possibile utilizzare le classi
  • TcpClient per creare un client TCP
  • TcpListener per creare un server TCP

Queste due classi offrono all’applicazione che le utilizza una visione più semplice della comunicazione di rete, gestendo per suo conto i dettagli tecnici relativi alla gestione dei socket.

  • .NET offre classi specifiche per determinati protocolli:
  • la classe SmtpClient per gestire il protocollo SMTP di comunicazione con un server SMTP per l’invio di e-mail
  • la classe WebClient per gestire i protocolli HTTP o FTP di comunicazione con un server web.

Va notato che la classe Socket è di per sé sufficiente per gestire qualsiasi comunicazione TCP/IP, ma si cercherà innanzitutto di utilizzare le classi di livello superiore per facilitare la scrittura dell’applicazione TCP/IP.

11.5.2. La classe TcpClient

La classe TcpClient è quella più adatta nella maggior parte dei casi per creare il client di un servizio TCP. Tra i suoi costruttori C, metodi M e proprietà P, presenta i seguenti:

TcpClient(string hostname, int port)
C
crea una connessione TCP con il servizio in esecuzione sulla porta specificata (port) del computer specificato (hostname). Ad esempio, new TcpClient("istia.univ-angers.fr",80) per connettersi alla porta 80 del computer istia.univ-angers.fr
Socket Client
P
il socket utilizzato dal client per comunicare con il server.
NetworkStream GetStream()
M
ottiene un flusso di lettura e scrittura verso il server. È questo flusso che consente gli scambi tra client e server.
void Close()
M
chiude la connessione. Vengono chiusi anche il socket e il flusso NetworkStream
bool Connected()
P
vero se la connessione è stata stabilita

La classe NetworkStream rappresenta il flusso di rete tra il client e il server. Deriva dalla classe Stream. Molte applicazioni client-server scambiano righe di testo terminate dai caratteri di fine riga "\r\n". È quindi utile utilizzare gli oggetti StreamReader e StreamWriter per leggere e scrivere tali righe nel flusso di rete. Pertanto, se una macchina M1 ha stabilito una connessione con una macchina M2 tramite un oggetto TcpClient client1 e le due macchine scambiano righe di testo, potrà creare i propri flussi di lettura e scrittura nel modo seguente:

StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=new StreamWriter(client1.GetStream());
out1.AutoFlush=true;

L'istruzione

out1.AutoFlush=true;

significa che il flusso di scrittura di client1 non passerà attraverso un buffer intermedio, ma andrà direttamente in rete. Questo punto è importante. In generale, quando client1 invia una riga di testo al proprio partner, si aspetta una risposta. Questa non arriverà mai se la riga è stata in realtà memorizzata nel buffer sulla macchina M1 e non è mai stata inviata alla macchina M2.

Per inviare una riga di testo alla macchina M2, si scriverà:

client1.WriteLine("un texte");

Per leggere la risposta di M2, si scriverà:

string réponse=client1.ReadLine();

Ora disponiamo degli elementi necessari per definire l’architettura di base di un client Internet dotato del seguente protocollo di comunicazione di base con il server:

  • il client invia una richiesta contenuta in una singola riga
  • il server invia una risposta contenuta in una singola riga

using System;
using System.IO;
using System.Net.Sockets;

namespace ... {
    class ... {
        static void Main(string[] args) {
            ...
            try {
                // si effettua la connessione al servizio
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flusso di output non bufferizzato
                                writer.AutoFlush = true;
                                // ciclo richiesta-risposta
                                while (true) {
                                    // la richiesta proviene dalla tastiera
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // finito?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // si invia la richiesta al server
                                    writer.WriteLine(demande);
                                    // si legge la risposta del server
                                    réponse = reader.ReadLine();
                                    // si elabora la risposta
                                    ...
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // errore
                ...
            }
        }
    }
}
  • riga 11: creazione della connessione da parte del client - la clausola using garantisce che le risorse ad essa associate vengano liberate all'uscita da using.
  • riga 12: apertura del flusso di rete in una clausola using
  • riga 13: creazione e gestione del flusso di lettura in una clausola using
  • riga 14: creazione e utilizzo del flusso di scrittura in una clausola using
  • riga 16: non bufferizzare il flusso di uscita
  • righe 18-31: il ciclo richiesta client / risposta server
  • riga 26: il client invia la sua richiesta al server
  • riga 28: il client attende la risposta del server. Si tratta di un'operazione bloccante, come quella di lettura dalla tastiera. L'attesa termina con l'arrivo di una stringa terminata da "\n" oppure con la fine del flusso. Ciò si verificherà se il server chiude la connessione che ha aperto con il client.

11.5.3. La classe TcpListener

La classe TcpListener è quella più adatta nella maggior parte dei casi per creare un servizio TCP. Tra i suoi costruttori C, metodi M e proprietà P, presenta i seguenti:

TcpListener(int port)
C
crea un servizio TCP che ascolterà (listen) le richieste dei client su una porta passata come parametro (port), denominata porta di ascolto. Se la macchina è connessa a più reti IP, il servizio ascolta su ciascuna di esse.
TcpListener(IPAddress ip, int port)
C
idem, ma l'ascolto avviene solo sull'indirizzo IP specificato.
void Start()
M
avvia l'ascolto delle richieste dei client
TcpClient AcceptTcpClient()
M
accetta la richiesta di un cliente. A questo punto apre una nuova connessione con il cliente, denominata connessione di servizio. La porta utilizzata sul lato server è casuale e viene scelta dal sistema. Viene chiamata porta di servizio. AcceptTcpClient restituisce come risultato l'oggetto TcpClient associato sul lato server alla connessione di servizio.
void Stop()
M
interrompe l’ascolto delle richieste dei client
Socket Server
P
il socket di ascolto del server

La struttura di base di un server TCP che comunicherebbe con i propri clienti secondo il seguente protocollo:

  • il cliente invia una richiesta contenuta in un'unica riga
  • il server invia una risposta contenuta in una singola riga

potrebbe essere simile a questa:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;

namespace ... {
    public class ... {
            ...
            // si crea il servizio di ascolto
            TcpListener ecoute = null;
            try {
                // si crea il servizio - ascolterà su tutte le interfacce di rete della macchina
                ecoute = new TcpListener(IPAddress.Any, port);
                // si avvia il servizio
                ecoute.Start();
                // ciclo di servizio
                TcpClient tcpClient = null;
                // ciclo infinito - verrà interrotto con Ctrl-C
                while (true) {
                    // in attesa di un client
                    tcpClient = ecoute.AcceptTcpClient();
                    // il servizio è gestito da un altro processo
                    ThreadPool.QueueUserWorkItem(Service, tcpClient);
                    // cliente successivo
                }
            } catch (Exception ex) {
                // viene segnalato l'errore
                ...
            } finally {
                // fine del servizio
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // fornisce il servizio a un cliente
        public static void Service(Object infos) {
            // si recupera il cliente da servire
            Client client = infos as Client;
            // gestione del collegamento TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flusso di uscita non bufferizzato
                                writer.AutoFlush = true;
                                // ciclo di lettura richiesta/scrittura risposta
                                bool fini=false;
                                while (! fini) != null) {
                                    // attesa richiesta cliente - operazione bloccante
                                    demande=reader.ReadLine();
                                    // preparazione della risposta
                                    réponse=...;
                                    // invio della risposta al cliente
                                    writer.WriteLine(réponse);
                                    // richiesta successiva
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // errore
                ...
            } finally {
                // fine cliente
                ...
            }
        }
    }
}
  • riga 14: viene creato il servizio di ascolto per una determinata porta e un determinato indirizzo IP. Va ricordato che una macchina ha almeno due indirizzi IP: l’indirizzo “127.0.0.1”, che è il suo indirizzo di loopback, e l’indirizzo “I1.I2.I3.I4” che le è assegnato sulla rete a cui è connessa. Può avere altri indirizzi IP se è connessa a più reti IP. IPAddress.Any indica tutti gli indirizzi IP di una macchina.
  • riga 16: il servizio di ascolto si avvia. Era già stato creato in precedenza, ma non era ancora in ascolto. Essere in ascolto significa attendere le richieste dei client.
  • righe 20-26: il ciclo di attesa della richiesta del cliente / servizio al cliente viene ripetuto per ogni nuovo cliente
  • riga 22: la richiesta di un cliente viene accettata. Il metodo AcceptTcpClient restituisce un'istanza TcpClient detta di servizio:
    • il cliente ha effettuato la richiesta con la propria istanza TcpClient lato client, che chiameremo TcpClientDemande
    • il server accetta questa richiesta con AcceptTcpClient. Questo metodo crea un'istanza TcpClient sul lato server, che chiameremo TcpClientService. Si ha quindi una connessione TCP aperta con alle due estremità le istanze TcpClientDemande <--> TcpClientService.
    • La comunicazione client/server che avviene successivamente si svolge su questa connessione. Il servizio di ascolto non interviene più.
  • riga 24: affinché il server possa gestire più client contemporaneamente, il servizio è gestito da thread, 1 thread per ogni client.
  • riga 32: il servizio di ascolto viene chiuso
  • riga 38: il metodo eseguito dal thread di servizio per un cliente. Riceve come parametro l’istanza TcpClient già connessa al cliente che deve essere servito.
  • righe 38-71: si ritrova un codice simile a quello del client TCP di base studiato in precedenza.

11.6. Esempi di client/server TCP

11.6.1. Un server echo

Ci proponiamo di scrivere un server echo che verrà avviato da una finestra DOS tramite il comando:

ServeurEcho porta

Il server opera sulla porta specificata come parametro. Si limita a rinviare al client la richiesta che quest’ultimo gli ha inviato. Il programma è il seguente:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;

// chiamata: serveurEcho porta
// server di eco
// restituisce al cliente la riga che questi gli ha inviato

namespace Chap9 {
    public class ServeurEcho {
        public const string syntaxe = "Syntaxe : [serveurEcho] port";

        // programma principale
        public static void Main(string[] args) {

            // c'è un argomento?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }
            // questo argomento deve essere un numero intero >0
            int port = 0;
            if (!int.TryParse(args[0], out port) || port<=0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // si crea il servizio di ascolto
            TcpListener ecoute = null;
            int numClient = 0;    // numero cliente successivo
            try {
                // si crea il servizio: ascolterà su tutte le interfacce di rete della macchina
                ecoute = new TcpListener(IPAddress.Any, port);
                // si avvia il servizio
                ecoute.Start();
                // monitoraggio
                Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
                // thread di servizio
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);
                // ciclo di servizio
                TcpClient tcpClient = null;
                // ciclo infinito - verrà interrotto con Ctrl-C
                while (true) {
                    // in attesa di un client
                    tcpClient = ecoute.AcceptTcpClient();
                    // il servizio è gestito da un altro processo
                    ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // cliente successivo
                    numClient++;
                }
            } catch (Exception ex) {
                // viene segnalato l'errore
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            } finally {
                // fine del servizio
                ecoute.Stop();
            }
        }

        // -------------------------------------------------------
        // fornisce il servizio a un cliente del server di eco
        public static void Service(Object infos) {
            // si recupera il cliente da servire
            Client client = infos as Client;
            // fornisce il servizio al cliente
            Console.WriteLine("Début de service au client {0}", client.NumClient);
            // gestione della connessione TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flusso di uscita non bufferizzato
                                writer.AutoFlush = true;
                                // ciclo di lettura richiesta/scrittura risposta
                                string demande = null;
                                while ((demande = reader.ReadLine()) != null) {
                                    // monitoraggio console
                                    Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    // eco della richiesta al client
                                    writer.WriteLine("[{0}]", demande);
                                    // monitoraggio console
                                    Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
                                    // il servizio si arresta quando il client invia "bye"
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // errore
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                // fine client
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }
    }

    // informazioni sul client
    internal class Client {
        public TcpClient CanalTcp { get; set; }        // connessione con il client
        public int NumClient { get; set; }            // numero cliente
    }
}

La struttura del server echo è conforme all'architettura di base dei server TCP illustrata in precedenza. Commenteremo solo la parte relativa al «servizio al cliente»:

  • riga 79: viene letta la richiesta del cliente
  • riga 83: viene restituita al client racchiusa tra parentesi quadre
  • riga 79: il servizio si arresta quando il client chiude la connessione

In una finestra DOS, utilizziamo l'eseguibile del progetto C#:

...\Chap9\02\bin\Release>dir
 03/05/2008  11:46             7 168 ServeurEcho.exe
...>ServeurEcho 100
Serveur d'écho lancé sur le port 0.0.0.0:100

Avviamo quindi due client putty che colleghiamo alla porta 100 del computer localhost:

 

L'output della console del server di eco diventa:

1
2
3
Serveur d'écho lancé sur le port 0.0.0.0:100
Début de service au client 0
Début de service au client 1

Il client 1 e poi il client 0 inviano i seguenti testi:

  • [1]: il client n. 1
  • [2]: il client n. 0
  • [3]: la console del server di eco
  • in [4]: il client 1 si disconnette con il comando bye.
  • in [5]: il server lo rileva

Il server può essere arrestato con Ctrl-C. Il client n. 0 lo rileva quindi con [6].

11.6.2. Un client per il server di eco

Ora scriviamo un client per il server precedente. Verrà chiamato come segue:

ClientEcho nomServeur porta

Si connette alla macchina nomServeur sulla porta port, quindi invia al server delle righe di testo che quest’ultimo gli restituisce in eco.


using System;
using System.IO;
using System.Net.Sockets;

namespace Chap9 {
    // si connette a un server di eco
    // ogni riga digitata sulla tastiera viene ricevuta in eco
    class ClientEcho {
        static void Main(string[] args) {
            // sintassi
            const string syntaxe = "pg machine port";

            // numero di argomenti
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // si annota il nome del server
            string serveur = args[0];

            // la porta deve essere un numero intero maggiore di 0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }

            // è possibile procedere
            string demande = null;        // richiesta del client
            string réponse = null;        // risposta del server
            try {
                // ci si connette al servizio
                using (TcpClient tcpClient = new TcpClient(serveur, port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flusso di uscita non bufferizzato
                                writer.AutoFlush = true;
                                // ciclo richiesta-risposta
                                while (true) {
                                    // la richiesta proviene dalla tastiera
                                    Console.Write("Demande (bye pour arrêter) : ");
                                    demande = Console.ReadLine();
                                    // finito?
                                    if (demande.Trim().ToLower() == "bye")
                                        break;
                                    // si invia la richiesta al server
                                    writer.WriteLine(demande);
                                    // si legge la risposta del server
                                    réponse = reader.ReadLine();
                                    // si sta elaborando la risposta
                                    Console.WriteLine("Réponse : {0}", réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // errore
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

La struttura di questo client è conforme all'architettura generale di base proposta per i client Tcp. Ecco i risultati ottenuti con la seguente configurazione:

  • il server è avviato sulla porta 100 in una finestra DOS
  • sulla stessa macchina sono avviati due client in altre due finestre DOS

Nella finestra del client A (n. 0) si osservano le seguenti visualizzazioni:

1
2
3
4
5
6
...\Chap9\03\bin\Release>ClientEcho localhost 100
Demande (bye pour arrêter) : ligne1A
Réponse : [ligne1A]
Demande (bye pour arrêter) : ligne2A
Réponse : [ligne2A]
Demande (bye pour arrêter) :

In quella del client B (n. 1):

1
2
3
4
5
6
...\Chap9\03\bin\Release>ClientEcho localhost 100
Demande (bye pour arrêter) : ligne1B
Réponse : [ligne1B]
Demande (bye pour arrêter) : ligne2B
Réponse : [ligne2B]
Demande (bye pour arrêter) :

In quella del server:

...\Chap9\02\bin\Release>ServeurEcho 100
Serveur d'écho lancé sur le port 0.0.0.0:100
Début de service au client 0
<--- Client 0 : ligne1A
---> Client 0 : ligne1A
<--- Client 0 : ligne2A
---> Client 0 : ligne2A
Début de service au client 1
<--- Client 1 : ligne1B
---> Client 1 : ligne1B
<--- Client 1 : ligne2B
---> Client 1 : ligne2B

Il client A n. 0 si disconnette:

1
2
3
4
Demande (bye pour arrêter) : ligne1A
Réponse : [ligne1A]
...
Demande (bye pour arrêter) : bye

La console del server:

1
2
3
Serveur d'écho lancé sur le port 0.0.0.0:100
...
Fin du service au client 0

11.6.3. Un client generico TCP

Scriveremo un client TCP generico che verrà avviato nel modo seguente: ClientTcpGenerique porta del server. Funzionerà in modo analogo al client PuTTY, ma avrà un'interfaccia a console e non presenterà opzioni di configurazione.

Nell’applicazione precedente, il protocollo di comunicazione era noto: il client inviava una sola riga e il server rispondeva con una sola riga. Ogni servizio ha il proprio protocollo specifico e si possono verificare anche le seguenti situazioni:

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

Pertanto, il ciclo «invio di una singola riga al server / ricezione di una singola riga inviata dal server» non è sempre adeguato. Per gestire protocolli più complessi di quello echo, il client TCP generico disporrà di due thread:

  • il thread principale leggerà le righe di testo digitate sulla tastiera e le invierà al server.
  • un thread secondario lavorerà in parallelo e sarà dedicato alla lettura delle righe di testo inviate dal server. Non appena ne riceve una, la visualizza sulla console. Il thread si interrompe solo quando il server chiude la connessione. Funziona quindi in modo continuo.

Il codice è il seguente:


using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;

namespace Chap9 {
    // riceve come parametro le caratteristiche di un servizio nel formato: server porta
    // si connette al servizio
    // invia al server ogni riga digitata sulla tastiera
    // crea un thread per leggere in modo continuo le righe di testo inviate dal server
    class ClientTcpGenerique {
        static void Main(string[] args) {
            // sintassi
            const string syntaxe = "pg serveur port";

            // numero di argomenti
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // si annota il nome del server
            string serveur = args[0];

            // la porta deve essere un numero intero maggiore di 0
            int port = 0;
            if (!int.TryParse(args[1], out port) || port <= 0) {
                Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
                return;
            }
            // ci si connette al servizio
            TcpClient tcpClient = null;
            try {
                tcpClient = new TcpClient(serveur, port);
            } catch (Exception ex) {
                // errore
                Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
                // fine
                return;
            }

            // si avvia un thread separato per leggere le righe di testo inviate dal server
            ThreadPool.QueueUserWorkItem(Receive, tcpClient);

            // la lettura dei comandi da tastiera avviene nel thread principale
            Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
            string demande = null;        // richiesta del client
            try {
                // si gestisce la connessione del client
                using (tcpClient) {
                    // si crea un flusso di scrittura verso il server
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamWriter writer = new StreamWriter(networkStream)) {
                            // flusso di uscita non bufferizzato
                            writer.AutoFlush = true;
                            // ciclo richiesta-risposta
                            while (true) {
                                demande = Console.ReadLine();
                                // finito?
                                if (demande.Trim().ToLower() == "bye")
                                    break;
                                // si invia la richiesta al server
                                writer.WriteLine(demande);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // errore
                Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
            }
        }

        // thread di lettura client <-- server
        public static void Receive(object infos) {
            // dati locali
            string réponse = null;    // risposta del server
            // creazione del flusso di input
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // ciclo di lettura continua delle righe di testo del flusso di input
                            while ((réponse = reader.ReadLine()) != null) {
                                // visualizzazione console
                                Console.WriteLine("<-- {0}", réponse);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // errore
                Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // viene segnalata la fine del thread di lettura
                Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
            }
        }
    }
}
  • riga 34: il client si connette al server
  • riga 43: viene avviato un thread per la lettura delle righe di testo dal server. Deve eseguire il metodo Receive della riga 73. A questo metodo viene passata l’istanza TcpClient che è stata connessa al server.
  • righe 57-64: il ciclo di acquisizione dei comandi da tastiera / invio dei comandi al server. L'acquisizione dei comandi da tastiera è gestita dal thread principale.
  • righe 75-98: il metodo Receive eseguito dal thread di lettura delle righe di testo. Questo metodo riceve come parametro l’istanza TcpClient che è stata connessa al server.
  • righe 84-87: il ciclo continuo di lettura delle righe di testo inviate dal server. Si interrompe solo quando il server chiude la connessione aperta con il client.

Di seguito sono riportati alcuni esempi che riprendono quelli utilizzati con il client putty nel paragrafo 11.4. Il client viene eseguito in una console DOS.

Protocollo HTTP

...\Chap9\04\bin\Release>ClientTcpGenerique istia.univ-angers.fr 80
Tapez vos commandes (bye pour arrêter) :
GET /inconnu HTTP/1.1
Host: istia.univ-angers.fr:80
Connection: Close

<-- HTTP/1.1 404 Not Found
<-- Date: Sat, 03 May 2008 12:35:11 GMT
<-- Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29

<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html; charset=iso-8859-1
<--
<-- 11a
<-- <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<-- <HTML><HEAD>
<-- <TITLE>404 Not Found</TITLE>
<-- </HEAD><BODY>
<-- <H1>Not Found</H1>
<-- The requested URL /inconnu was not found on this server.<P>
<-- <HR>
<-- <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
<-- </BODY></HTML>
<--
<-- 0
<--
[Fin du thread de lecture des réponses du serveur]
bye

...\Chap9\04\bin\Release>

Si invita il lettore a rileggere le spiegazioni fornite al paragrafo 11.4.2. Ci limitiamo a commentare solo gli aspetti specifici dell'applicazione:

  • riga 28: dopo l'invio della riga 27, il server HTTP ha chiuso la connessione, provocando la chiusura del thread di lettura. Il thread principale, che legge i comandi digitati dalla tastiera, rimane invece attivo. Il comando della riga 29, digitato dalla tastiera, lo arresta.

Protocollo SMTP

...\Chap9\04\bin\Release>ClientTcpGenerique smtp.neuf.fr 25
Tapez vos commandes (bye pour arrêter) :
<-- 220 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom Service relais mail ready
HELO istia.univ-angers.fr
<-- 250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
mail from: xx@neuf.fr
<-- 250 2.1.0 <xx@neuf.fr> sender ok
rcpt to: yy@univ-angers.fr
<-- 250 2.1.5 <yy@univ-angers.fr> destinataire ok
data
<-- 354 enter mail, end with "." on a line by itself
ligne1
ligne2
.
<-- 250 2.0.0 M0jL1Z0044AoCxw0200000 message ok
quit
<-- 221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection
[Fin du thread de lecture des réponses du serveur]
bye

...\Chap9\04\bin\Release>

Si invita il lettore a rileggere le spiegazioni fornite al paragrafo 11.4.3 e a provare gli altri esempi utilizzati con il client putty.

11.6.4. Un server TCP generico

Ora ci concentriamo su un server

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

Il programma viene avviato in una finestra DOS con il comando: ServeurTcpGenerique portEcoute, dove portEcoute è la porta a cui i client devono connettersi. Il servizio ai client sarà gestito da due thread:

  • il thread principale che:
    • gestirà i client uno dopo l’altro e non in parallelo;
    • leggerà le righe digitate dall'utente sulla tastiera e le invierà al client. L'utente segnalerà, tramite il comando «bye», che sta chiudendo la connessione con il client. Poiché la console non può essere utilizzata per due client contemporaneamente, il nostro server gestisce un solo client alla volta.
  • un thread secondario dedicato esclusivamente alla lettura delle righe di testo inviate dal cliente

Il server, dal canto suo, non si arresta mai, a meno che l’utente non prema Ctrl-C sulla tastiera.

Vediamo alcuni esempi. Il server viene avviato sulla porta 100 e si utilizza il client generico di paragraphe11.6.3 per comunicare con esso. La finestra del client è la seguente:

1
2
3
4
5
6
7
...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
Tapez vos commandes (bye pour arrêter) :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
bye

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

...\Chap9\05\bin\Release>ServeurTcpGenerique 100
Serveur générique lancé sur le port 0.0.0.0:100
Client 127.0.0.1:4165
Tapez vos commandes (bye pour arrêter) :
<-- commande 1 du client 1
réponse 1 au client 1
<-- commande 2 du client 1
réponse 2 au client 1
[Fin du thread de lecture des demandes du client]
bye

Le righe che iniziano con <-- sono quelle inviate dal client al server, le altre sono quelle inviate dal server al client. La riga 9 indica che il thread di lettura delle richieste del client si è arrestato. Il thread principale del server è ancora in attesa di comandi digitati sulla tastiera da inviare al client. È quindi necessario digitare sulla tastiera il comando bye della riga 10 per passare al client successivo. Il server è ancora attivo mentre il client 1 ha terminato. Si avvia un secondo client per lo stesso server:

1
2
3
4
5
...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
Tapez vos commandes (bye pour arrêter) :
commande 3 du client 2
<-- réponse 3 au client 2
bye

La finestra del server appare quindi così:

1
2
3
4
5
6
Tapez vos commandes (bye pour arrêter) :
Client 127.0.0.1:4166
<-- commande 3 du client 2
réponse 3 au client 2
[Fin du thread de lecture des demandes du client]
bye

Dopo la riga 6 sopra riportata, il server è in attesa di un nuovo client. È possibile arrestarlo con Ctrl-C.

Simuliamo ora un server web avviando il nostro server generico sulla porta 88:

1
2
3
...\Chap9\05\bin\Release>ServeurTcpGenerique 88

Serveur générique lancé sur le port 0.0.0.0:88

Apriamo ora un browser e richiediamo l'http://localhost:88/exemple.html URL. Il browser si connetterà quindi alla porta 88 della macchina localhost e richiederà la pagina /exemple.html:

 

Diamo ora un’occhiata alla finestra del nostro server:

Serveur générique lancé sur le port 0.0.0.0:88
Client 127.0.0.1:4167
Tapez vos commandes (bye pour arrêter) :
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
ication, application/x-silverlight, */*
<-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
<-- UA-CPU: x86
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Notiamo le intestazioni HTTP inviate dal browser. Questo ci permette di individuare altre intestazioni HTTP oltre a quelle già incontrate. Elaboriamo una risposta per il nostro client. L’utente alla tastiera è qui il vero server e può elaborare una risposta manualmente. Ricordiamo la risposta fornita da un server Web in un esempio precedente:

HTTP/1.1 200 OK
Date: Sat, 03 May 2008 07:53:47 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=0d2e64b317; path=/
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1

693f
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
         <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
....
         </html>
0

Proviamo a fornire una risposta analoga limitandoci allo stretto necessario:

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

<html>
<head><title>Serveur generique</title></head>
<body><h2>Reponse du serveur generique</h2></body>
</html>
bye
Flux de lecture des lignes de texte du client : l'erreur suivante s'est produite : Unable to read data from the transport connection: Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall.
[Fin du thread de lecture des demandes du client]

Nella nostra risposta ci siamo limitati alle intestazioni HTTP delle righe 1-4. Non forniamo la dimensione del documento che stiamo per inviare (Content-Length), ma ci limitiamo a indicare che chiuderemo la connessione (Connection: close) dopo averlo inviato. Questo è sufficiente per il browser. Vedendo che la connessione è stata chiusa, capirà che la risposta del server è terminata e visualizzerà la pagina HTML che gli è stata inviata. Quest’ultima è quella delle righe 6-9. L'utente alla tastiera chiude quindi la connessione con il client digitando il comando bye, riga 10. A seguito di questo comando da tastiera, il thread principale chiude la connessione con il client. Ciò provoca l'eccezione della riga 11. Il thread di lettura delle righe di testo del client è stato interrotto bruscamente dalla chiusura del collegamento con il client e ha generato un'eccezione. Dopo la riga 12, il server entra in stato di attesa di un nuovo client.

Il browser del cliente ora visualizza quanto segue:

Se, come sopra, si esegue Affichage/Source per vedere cosa ha ricevuto il browser, si ottiene [2], ovvero esattamente ciò che è stato inviato dal server generico.

Il codice del server generico TCP è il seguente:


using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Chap9 {
    public class ServeurTcpGenerique {
        public const string syntaxe = "Syntaxe : ServeurGénérique Port";

        // programma principale
        public static void Main(string[] args) {

            // c'è un argomento?
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                Environment.Exit(1);
            }
            // questo argomento deve essere un numero intero >0
            int port = 0;
            if (!int.TryParse(args[0], out port) || port <= 0) {
                Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
                Environment.Exit(2);
            }
            // si crea il servizio di ascolto
            TcpListener ecoute = null;
            try {
                // si crea il servizio
                ecoute = new TcpListener(IPAddress.Any, port);
                // lo si avvia
                ecoute.Start();
                // monitoraggio
                Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
                while (true) {
                    // attesa di un client
                    Console.WriteLine("Attente du client suivant...");
                    TcpClient tcpClient = ecoute.AcceptTcpClient();
                    Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
                    // si avvia un thread separato per leggere le righe di testo inviate dal cliente
                    ThreadPool.QueueUserWorkItem(Receive, tcpClient);
                    // la lettura dei comandi da tastiera avviene nel thread principale
                    Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
                    string réponse = null;        // risposta del server
                    // si gestisce la connessione del client
                    using (tcpClient) {
                        // si crea un flusso di scrittura verso il client
                        using (NetworkStream networkStream = tcpClient.GetStream()) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flusso di output non bufferizzato
                                writer.AutoFlush = true;
                                // ciclo di acquisizione delle risposte dalla tastiera
                                while (true) {
                                    réponse = Console.ReadLine();
                                    // finito?
                                    if (réponse.Trim().ToLower() == "bye")
                                        break;
                                    // si invia la richiesta al client
                                    writer.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // si segnala l'errore
                Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
            } finally {
                // fine dell'ascolto
                ecoute.Stop();
            }
        }

        // thread di lettura server <-- client
        public static void Receive(object infos) {
            // dati locali
            string demande = null;    // richiesta del cliente
            string idClient=null;    // identità del client

            // gestione della connessione del cliente
            try {
                using (TcpClient tcpClient = infos as TcpClient) {
                    // identità del cliente
                    idClient = tcpClient.Client.RemoteEndPoint.ToString();
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            // ciclo di lettura continua delle righe di testo del flusso di input
                            while ((demande = reader.ReadLine()) != null) {
                                // visualizzazione console
                                Console.WriteLine("<-- {0}", demande);
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                // errore
                Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
            } finally {
                // viene segnalata la fine del thread di lettura
                Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
            }
        }
    }
}
  • riga 29: il servizio di ascolto viene creato ma non avviato. Ascolta tutte le interfacce di rete della macchina.
  • riga 31: il servizio di ascolto viene avviato
  • riga 34: ciclo infinito di attesa dei client. L'utente arresterà il server con Ctrl-C.
  • riga 37: attesa di un cliente - operazione bloccante. Quando il cliente arriva, l'istanza TcpClient restituita dal metodo AcceptTcpClient rappresenta il lato server di una connessione aperta con il cliente.
  • riga 40: il flusso di lettura delle richieste del cliente viene affidato a un thread separato.
  • riga 45: utilizzo della connessione al cliente in una clausola using per garantire che venga chiusa in ogni caso.
  • riga 47: utilizzo del flusso di rete in una clausola using
  • riga 48: creazione, in una clausola using, di un flusso di scrittura sul flusso di rete
  • riga 50: il flusso di scrittura non sarà bufferizzato
  • righe 52-59: ciclo di immissione da tastiera dei comandi da inviare al client
  • riga 69: fine del servizio di ascolto. Questa istruzione non verrà mai eseguita in questo contesto poiché il server viene arrestato con Ctrl-C.
  • riga 78: il metodo Receive che visualizza in continuo sulla console le righe di testo inviate dal client. Qui ritroviamo quanto visto per il client generico TCP.

11.6.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/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
ication, application/x-silverlight, */*
<-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
<-- UA-CPU: x86
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Scriveremo un client Web a cui passeremo come parametro un URL e che visualizzerà sullo schermo il testo inviato dal server. Supporremo che quest’ultimo supporti il protocollo HTTP 1.1. Tra le intestazioni precedenti, utilizzeremo solo le seguenti:

1
2
3
4
<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
<--
  • la prima intestazione indica il documento desiderato
  • il secondo indica il server interpellato
  • il terzo che desideriamo che il server chiuda la connessione dopo averci risposto.

Se nella riga 1 sopra riportata sostituiamo GET con HEAD, il server ci invierà solo le intestazioni HTTP e non il documento specificato nella riga 1.

Il nostro client web verrà chiamato nel modo seguente: ClientWeb URL cmd, dove URL è l’URL desiderata e cmd una delle due parole chiave GET o HEAD per indicare se si desiderano solo le intestazioni (HEAD) o anche il contenuto della pagina (GET). Vediamo un primo esempio:

...\Chap9\06\bin\Release>ClientWeb http://istia.univ-angers.fr:80 HEAD
HTTP/1.1 200 OK
Date: Sat, 03 May 2008 14:05:24 GMT
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
Set-Cookie: fe_typo_user=e668408ac1; path=/
Connection: close
Content-Type: text/html;charset=iso-8859-1

...\Chap9\06\bin\Release>
  • riga 1: richiediamo solo le intestazioni HTTP (HEAD)
  • righe 2-9: la risposta del server

Se utilizziamo GET al posto di HEAD nella chiamata al client Web, otteniamo lo stesso risultato che con HEAD, con in più il corpo del documento richiesto.

Il codice del client Web è il seguente:


using System;
using System.IO;
using System.Net.Sockets;

namespace Chap9 {
    class ClientWeb {
        static void Main(string[] args) {
            // sintassi
            const string syntaxe = "pg URI GET/HEAD";

            // numero di argomenti
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // si rileva il codice URI richiesto
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // verifica della validità di URI
            if(! stringURI.StartsWith("http://")){
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI non corretto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // verifica dell'ordine
            if (commande != "GET" && commande != "HEAD") {
                // ordine non corretto
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // si sta effettuando la connessione al servizio
                using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flusso di uscita non bufferizzato
                                writer.AutoFlush = true;
                                // richiesta di URL - invio delle intestazioni HTTP
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // si legge la risposta
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != null) {
                                    // visualizzazione della risposta sulla console
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // viene visualizzata l'eccezione
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

L'unica novità in questo programma è l'utilizzo della classe Uri. Il programma riceve un URL (Uniform Resource Locator) o un URI (Uniform Resource Identifier) della forma http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... La classe Uri ci permette di scomporre la stringa URL nei suoi diversi elementi.

  • righe 26-33: viene creato un oggetto Uri a partire dalla stringa stringURI ricevuta come parametro. Se la stringa URI ricevuta come parametro non è un URI valido (mancanza del protocollo, del server, ...), viene generata un'eccezione. Questo ci permette di verificare la validità del parametro ricevuto. Una volta costruito l’oggetto Uri, si ha accesso ai diversi elementi di questa URI. Pertanto, se l’oggetto uri del codice precedente è stato costruito a partire dalla stringa http://serveur:port/document?param1=val1&param2=val2;..., si avrà:
    • uri.Host=serveur,
    • uri.Port=port,
    • uri.Path=document,
    • uri.Query=param1=val1&param2=val2;...,
    • uri.pathAndQuery= cheminPageHTML?param1=val1&param2=val2;...,
    • uri.Scheme=http.

11.6.6. Un client Web che gestisce i reindirizzamenti

Il client Web precedente non gestisce un eventuale reindirizzamento da URL che ha richiesto. Ecco un esempio:

...\Chap9\06\bin\Release>ClientWeb http://www.ibm.com GET
HTTP/1.1 302 Found
Date: Sat, 03 May 2008 14:50:52 GMT
Server: IBM_HTTP_Server
Location: http://www.ibm.com/us/
Content-Length: 206
Kp-eeAlive: timeout=10, max=73
Connection: Keep-Alive
Content-Type: text/html

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.ibm.com/us/">here</a>.</p>
</body></html>
  • riga 2: il codice 302 Found indica un reindirizzamento. L'indirizzo verso cui il browser deve reindirizzarsi si trova nel corpo del documento, alla riga 16.

Un secondo esempio:

...\Chap9\06\bin\Release>ClientWeb http://www.bull.com GET
HTTP/1.1 301 Moved Permanently
Date: Sat, 03 May 2008 14:52:31 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Location: http://www.bull.com/index.php
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html

0
  • riga 2: il codice 301 Moved Permanently indica un reindirizzamento. L'indirizzo verso cui il browser deve reindirizzarsi è indicato alla riga 6, nell'intestazione HTTP Location.

Un terzo esempio:

1
2
3
4
5
6
7
...\Chap9\06\bin\Release>ClientWeb http://www.gouv.fr GET
HTTP/1.1 302 Moved Temporarily
Server: AkamaiGHost
Content-Length: 0
Location: http://www.premier-ministre.gouv.fr/fr/
Date: Sat, 03 May 2008 14:56:53 GMT
Connection: close
  • riga 2: il codice 302 Moved Temporarily indica un reindirizzamento. L'indirizzo verso cui il browser deve reindirizzarsi è indicato alla riga 5, nell'intestazione HTTP Location.

Un quarto esempio con un server IIS locale sul computer:

...\istia\Chap9\06\bin\Release>ClientWeb.exe http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:16:56 GMT
Connection: close
Location: localstart.asp
Content-Length: 121
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQASDQAB=FDJLADLCOLDHGKGNIPMLHIIA; path=/
Cache-control: private
  • riga 2: il codice 302 Object moved indica un reindirizzamento. L'indirizzo verso cui il browser deve reindirizzarsi è indicato alla riga 5, nell'intestazione HTTP Location. Si noti che, a differenza degli esempi precedenti, l’indirizzo di reindirizzamento è relativo. L’indirizzo completo è infatti http://localhost/localstart.asp.

Ci proponiamo di gestire i reindirizzamenti quando la prima riga delle intestazioni HTTP contiene la parola chiave moved (senza distinzione tra maiuscole e minuscole) e l’indirizzo di reindirizzamento si trova nell’intestazione HTTP Location.

Riprendendo gli ultimi tre esempi, otteniamo i seguenti risultati:

URL: http://www.bull.com

...\Chap9\06B\bin\Release>ClientWebAvecRedirection http://www.bull.com HEAD
HTTP/1.1 301 Moved Permanently
Date: Sun, 04 May 2008 10:22:48 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Location: http://www.bull.com/index.php
Connection: close
Content-Type: text/html


<--Redirection vers l'URL http://www.bull.com/index.php-->

HTTP/1.1 200 OK
Date: Sun, 04 May 2008 10:22:49 GMT
Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
X-Powered-By: PHP/4.3.4
Connection: close
Content-Type: text/html
  • riga 11: il reindirizzamento avviene verso l’indirizzo della riga 6

URL: http://www.gouv.fr

...\Chap9\06B\bin\Release>ClientWebAvecRedirect
ion http://www.gouv.fr HEAD
HTTP/1.1 302 Moved Temporarily
Server: AkamaiGHost
Content-Length: 0
Location: http://www.premier-ministre.gouv.fr/fr/
Date: Sun, 04 May 2008 10:30:38 GMT
Connection: close


<--Redirection vers l'URL http://www.premier-ministre.gouv.fr/fr/-->

HTTP/1.1 200 OK
Server: Apache
X-Powered-By: PHP/4.4.1
Last-Modified: Sun, 04 May 2008 10:29:48 GMT
Content-Type: text/html
Expires: Sun, 04 May 2008 10:40:38 GMT
Date: Sun, 04 May 2008 10:30:38 GMT
Connection: close
  • riga 11: il reindirizzamento avviene verso l'indirizzo della riga 6

URL: http://localhost

...\Chap9\06B\bin\Release>ClientWebAvecRedirection.exe http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:37:11 GMT
Connection: close
Location: localstart.asp
Content-Length: 121
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQASDQAB=GDJLADLCJCMPCHFFEJEFPKMK; path=/
Cache-control: private


<--Redirection vers l'URL http://localhost/localstart.asp-->

HTTP/1.1 401 Access Denied
Server: Microsoft-IIS/5.1
Date: Sun, 04 May 2008 10:37:11 GMT
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="localhost"
Connection: close
Content-Length: 4766
Content-Type: text/html
  • riga 13: il reindirizzamento avviene verso l'indirizzo della riga 6
  • riga 15: l'accesso alla pagina http://localhost/localstart.asp ci è stato negato.

Il programma che gestisce il reindirizzamento è il seguente:


using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;

namespace Chap9 {
    class ClientWebAvecRedirection {
        static void Main(string[] args) {
            // sintassi
            const string syntaxe = "pg URI GET/HEAD";

            // numero di argomenti
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // si nota il valore richiesto URI
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // verifica della validità di URI
            if (!stringURI.StartsWith("http://")) {
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI non corretto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // verifica dell'ordine
            if (commande != "GET" && commande != "HEAD") {
                // ordine non corretto
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            const int nbRedirsMax = 1;        // Non è consentito più di un reindirizzamento
            int nbRedirs = 0;                            // numero di reindirizzamenti in corso

            // espressione regolare per individuare un reindirizzamento URL
            Regex location = new Regex(@"^Location: (.+?)$");
            try {
                // è possibile avere più URL da richiedere se sono presenti reindirizzamenti
                while (nbRedirs <= nbRedirsMax) {
                    // gestione dei reindirizzamenti
                    bool redir = false;
                    bool locationFound = false;
                    string locationString = null;
                    // si effettua la connessione al servizio
                    using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
                        using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
                            using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
                                // flusso di uscita non bufferizzato
                                writer.AutoFlush = true;
                                // si richiede l'URL - invio delle intestazioni HTTP
                                writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
                                writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
                                writer.WriteLine("Connection: close");
                                writer.WriteLine();
                                // si legge la prima riga della risposta
                                string premièreLigne = reader.ReadLine();
                                // echo su schermo
                                Console.WriteLine(premièreLigne);

                                // reindirizzamento?
                                if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
                                    // c'è un reindirizzamento
                                    redir = true;
                                    nbRedirs++;
                                }

                                // intestazioni HTTP successive fino a trovare la riga vuota che segnala la fine delle intestazioni
                                string réponse = null;
                                while ((réponse = reader.ReadLine()) != "") {
                                    // si visualizza la risposta
                                    Console.WriteLine(réponse);
                                    // se c'è un reindirizzamento, si cerca l'intestazione Location
                                    if (redir && !locationFound) {
                                        // si confronta la riga corrente con l'espressione relazionale location
                                        Match résultat = location.Match(réponse);
                                        if (résultat.Success) {
                                            // se è stata trovata, si annota il URL di reindirizzamento
                                            locationString = résultat.Groups[1].Value;
                                            // si registra che è stato trovato
                                            locationFound = true;
                                        }
                                    }
                                }

                                // le intestazioni HTTP sono state esaurite - si scrive la riga vuota
                                Console.WriteLine(réponse);
                                // poi si passa al corpo del documento
                                while ((réponse = reader.ReadLine()) != null) {
                                    Console.WriteLine(réponse);
                                }
                            }
                        }
                    }
                    // si è terminato?
                    if (!locationFound || nbRedirs > nbRedirsMax)
                        break;
                    // c'è un reindirizzamento da effettuare - si costruisce la nuova URI
                    try {
                        if (locationString.StartsWith("http")) {
                            // indirizzo http completo
                            uri = new Uri(locationString);
                        } else {
                            // indirizzo http relativo all'URI corrente
                            uri = new Uri(uri, locationString);
                        }
                        // log della console
                        Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
                    } catch (Exception ex) {
                        // problema con l'URI
                        Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
                    }
                }
            } catch (Exception e) {
                // viene visualizzata l'eccezione
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            }
        }
    }
}

Rispetto alla versione precedente, le modifiche sono le seguenti:

  • riga 46: l'espressione regolare per recuperare l'indirizzo di reindirizzamento nell'intestazione HTTP Location: indirizzo.
  • riga 49: il codice che in precedenza veniva eseguito per un singolo Uri ora può essere eseguito in successione per più Uri.
  • riga 66: si legge la prima riga delle intestazioni HTTP inviate dal server. È questa che contiene la parola chiave moved se il documento richiesto è stato spostato.
  • righe 71-75: si verifica se la prima riga contiene la parola chiave moved. In caso affermativo, la si annota.
  • righe 79-93: lettura delle altre intestazioni HTTP fino a quando non si incontra la riga vuota che ne segnala la fine. Se la prima riga indicava un reindirizzamento, ci si sofferma quindi sull’intestazione HTTP Location: indirizzo per memorizzare l’indirizzo di reindirizzamento in locationString.
  • righe 98-100: il resto della risposta del server HTTP viene visualizzato sulla console.
  • righe 105-106: l’URI richiesto è stato completamente elaborato e visualizzato. Se non è necessario effettuare alcun reindirizzamento o se il numero di reindirizzamenti consentiti è stato superato, si esce dal programma.
  • righe 108-122: se è presente un reindirizzamento, si calcola la nuova URI da richiedere. È necessario eseguire alcune operazioni a seconda che l’indirizzo di reindirizzamento trovato fosse assoluto (riga 111) o relativo (riga 114).

11.7. Le classi .NET specializzate in un particolare protocollo Internet

Negli esempi precedenti del client web, il protocollo HTTP veniva gestito con un client TCP. Dovevamo quindi gestire autonomamente il particolare protocollo di comunicazione utilizzato. Avremmo potuto creare, in modo analogo, un client SMTP o POP. Il framework .NET offre classi specializzate per i protocolli HTTP e SMTP. Queste classi conoscono il protocollo di comunicazione tra client e server ed evitano allo sviluppatore di doverlo gestire. Le presentiamo ora.

11.7.1. La classe classeWebClient

Esiste una classe WebClient in grado di dialogare con un server web. Consideriamo l’esempio del client web del paragrafo 11.6.5, trattato qui con la classe WebClient.


using System;
using System.IO;
using System.Net;
namespace Chap9 {
    public class Program {
        public static void Main(string[] args) {
            // sintassi: [prog] URI
            const string syntaxe = "pg URI";

            // numero di argomenti
            if (args.Length != 1) {
                Console.WriteLine(syntaxe);
                return;
            }

            // si rileva la richiesta di URI
            string stringURI = args[0];

            // verifica della validità di URI
            if (!stringURI.StartsWith("http://")) {
                Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
                return;
            }
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI non corretto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }

            try {
                // creazione cliente web
                using (WebClient client = new WebClient()) {
                    // aggiunta di un'intestazione HTTP 
                    client.Headers.Add("user-agent", "st");
                    using (Stream stream = client.OpenRead(uri)) {
                        using (StreamReader reader = new StreamReader(stream)) {
                            // visualizzazione della risposta del server web
                            Console.WriteLine(reader.ReadToEnd());
                            // visualizzazione delle intestazioni di risposta del server
                            Console.WriteLine("---------------------");
                            foreach (string clé in client.ResponseHeaders.Keys) {
                                Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
                            }
                            Console.WriteLine("---------------------");
                        }
                    }
                }
            } catch (WebException e1) {
                Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
            } catch (Exception e2) {
                Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
            }
        }
    }
}
  • riga 35: il client web viene creato ma non è ancora configurato
  • riga 37: si aggiunge un'intestazione HTTP alla richiesta HTTP che verrà effettuata. Scopriremo che altre intestazioni verranno inviate per impostazione predefinita.
  • riga 38: il client web richiede l'URI fornito dall'utente e legge il documento inviato. [WebClient].OpenRead(URI) apre la connessione con Uri e legge la risposta. È proprio questo il vantaggio della classe. Si occupa del dialogo con il server web. Il risultato del metodo OpenRead è di tipo Stream e rappresenta il documento richiesto. Le intestazioni HTTP inviate dal server e che precedono il documento nella risposta non ne fanno parte.
  • riga 39: si utilizza un StreamReader e, alla riga 41, il suo metodo ReadToEnd per leggere l’intera risposta.
  • righe 44-46: vengono visualizzate le intestazioni HTTP della risposta del server. [WebClient].ResponseHeaders rappresenta una collezione con valori le cui chiavi sono i nomi delle intestazioni HTTP e i valori sono le stringhe di caratteri associate a tali intestazioni.
  • riga 51: le eccezioni generate durante uno scambio client/server sono di tipo WebException.

Vediamo alcuni esempi.

Si avvia il server generico TCP creato nel paragrafo 6.4.6:

...\Chap9\05\bin\Release>ServeurTcpGenerique.exe 88
Serveur générique lancé sur le port 0.0.0.0:88

Si avvia il client web precedente nel modo seguente:

...\Chap9\09\bin\Release>09 http://localhost:88

L'URI richiesto è quello del server generico. Quest'ultimo visualizza quindi le intestazioni HTTP che gli sono state inviate dal client web:

1
2
3
4
5
6
7
Client 127.0.0.1:1415
Tapez vos commandes (bye pour arrêter) :
<-- GET / HTTP/1.1
<-- User-Agent: st
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Si osserva quindi che:

  • che il client web invia 3 intestazioni HTTP per impostazione predefinita (righe 3, 5, 6)
  • riga 4: l’intestazione che abbiamo generato noi stessi (riga 37 del codice)
  • che il client web utilizza per impostazione predefinita il metodo GET (riga 3). Esistono altri metodi, tra cui POST e HEAD.

Ora richiediamo una risorsa inesistente:

1
2
3
4
5
...\Chap9\09\bin\Release>09 http://istia.univ-angers.fr/sconosciuto
L'exception suivante s'est produite : System.Net.WebException: The remote server returned an error: (404) Not Found.
   at System.Net.WebClient.OpenRead(Uri address)
   at System.Net.WebClient.OpenRead(String address)
   at Chap9.WebClient1.Main(String[] args) in C:\data\2007-2008\c# 2008\poly\istia\Chap9\09\Program.cs:riga 16
  • riga 2: si è verificata un'eccezione di tipo WebException perché il server ha risposto con il codice 404 Not Found per indicare che la risorsa richiesta non esisteva.

Infine, concludiamo richiedendo una risorsa esistente:

...\istia\Chap9\09\bin\Release>09 http://istia.univ-angers.fr >istia.univ-angers.txt

Il file istia.univ-angers.txt generato dal comando è il seguente:

<!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
...
</html>
---------------------
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html;charset=iso-8859-1
Date: Sun, 04 May 2008 14:30:53 GMT
Set-Cookie: fe_typo_user=22eaaf283a; path=/
Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
X-Powered-By: PHP/4.4.4-8+etch4
---------------------
  • riga 1: il documento HTML richiesto.
  • righe 3-10: le intestazioni della risposta HTTP in un ordine che non è necessariamente quello in cui sono state inviate.

La classe WebClient dispone di metodi che consentono di ricevere un documento (metodi DownLoad) o di inviarne uno (metodi UpLoad):

DownLoadData
per scaricare una risorsa come array di byte (ad esempio un’immagine)
DownLoadFile
per scaricare una risorsa e salvarla in un file locale
DownLoadString
per scaricare una risorsa e recuperarla come stringa di caratteri (ad esempio un file HTML)
OpenWrite
l'equivalente di OpenRead, ma per inviare dati al server
UpLoadData
la controparte di DownLoadData, ma verso il server
UpLoadFile
la controparte di DownLoadFile, ma verso il server
UpLoadString
la controparte di DownLoadString, ma verso il server
UpLoadValues
per inviare al server i dati di un comando POST e recuperarne i risultati sotto forma di un array di byte. Il comando POST richiede un documento trasmettendo al contempo al server le informazioni necessarie per determinare il documento effettivo da inviare. Queste informazioni vengono inviate al server come documento, da cui il nome UpLoad del metodo. Vengono inviate dopo la riga vuota delle intestazioni HTTP nella forma param1=valeur1&param2=valeur2&...:
POST /document HTTP/1.1
...
[ligne vide]
param1=valeur1&param2=valeur2&...
Lo stesso documento potrebbe essere richiesto con il metodo GET:
GET /document?param1=valeur1&param2=valeur2&...
...
[ligne vide]
La differenza tra i due metodi sta nel fatto che il browser, visualizzando l'URI richiesto, mostrerà /document nel caso di POST e /document?param1=valeur1&param2=valeur2&... nel caso di GET.

11.7.2. Le classi WebRequest / WebResponse

A volte la classe WebClient non è sufficientemente flessibile per ottenere il risultato desiderato. Riprendiamo l’esempio del client web con reindirizzamento esaminato nel paragrafo 11.6.6. Dobbiamo inviare l’intestazione HTTP:

HEAD /document HTTP/1.1

Abbiamo visto che le intestazioni HTTP inviate di default dal client web erano le seguenti:

1
2
3
<-- GET / HTTP/1.1
<-- Host: machine:port
<-- Connection: Keep-Alive

Abbiamo inoltre visto che era possibile aggiungere le intestazioni HTTP a quelle precedenti utilizzando la collezione [WebClient].Headers. Solo la riga 1 non è un'intestazione appartenente alla collezione Headers poiché non ha la forma chiave: valore. Non sono riuscito a capire come modificare il valore GET in HEAD nella riga 1 partendo dalla classe WebClient (forse non ho cercato bene?). Quando la classe WebClient ha raggiunto i propri limiti, è possibile passare alle classi WebRequest / WebResponse:

  • WebRequest: rappresenta l’intera richiesta del client Web.
  • WebResponse: rappresenta l’intera risposta del server Web

Abbiamo detto che la classe WebClient gestisce gli schemi http:, https:, ftp:, file:. Le richieste e le risposte di questi diversi protocolli non hanno la stessa forma. È quindi necessario gestire il tipo esatto di questi elementi piuttosto che il loro tipo generico WebRequest e WebResponse. Utilizzeremo quindi le classi:

  • HttpWebRequest, HttpWebResponse per un client HTTP
  • FtpWebRequest, FtpWebResponse per un cliente FTP

Ora trattiamo, con le classi HttpWebRequest e HttpWebresponse, l'esempio del client web con reindirizzamento esaminato nel paragrafo 11.6.6. Il codice è il seguente:


using System;
using System.IO;
using System.Net.Sockets;
using System.Net;

namespace Chap9 {
    class WebRequestResponse {
        static void Main(string[] args) {
            // sintassi
            const string syntaxe = "pg URI GET/HEAD";

            // numero di argomenti
            if (args.Length != 2) {
                Console.WriteLine(syntaxe);
                return;
            }

            // si rileva il codice URI richiesto
            string stringURI = args[0];
            string commande = args[1].ToUpper();

            // verifica della validità di URI
            Uri uri = null;
            try {
                uri = new Uri(stringURI);
            } catch (Exception ex) {
                // URI non corretto
                Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
                return;
            }
            // verifica dell'ordine
            if (commande != "GET" && commande != "HEAD") {
                // ordine non corretto
                Console.WriteLine("Le second paramètre doit être GET ou HEAD");
                return;
            }

            try {
                // si sta configurando la richiesta
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = commande;
                httpWebRequest.Proxy = null;
                // in esecuzione
                HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
                // risultato
                Console.WriteLine("---------------------");
                Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
                // intestazioni HTTP
                Console.WriteLine("---------------------");
                foreach (string clé in httpWebResponse.Headers.Keys) {
                    Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
                }
                Console.WriteLine("---------------------");
                // documento
                using (Stream stream = httpWebResponse.GetResponseStream()) {
                    using (StreamReader reader = new StreamReader(stream)) {
                        // si visualizza la risposta sulla console
                        Console.WriteLine(reader.ReadToEnd());
                    }
                }
            } catch (WebException e1) {
                // si recupera la risposta
                HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
                Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
            } catch (Exception e2) {
                // viene visualizzata l'eccezione
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
            }
        }
    }
}
  • riga 40: viene creato un oggetto di tipo WebRequest con il metodo statico WebRequest.Create(Uri uri), dove uri è l'URI del documento da scaricare. Poiché sappiamo che il protocollo dell’URI è HTTP, il tipo del risultato viene modificato in HttpWebRequest per poter accedere agli elementi specifici del protocollo HTTP.
  • riga 41: impostiamo il metodo GET / POST / HEAD della prima riga delle intestazioni HTTP. In questo caso sarà GET o HEAD.
  • riga 42: in una rete privata aziendale, è frequente che i computer dell’azienda siano isolati da Internet per motivi di sicurezza. A tal fine, la rete privata utilizza indirizzi Internet che i router di Internet non instradano. La rete privata è collegata a Internet tramite dispositivi speciali chiamati proxy, che sono collegati sia alla rete privata aziendale che a Internet. Un esempio di dispositivo con più indirizzi è IP. Un dispositivo della rete privata non può stabilire autonomamente una connessione con un server su Internet, ad esempio un server web. Deve chiedere a un dispositivo proxy di farlo per suo conto. Un dispositivo proxy può ospitare server proxy per diversi protocolli. Si parla di proxy HTTP per indicare il servizio che si occupa di effettuare le richieste HTTP per conto dei computer della rete privata. Se esiste un server proxy di questo tipo (HTTP), è necessario indicarlo nel campo [WebRequest].proxy. Ad esempio, si scriverà:
[WebRequest].proxy=new WebProxy("pproxy.istia.uang:3128");

se il proxy HTTP opera sulla porta 3128 del computer pproxy.istia.uang. Si inserisce null nel campo [WebRequest].proxy se il computer dispone di un accesso diretto a Internet e non deve passare attraverso un proxy.

  • riga 44: il metodo GetResponse() richiede il documento identificato dal suo URI e restituisce un oggetto WebRequestResponse che qui viene trasformato in un oggetto HttpWebResponse. Questo oggetto rappresenta la risposta del server alla richiesta del documento.
  • riga 47:
    • [HttpWebResponse].ResponseUri: è l'URI del server che ha inviato il documento. In caso di reindirizzamento, questo può essere diverso dall'URI del server inizialmente interrogato. Si noti che il codice non gestisce il reindirizzamento. Questo viene gestito automaticamente dal metodo GetResponse. Ancora una volta, questo rappresenta il vantaggio delle classi di alto livello rispetto alle classi di base del protocollo TCP.
    • [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription rappresentano la prima riga della risposta, ad esempio: HTTP/1.1 200 OK. StatusCode è 200 e StatusDescription è OK.
  • riga 50: [HttpWebResponse].Headers è l’insieme delle intestazioni HTTP della risposta.
  • riga 55: [HttpWebResponse].GetResponseStream: è il flusso che consente di ottenere il documento contenuto nella risposta.
  • riga 61: può verificarsi un'eccezione di tipo WebException
  • riga 63: [WebException].Response è la risposta che ha causato il lancio dell'eccezione.

Ecco un esempio di esecuzione:

...\Chap9\09B\bin\Release>09B http://www.gouv.fr HEAD
---------------------
Le serveur http://www.premier-ministre.gouv.fr/fr/ ha risposto: 200 OK
---------------------
Connection: keep-alive
Content-Type: text/html; charset=iso-8859-1
Date: Mon, 05 May 2008 13:02:29 GMT
Expires: Mon, 05 May 2008 13:07:20 GMT
Last-Modified: Mon, 05 May 2008 12:56:59 GMT
Server: Apache
X-Powered-By: PHP/4.4.1
---------------------
  • righe 1 e 3: il server che ha risposto non è lo stesso di quello che era stato interrogato. Si è quindi verificato un reindirizzamento.
  • righe 5-11: le intestazioni HTTP inviate dal server

11.7.3. Applicazione: un client proxy di un server web di traduzione

Mostriamo ora come le classi precedenti ci consentano di sfruttare le risorse del web.

11.7.3.1. L'application

Sul web esistono siti di traduzione. Quello che verrà utilizzato in questa sede è il sito http://trans.voila.fr/traduction_voila.php:

Il testo da tradurre viene inserito in [1], la direzione di traduzione viene selezionata in [2]. La traduzione viene richiesta tramite [3] e ottenuta in [4].

Scriveremo un'applicazione client Windows per l'applicazione sopra descritta. Non farà nulla di più dell'applicazione del sito [trans.voila.fr]. La sua interfaccia sarà la seguente:

11.7.3.2. L'architettura dell'applicazione

L’applicazione avrà la seguente architettura a due livelli:

11.7.3.3. Il progetto Visual Studio

Il progetto Visual Studio sarà il seguente:

  • In [1], la soluzione è composta da due progetti,
  • [2]: uno per il livello [dao] e le entità da esso utilizzate,
  • [3]: l'altro per l'interfaccia Windows

11.7.3.4. Il progetto [dao]

Il progetto [dao] è costituito dai seguenti elementi:

  • IServiceTraduction.cs: l'interfaccia presentata al livello [ui]
  • ServiceTraduction: l'implementazione di questa interfaccia
  • WebTraductionsException: un'eccezione specifica dell'applicazione

L’interfaccia IServiceTraduction è la seguente:


using System.Collections.Generic;

namespace dao {
    public interface IServiceTraduction {
        // lingue utilizzate
        IDictionary<string, string> LanguesTraduites { get; }
        // traduzione
        string Traduire(string texte, string deQuoiVersQuoi);
    }
}
  • riga 6: la proprietà LanguesTraduites restituisce il dizionario delle lingue accettate dal server di traduzione. Questo dizionario contiene voci della forma ["fe","Français-Anglais"], dove il valore indica una direzione di traduzione, in questo caso dal francese all'inglese, e la chiave "fe" è un codice utilizzato dal server di traduzione trans.voila.fr.
  • riga 8: il metodo Traduire è il metodo di traduzione:
    • texte è il testo da tradurre
    • deQuoiVersQuoi è una delle chiavi del dizionario delle lingue tradotte
    • il metodo restituisce la traduzione del testo

ServiceTraduction è una classe di implementazione dell'interfaccia IServiceTraduction. La descriviamo in dettaglio nella sezione seguente.

WebTraductionsException è la seguente classe di eccezione:


using System;

namespace entites {
    public class WebTraductionsException : Exception {

        // codice di errore
        public int Code { get; set; }

        // produttori
        public WebTraductionsException() {
        }
        public WebTraductionsException(string message)
            : base(message) {
        }
        public WebTraductionsException(string message, Exception e)
            : base(message, e) {
        }
    }
}
  • riga 7: un codice di errore

11.7.3.5. Il client web [ServiceTraduction]

Torniamo all’architettura della nostra applicazione:

La classe [ServiceTraduction] che dobbiamo scrivere è un client del servizio web di traduzione [trans.voila.fr]. Per scriverla, dobbiamo capire

  • cosa si aspetta il server di traduzione dal proprio client
  • cosa restituisce al proprio client

Vediamo con un esempio il dialogo client/server che avviene durante una traduzione. Riprendiamo l’esempio presentato nell’introduzione dell’applicazione:

Il testo da tradurre viene inserito in [1], la direzione di traduzione viene selezionata in [2]. La traduzione viene richiesta tramite [3] e ottenuta in [4].

Per ottenere la traduzione [4], il browser ha inviato la seguente richiesta GET (visualizzata nel campo dell'indirizzo):

http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=il+cane+è+malato

È piuttosto semplice da comprendere:

  • http://trans.voila.fr/traduction_voila.php è l’URL del servizio di traduzione
  • isText=1 sembra indicare che si tratta di testo
  • translationDirection indica la direzione della traduzione, in questo caso Français-Anglais
  • stext è il testo da tradurre in un formato denominato «URL codificato». Infatti, alcuni caratteri non possono comparire in un URL. È il caso, ad esempio, dello spazio, che qui è stato codificato con un +. Il framework .NET offre il metodo statico System.Web.HttpUtility.UrlEncode per eseguire questa operazione di codifica.

Ne consegue che, per interrogare il server di traduzione, la nostra classe [ServiceTraduction] potrà utilizzare la stringa

"http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"

in cui i marcatori {0} e {1} saranno sostituiti rispettivamente dal senso di traduzione e dal testo da tradurre.

Come si fa a conoscere le direzioni di traduzione accettate dal server? Nella schermata sopra riportata, le lingue tradotte sono presenti nell’elenco a discesa. Se nel browser si visualizza (Visualizza / Sorgente) il codice HTML della pagina, si trova quanto segue per l’elenco a discesa:

<select name="translationDirection" class="champs">
    <option selected value='fe'>Fran&ccedil;ais vers Anglais
    <option  value='ef'>Anglais vers Fran&ccedil;ais
    <option  value='fg'>Fran&ccedil;ais vers Allemand
    <option  value='gf'>Allemand vers Fran&ccedil;ais
    <option  value='fs'>Fran&ccedil;ais vers Espagnol
    <option  value='sf'>Espagnol vers Fran&ccedil;ais
    <option  value='fr'>Fran&ccedil;ais vers Russe
    <option  value='rf'>Russe vers Fran&ccedil;ais
    <option  value='es'>Anglais vers Espagnol
    <option  value='se'>Espagnol vers Anglais
    <option  value='eg'>Anglais vers Allemand
    <option  value='ge'>Allemand vers Anglais
    <option  value='ep'>Anglais vers Portugais
    <option  value='pe'>Portugais vers Anglais
    <option  value='ie'>Italien vers Anglais
    <option  value='gs'>Allemand vers Espagnol
    <option  value='sg'>Espagnol vers Allemand
</select>

Non si tratta di un codice HTML molto pulito, in quanto ogni tag <option> dovrebbe normalmente essere chiuso da un tag </option>. Detto questo, gli attributi "value" ci forniscono l’elenco dei codici di traduzione che devono essere inviati al server. Nel dizionario LanguesTraduites dell’interfaccia IServiceTraduction, le chiavi saranno gli attributi value sopra indicati e i valori saranno i testi visualizzati dal menu a tendina.

Ora vediamo (Visualizza / Sorgente) dove si trova nella pagina HTML la traduzione restituita dal server di traduzione:

...                                                                
<strong>Texte traduit : </strong><div class="txtTrad">this dog is sick</div> 
...

La traduzione si trova proprio al centro della pagina HTML restituita. Come individuarla? È possibile utilizzare un'espressione regolare con la sequenza <div class="txtTrad">...</div>, poiché il tag <div class="txtTrad"> è presente solo in quel punto della pagina HTML. L'espressione regolare in C# che consente di recuperare il testo tradotto è la seguente:

@"<div class=""txtTrad"">(.*?)</div>"

Ora disponiamo degli elementi necessari per scrivere la classe di implementazione ServiceTraduction dell’interfaccia IServiceTraduction:


using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using entites;

namespace dao {
    public class ServiceTraduction : IServiceTraduction {
        // proprietà di configurazione automatica del servizio
        public IDictionary<string, string> LanguesTraduites { get; set; }
        public string UrlServeurTraduction { get; set; }
        public string ProxyHttp { get; set; }
        public String RegexTraduction { get; set; }

        // traduzione
        public string Traduire(string texte, string deQuoiVersQuoi) {
            // la traduzione richiesta è possibile?
            if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
                throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
            }
            // testo da tradurre
            string texteATraduire = HttpUtility.UrlEncode(texte);
            // URI da richiedere
            string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
            // espressione regolare per individuare la traduzione nella risposta
            Regex patternTraduction = new Regex(RegexTraduction);
            // eccezione
            WebTraductionsException exception = null;
            // traduzione
            string traduction = null;
            try {
                // si configura la richiesta
                HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
                httpWebRequest.Method = "GET";
                httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
                // la si esegue
                HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
                // documento
                using (Stream stream = httpWebResponse.GetResponseStream()) {
                    using (StreamReader reader = new StreamReader(stream)) {
                        bool traductionTrouvée = false;
                        string ligne = null;
                        while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
                            // ricerca della traduzione nella riga corrente
                            MatchCollection résultats = patternTraduction.Matches(ligne);
                            // traduzione trovata?
                            if (résultats.Count != 0) {
                                traduction = résultats[0].Groups[1].Value.Trim();
                                traductionTrouvée = true;
                            }
                        }
                        // traduzione trovata?
                        if (!traductionTrouvée) {
                            exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
                        }
                    }
                }
            } catch (Exception e) {
                exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
            }
            // eccezione?
            if (exception != null) {
                throw exception;
            } else {
                return traduction;
            }
        }
    }
}
  • riga 12: la proprietà LanguesTraduites dell’interfaccia IServiceTraduction – inizializzata dall’esterno
  • riga 13: la proprietà UrlServeurTraduction è l'URL da richiedere al server di traduzione: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, dove il marcatore {0} dovrà essere sostituito con la direzione di traduzione e il marcatore {1} con il testo da tradurre - inizializzata dall'esterno
  • riga 14: la proprietà ProxyHttp è l'eventuale proxy HTTP da utilizzare, ad esempio: pproxy.istia.uang:3128 - inizializzata dall'esterno
  • riga 15: la proprietà RegexTraduction è l'espressione regolare che consente di recuperare la traduzione nel flusso HTML restituito dal server di traduzione, ad esempio @"<div class=""txtTrad"">(.*?)</div>" - inizializzata dall'esterno
  • queste quattro proprietà saranno, nella nostra applicazione, inizializzate da Spring.
  • righe 20-22: si verifica che la direzione di traduzione richiesta esista effettivamente nel dizionario delle lingue tradotte. In caso contrario, viene generata un'eccezione.
  • riga 24: il testo da tradurre viene codificato per poter essere inserito in un URL
  • riga 26: viene costruito l’URI del servizio di traduzione. Se la proprietà UrlServeurTraduction è la stringa http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, il marcatore {0} viene sostituito dal significato della traduzione e il marcatore {1} dal testo da tradurre.
  • riga 28: viene costruito il modello di ricerca della traduzione nella risposta HTML del server di traduzione.
  • righe 33, 60: l'operazione di interrogazione del server di traduzione avviene all'interno di un blocco try/catch
  • riga 35: viene creato l'oggetto HttpWebRequest, che verrà utilizzato per interrogare il server di traduzione, utilizzando l'URI del documento richiesto.
  • riga 36: il metodo di interrogazione è GET. Si potrebbe fare a meno di questa istruzione, poiché GET è probabilmente il metodo predefinito dell'oggetto HttpWebRequest.
  • riga 37: si imposta la proprietà Proxy dell'oggetto HttpWebRequest.
  • riga 39: viene inviata la richiesta al server di traduzione e si recupera la risposta, che è di tipo HttpWebResponse.
  • righe 41-42: si utilizza un StreamReader per leggere ogni riga della risposta HTML del server.
  • righe 45-53: in ogni riga della risposta si cerca la traduzione. Una volta trovata, si interrompe la lettura della risposta HTML e si chiudono tutti i flussi aperti.
  • righe 55-57: se non è stata trovata alcuna traduzione nella risposta HTML, si prepara un'eccezione di tipo WebTraductionsException per segnalarlo.
  • righe 60-62: se si è verificata un'eccezione durante lo scambio client/server, la si incapsula in un'eccezione di tipo WebTraductionsException per segnalarla.
  • righe 64-68: se è stata registrata un'eccezione, questa viene generata; in caso contrario, viene restituita la traduzione trovata.

Il nostro esempio presuppone che il proxy HTTP non richieda l’autenticazione. Se così non fosse, si scriverebbe qualcosa del tipo:


httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");

Qui abbiamo utilizzato WebRequest / WebResponse anziché WebClient perché non dobbiamo sfruttare l’intera risposta HTML del server di traduzione. Una volta individuata la traduzione in questa risposta, non abbiamo più bisogno delle restanti righe della risposta. La classe WebClient non consente di farlo.

Ecco un programma di test della classe ServiceTraduction:


using System;
using System.Collections.Generic;
using dao;
using entites;

namespace ui {
    class Program {
        static void Main(string[] args) {
            try {
                // creazione servizio di traduzione
                ServiceTraduction serviceTraduction = new ServiceTraduction();
                // espressione regolare per trovare la traduzione
                serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
                // URL del server di traduzione
                serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
                // dizionario delle lingue tradotte
                Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
                languesTraduites["fe"]= "Français-Anglais";
                languesTraduites["fs"]= "Français-Espagnol";
                languesTraduites["ef"]= "Anglais-Français";
                serviceTraduction.LanguesTraduites = languesTraduites;
                // proxy
                //serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
                // traduzione
                string texte = "ce chien est perdu";
                string deQuoiVersQuoi = "fe";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "l'été sera chaud";
                deQuoiVersQuoi = "fs";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "my tailor is rich";
                deQuoiVersQuoi = "ef";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
                texte = "xx";
                deQuoiVersQuoi = "ef";
                Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
            } catch (WebTraductionsException e) {
                // errore
                Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
            }
        }
    }
}

I risultati ottenuti sono i seguenti:

1
2
3
4
Traduction [Français-Anglais] de [ce chien est perdu] : [this dog is lost]
Traduction [Français-Espagnol] de [l'été sera chaud] : [el verano será caliente]
Traduction [Anglais-Français] de [my tailor is rich] : [mon tailleur est riche]
Traduction [Anglais-Français] de [xx] : [xx]

Il progetto [dao] della soluzione viene compilato in DLL e HttpTraductions.dll:

 

11.7.3.6. L'interfaccia grafica dell'applicazione

Torniamo all’architettura della nostra applicazione:

Ora scriviamo il livello [ui]. Questo è l’oggetto del progetto [ui] della soluzione in fase di sviluppo:

La cartella [lib] [3] contiene alcuni dei file DLL a cui fa riferimento il progetto [4]:

  • quelle necessarie per Spring: Spring.Core, Common.Logging, antlr.runtime
  • quella del livello [dao]: HttpTraductions

Il file [App.config] contiene la configurazione di Spring:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <description>Traductions sur le web</description>
            <!-- il servizio di traduzione -->
            <object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
                <property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&amp;translationDirection={0}&amp;stext={1}"/>
                <!--
                <property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
                -->
                <property name="RegexTraduction" value="&lt;div class=&quot;txtTrad&quot;&gt;(.*?)&lt;/div&gt;"/>
                <property name="LanguesTraduites">
                    <dictionary key-type="string" value-type="string">
                        <entry key="fe" value="Français-Anglais"/>
                        <entry key="ef" value="Anglais-Français"/>
...
                        <entry key="ei" value="Anglais-Italien"/>
                        <entry key="ie" value="Italien-Anglais"/>
                    </dictionary>
                </property>
            </object>
        </objects>
    </spring>
</configuration>
  • riga 15: gli oggetti da istanziare da Spring. Ce ne sarà solo uno, quello della riga 18 che istanzia il servizio di traduzione con la classe ServiceTraduction presente nel file DLL HttpTraductions.
  • riga 19: la proprietà UrlServeurTraduction della classe ServiceTraduction. C'è un problema con il carattere & dell'URL. Questo carattere ha un significato in un file XML. Deve quindi essere protetto. Lo stesso vale per altri caratteri che incontreremo nel prosieguo del file. Devono essere sostituiti da una sequenza [&code;]: & con [&amp;], < con [&lt;], > con [&gt;], " con [&quot;].
  • riga 21: la proprietà ProxyHttp della classe ServiceTraduction. Una proprietà non inizializzata rimane a null. Non definire questa proprietà equivale a dire che non esiste un proxy HTTP.
  • riga 23: la proprietà RegexTraduction della classe ServiceTraduction. Nell'espressione regolare è stato necessario sostituire i caratteri [< > "] con i loro equivalenti protetti.
  • righe 24-33: la proprietà LanguesTraduites della classe ServiceTraduction.

Il programma [Program.cs] viene eseguito all’avvio dell’applicazione. Il suo codice è il seguente:


using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;

namespace ui {
    static class Program {
        /// <summary>
        /// Il punto di accesso principale dell'applicazione.
        /// </summary>
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
    
            // --------------- Codice sviluppatore
            // istanziazione del servizio di traduzione
            IApplicationContext ctx = null;
            Exception ex = null;
            ServiceTraduction serviceTraduction = null;
            try {
                // contesto Spring
                ctx = ContextRegistry.GetContext();
                // richiesta di un riferimento al servizio di traduzione
                serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
            } catch (Exception e1) {
                // memorizzazione dell'eccezione
                ex = e1;
            }
            // modulo da visualizzare
            Form form = null;
            // Si è verificata un'eccezione?
            if (ex != null) {
                // sì - si crea il messaggio di errore da visualizzare
                StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
                Exception e = ex;
                while (e != null) {
                    msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
                    msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
                    e = e.InnerException;
                }
                // creazione della finestra di errore a cui viene passato il messaggio di errore da visualizzare
                Form2 form2 = new Form2();
                form2.MsgErreur = msgErreur.ToString();
                // questa sarà la finestra da visualizzare
                form = form2;
            } else {
                // tutto è andato bene
                // creazione dell'interfaccia grafica [Form1] a cui viene passato il riferimento al servizio di traduzione
                Form1 form1 = new Form1();
                form1.ServiceTraduction = serviceTraduction;
                // questa sarà la finestra da visualizzare
                form = form1;
            }
            // visualizzazione della finestra
            Application.Run(form);
        }
    }
}

Questo codice è già stato utilizzato nell’applicazione Impôts versione 6, al paragrafo 7.6.2.

  • Il servizio di traduzione viene creato alla riga 27 da Spring. Se la creazione è andata a buon fine, verrà visualizzato il modulo [Form1] (righe 52-55), altrimenti verrà visualizzato il modulo di errore [Form2] (righe 36-48).

Il modulo [Form2] è quello utilizzato nell’applicazione Impôts versione 6 ed è stato illustrato nel paragrafo 7.6.4.

Il modulo [Form1] è il seguente:

n.
tipo
nome
ruolo
1
TextBox
textBoxTexteATraduire
casella di immissione del testo da tradurre
MultiLine=true
2
ComboBox
comboBoxLangues
l'elenco delle direzioni di traduzione
3
Pulsante
buttonTraduire
per richiedere la traduzione del testo [1] nella direzione [2]
4
TextBox
textBoxTraduction
la traduzione del testo [1]

Il codice del modulo [Form1] è il seguente:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;

namespace ui {
    public partial class Form1 : Form {
        // servizio di traduzione
        public ServiceTraduction ServiceTraduction { get; set; }
        // dizionario delle lingue
        Dictionary<string, string> languesInversées = new Dictionary<string, string>();

        // costruttore
        public Form1() {
            InitializeComponent();
        }

        // caricamento iniziale del modulo
        private void Form1_Load(object sender, EventArgs e) {
            // creazione del dizionario inverso delle lingue
            foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
                // lingue
                string langues = ServiceTraduction.LanguesTraduites[code];
                // aggiunta (lingue, codice) al dizionario inverso
                languesInversées[langues] = code;
            }
            // compilazione della lista a discesa in ordine alfabetico delle lingue
            string[] languesCombo = languesInversées.Keys.ToArray();
            Array.Sort<string>(languesCombo);
            foreach (string langue in languesCombo) {
                comboBoxLangues.Items.Add(langue);
            }
            // selezione della prima lingua
            if (comboBoxLangues.Items.Count != 0) {
                comboBoxLangues.SelectedIndex = 0;
            }
        }

        private void buttonTraduire_Click(object sender, EventArgs e) {
            // C'è qualcosa da tradurre?
            string texte = textBoxTexteATraduire.Text.Trim();
            if (texte == "") return;
            // traduzione
            try {
                textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
            } catch (Exception ex) {
                textBoxTraduction.Text = ex.Message;
            }
        }
    }
}
  • riga 10: un riferimento al servizio di traduzione. Questa proprietà pubblica è stata inizializzata da [Program.cs], riga 53. Quando vengono eseguiti i metodi Form1_Load (riga 20) o buttonTraduire_Click (riga 40), questo campo è quindi già inizializzato.
  • riga 12: il dizionario delle lingue tradotte con voci di tipo ["Français-Anglais","fe"], c.a.d. L’inverso del dizionario LanguesTraduites restituito dal servizio di traduzione.
  • riga 20: il metodo Form1_Load viene eseguito al caricamento del modulo.
  • righe 22-27: si utilizzano i dizionari serviceTraduction.LanguesTraduites e ["fe","Français-Anglais"] per costruire i dizionari languesInversées e ["Français-Anglais", "fe"].
  • riga 29: languesCombo è la tabella delle chiavi dei dizionari languesInversées e c.a.d. Una tabella di elementi ["Français-Anglais"]
  • riga 30: questa tabella è ordinata in modo da presentare nel menu a tendina i significati della traduzione in ordine alfabetico
  • righe 31-33: il menu a tendina delle lingue viene popolato.
  • riga 40: il metodo eseguito quando l’utente fa clic sul pulsante [Traduire]
  • riga 46: è sufficiente chiamare il metodo serviceTraduction.Traduire per richiedere la traduzione. Il primo parametro è il testo da tradurre, il secondo il codice della direzione di traduzione. Questo codice si trova nel dizionario languesInversées in base all’elemento selezionato nel menu a tendina delle lingue.
  • riga 48: se si verifica un'eccezione, questa viene visualizzata al posto della traduzione.

11.7.3.7. Conclusion

Questa applicazione ha dimostrato che i client web del framework .NET ci consentono di sfruttare le risorse del web. La tecnica è sempre simile:

  • determinare l’URI da interrogare. Questo URI è nella maggior parte dei casi parametrizzato.
  • interrogarla
  • trovare nella risposta del server ciò che si sta cercando grazie alle espressioni regolari

Questa tecnica è aleatoria. Infatti, nel corso del tempo, l’URI interrogato o l’espressione regolare che permette di trovare il risultato atteso possono cambiare. È quindi opportuno inserire queste due informazioni in un file di configurazione. Ma ciò può rivelarsi insufficiente. Vedremo nel capitolo seguente che esistono risorse più stabili sul web: i servizi web.

11.7.4. Un client SMTP (Simple Mail Transport Protocol) con la classe SmtpClient

Un client SMTP è un client di un server SMTP, ovvero un server di invio della posta. La classe .NET SmtpClient incapsula completamente le esigenze di un client di questo tipo. Lo sviluppatore non deve conoscere i dettagli del protocollo SMTP. Noi conosciamo quest’ultimo. È stato presentato al paragrafo 11.4.3.

Presentiamo la classe SmtpClient nell’ambito di un’applicazione Windows di base che consente di inviare e-mail con allegati. L’applicazione si connetterà alla porta 25 di un server SMTP. Si ricorda che sulla maggior parte dei sistemi Windows, i firewall o altri antivirus bloccano le connessioni alla porta 25. È quindi necessario disattivare questa protezione per testare l’applicazione:

Il client SMTP avrà un'architettura a singolo livello:

Il progetto Visual Studio è il seguente:

  

L'interfaccia grafica [SendMailForm.cs] dell'applicazione è la seguente:

n.
tipo
nome
ruolo
1
TextBox
textBoxServeur
nome del server SMTP a cui connettersi
2
NumericUpDown
numericUpDownPort
la porta a cui connettersi
3
TextBox
textBoxExpediteur
indirizzo del mittente del messaggio
4
TextBox
textBoxTo
indirizzi dei destinatari nel formato: indirizzo1, indirizzo2, ...
5
TextBox
textBoxCc
indirizzi dei destinatari in copia (CC=Carbon Copy) nel formato: indirizzo1, indirizzo2, ...
6
TextBox
textBoxBcc
indirizzi dei destinatari in copia nascosta (BCC=Blind Carbon Copy) nel formato: indirizzo1, indirizzo2, ... Tutti gli indirizzi di questi tre campi di inserimento riceveranno lo stesso messaggio con gli stessi allegati. I destinatari del messaggio potranno conoscere gli indirizzi presenti nei campi 4 e 5, ma non quelli del campo 6. Il Bcc è quindi un modo per mettere qualcuno in copia senza che gli altri destinatari del messaggio lo sappiano.
7
Pulsante
buttonAjouter
per aggiungere un allegato all’e-mail
8
ListBox
listBoxPiecesJointes
elenco degli allegati da aggiungere all'e-mail
9
TextBox
textBoxSujet
Oggetto della lettera
10
TextBox
textBoxMessage
il testo del messaggio.
MultiLine=true
11
Pulsante
buttonEnvoyer
per inviare il messaggio ed eventuali allegati
12
TextBox
textBoxRésultat
visualizza un riepilogo del messaggio inviato oppure un messaggio di errore se si è verificato un problema
13
Pulsante
buttonEffacer
per cancellare [12]
 
OpenfileDialog
openFileDialog1
controllo non visivo che consente di selezionare un allegato nel file system locale

Nell'esempio precedente, il riepilogo visualizzato in [12] è il seguente:

Envoi réussi...
Sujet : votre demande
Destinataires : y2000@hotmail.com
Cc : 
Bcc : 
Pièces jointes :
C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
Texte : Bonjour,

Vous trouverez ci-joint le dossier de candidature à l'ISTIA.

Cordialement,

ST

Il codice del modulo [SendMailForm.cs] è il seguente:


using System;
using System.Windows.Forms;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Text;

namespace Chap9 {
    public partial class SendMailForm : Form {
        public SendMailForm() {
            InitializeComponent();
        }

        // aggiunta di un allegato
        private void buttonAjouter_Click(object sender, EventArgs e) {
            // si configura la finestra di dialogo openfileDialog1
            openFileDialog1.InitialDirectory = Application.ExecutablePath;
            openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
            openFileDialog1.FilterIndex = 0;
            openFileDialog1.FileName = "";
            // visualizzazione della finestra di dialogo e recupero del risultato
            if (openFileDialog1.ShowDialog() == DialogResult.OK) {
                // si recupera il nome del file
                listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
            }
        }

        private void textBoxServeur_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void setStatutEnvoyer() {
            buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
        }

        // rimuovere un allegato
        private void buttonRetirer_Click(object sender, EventArgs e) {
            // Allegato selezionato?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // lo si rimuove
                listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
                // si aggiorna il pulsante «Rimuovi»
                buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
            }
        }

        private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
            // Allegato selezionato?
            if (listBoxPiecesJointes.SelectedIndex != -1) {
                // si aggiorna il pulsante Rimuovi
                buttonRetirer.Enabled = true;
            }
        }

        // invio del messaggio con i relativi allegati
        private void buttonEnvoyer_Click(object sender, EventArgs e) {
....
        }

        private void textBoxTo_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void textBoxSujet_TextChanged(object sender, EventArgs e) {
            setStatutEnvoyer();
        }

        private void buttonEffacer_Click(object sender, EventArgs e) {
            textBoxResultat.Text = "";
        }
    }
}

Non commenteremo questo codice, che non presenta novità. Per comprendere il metodo buttonAjouter_Click della riga 14, si invita il lettore a rileggere il paragrafo 7.5.1.

Il metodo buttonEnvoyer_Click alla riga 55, che invia l’e-mail, è il seguente:


private void buttonEnvoyer_Click(object sender, EventArgs e) {
            try {
                // clessidra
                Cursor = Cursors.WaitCursor;
                // il client SMTP
                SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
                // il messaggio
                MailMessage message = new MailMessage();
                // mittente
                message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
                message.From = message.Sender;
                // destinatari
                Regex marqueur = new Regex("\\s*,\\s*");
                string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
                foreach (string destinataire in destinataires) {
                    if (destinataire.Trim() != "") {
                        message.To.Add(new MailAddress(destinataire));
                    }
                }
                // CC
                string[] copies = marqueur.Split(textBoxCc.Text.Trim());
                foreach (string copie in copies) {
                    if (copie.Trim() != "") {
                        message.CC.Add(new MailAddress(copie));
                    }
                }
                // BCC
                string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
                foreach (string blindCopie in blindCopies) {
                    if (blindCopie.Trim() != "") {
                        message.Bcc.Add(new MailAddress(blindCopie));
                    }
                }
                // oggetto
                message.Subject = textBoxSujet.Text.Trim();
                // testo del messaggio
                message.Body = textBoxMessage.Text;
                // allegati
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    message.Attachments.Add(new Attachment(attachement));
                }
                // invio del messaggio
                smtpClient.Send(message);
                // Ok - viene visualizzato un riepilogo
                StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
                msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
                textBoxSujet.Clear();
                msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
                textBoxTo.Clear();
                msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
                textBoxCc.Clear();
                msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
                textBoxBcc.Clear();
                msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
                foreach (string attachement in listBoxPiecesJointes.Items) {
                    msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
                }
                msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
                listBoxPiecesJointes.Items.Clear();
                textBoxResultat.Text = msg.ToString();
            } catch (Exception ex) {
                // viene visualizzato l'errore
                textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
            }
            // cursore normale
            Cursor = Cursors.Arrow;
        }
  • riga 6: viene creato il client SMTP. Sono necessari due parametri: il nome del server SMTP e la porta su cui opera
  • riga 8: viene creato un messaggio di tipo MailMessage. È questo che incapsulerà l’intero messaggio da inviare.
  • riga 10: viene specificato l'indirizzo e-mail Sender del mittente. Un indirizzo e-mail è un'istanza di tipo MailAddress costruita a partire da una stringa di caratteri "xx@yy.zz". Questa stringa deve avere il formato previsto per un indirizzo e-mail, altrimenti viene generata un'eccezione. In tal caso, verrà visualizzata nel campo textBoxResultat (riga 63) in un formato poco intuitivo.
  • righe 13-19: gli indirizzi e-mail dei destinatari vengono inseriti nel campo "A" del messaggio. Questi indirizzi vengono recuperati dal campo textBoxTo. L'espressione regolare della riga 13 consente di recuperare i diversi indirizzi separati da una virgola.
  • righe 21-26: si ripete lo stesso processo per inizializzare il campo CC del messaggio con gli indirizzi in copia dal campo textBoxCc.
  • righe 28-33: si ripete la stessa procedura per inizializzare il campo Ccn del messaggio con gli indirizzi in copia nascosta del campo textBoxBcc.
  • riga 35: il campo Subject del messaggio viene inizializzato con l'oggetto del campo textBoxSujet.
  • riga 37: il campo Body del messaggio viene inizializzato con il testo del messaggio textBoxMessage.
  • righe 39-41: gli allegati vengono aggiunti al messaggio. Ogni allegato viene aggiunto sotto forma di oggetto Attachment al campo Attachments del messaggio. Un oggetto Attachment viene istanziato a partire dal percorso completo dell’allegato da aggiungere nel file system locale.
  • riga 43: il messaggio viene inviato utilizzando il metodo Send del client SMTP.
  • righe 45-60: scrittura del riepilogo dell'invio nel campo textBoxResultat e reinizializzazione del modulo.
  • riga 63: visualizzazione di un eventuale errore

11.8. Un client TCP generico asincrono

11.8.1. Panoramica

In tutti gli esempi di questo capitolo, la comunicazione client/server avveniva in modalità bloccante, detta anche modalità sincrona:

  • quando un client si connette a un server, attende la risposta del server a tale richiesta prima di proseguire.
  • quando un client legge una riga di testo inviata dal server, rimane bloccato finché il server non l'ha inviata.
  • Dal lato server, i thread di servizio che gestiscono la comunicazione con il client funzionano allo stesso modo descritto sopra.

Nelle interfacce grafiche, è spesso necessario evitare di bloccare l’utente durante operazioni che richiedono molto tempo. Il caso più citato è quello del download di un file di grandi dimensioni. Durante il download, è necessario consentire all’utente di continuare a interagire con l’interfaccia grafica.

In questa sede ci proponiamo di riscrivere il client TCP generico del paragrafo 11.6.3 apportando le seguenti modifiche:

  • l’interfaccia sarà grafica
  • lo strumento di comunicazione con il server sarà un oggetto Socket
  • la modalità di comunicazione sarà asincrona:
    • il client avvierà una connessione al server ma non rimarrà bloccato in attesa che venga stabilita
    • il client avvierà l'invio di dati al server ma non rimarrà bloccato in attesa che l'operazione sia completata
    • il client avvierà la ricezione dei dati provenienti dal server, ma non rimarrà bloccato in attesa che essa sia terminata.

Ricordiamo a quale livello si trova l’oggetto Socket nella comunicazione client/server TCP:

La classe Socket è quella che opera più a livello di rete. Consente una gestione dettagliata della connessione di rete. Il termine socket indica una presa di corrente. Il termine è stato esteso per indicare una presa di rete software. In una comunicazione TCP-IP tra due macchine A e B, sono due sockets a comunicare tra loro. Un'applicazione può operare direttamente con i sockets. È il caso dell'applicazione A sopra citata. Un socket può essere un socket client o serveur.

11.8.2. L'interfaccia grafica del client TCP asincrono

L'applicazione Visual Studio è la seguente:

  

[ClientTcpAsynchrone.cs] è l’interfaccia grafica. Questa è la seguente:

n.
tipo
nome
ruolo
1
TextBox
textBoxNomServeur
nome del server TCP a cui connettersi
2
NumericUpDown
numericUpDownPortServeur
la porta a cui connettersi
3
RadioButton
radioButtonLF
radioButtonRCLF
per indicare il carattere di fine riga che il client deve utilizzare: LF "\n" o RCLF "\r\n"
4
Pulsante
buttonConnexion
per connettersi alla porta [2] del server [1]. Il pulsante riporta la dicitura [Connecter] quando il client non è connesso a un server, [Déconnecter] quando è connesso.
5
TextBox
textBoxMsgToServeur
messaggio da inviare al server una volta stabilita la connessione. Quando l'utente preme il tasto [Entrée], il messaggio viene inviato con il carattere di fine riga selezionato in [3]
6
ListBox
listBoxEvts
elenco in cui vengono visualizzati i principali eventi della connessione client/server: connessione, disconnessione, chiusura del flusso, errori di comunicazione
7
ListBox
listBoxDialogue
elenco in cui vengono visualizzati i messaggi del dialogo client/server
8
Pulsante
buttonRazEvts
per cancellare l'elenco [6]
4
Pulsante
buttonRazDialogue
per cancellare l'elenco [7]

I principi di funzionamento di questa interfaccia sono i seguenti:

  • l'utente connette il proprio client TCP grafico a un servizio TCP tramite [1, 2, 3, 4].
  • un thread asincrono accetta in modo continuo tutti i dati inviati dal server TCP e li visualizza nell'elenco [7]. Questo thread è slegato dalle altre attività dell'interfaccia.
  • L'utente può inviare messaggi al server secondo i propri ritmi tramite [5]. Ogni messaggio viene inviato da un thread asincrono. A differenza del thread di ricezione, che non si interrompe mai, il thread di trasmissione termina non appena il messaggio è stato inviato. Per il messaggio successivo verrà utilizzato un nuovo thread asincrono.
  • La comunicazione client/server termina quando uno dei due partner chiude la connessione. L’utente può prendere questa iniziativa tramite il pulsante [4] che, una volta stabilita la connessione, assume la dicitura [Déconnecter].

Ecco uno screenshot di un'esecuzione:

  • in [1]: connessione a un servizio POP
  • in [2]: visualizzazione degli eventi verificatisi durante la connessione
  • in [3]: il messaggio inviato dal server POP al termine della connessione
  • in [4]: il pulsante [Connecter] è diventato il pulsante [Déconnecter]
  • in [1], è stato inviato il comando quit al server POP. Il server ha risposto +OK goodbye e ha chiuso la connessione
  • in [2], questa chiusura da parte del server è stata rilevata. Il client ha quindi chiuso la connessione da parte sua.
  • in [3], il pulsante [Déconnecter] è tornato ad essere un pulsante [Connecter]

11.8.3. Connessione asincrona al server

Premendo il pulsante [Connecter] viene eseguito il seguente metodo:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // Accedere o disconnettersi?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • riga 3: il pulsante può avere l'etichetta [Connecter] o [Déconnecter].

Il metodo di connessione è il seguente:


using System.Net.Sockets;
...

namespace Chap9 {
    public partial class ClientTcp : Form {
        const int tailleBuffer = 1024;
        private Socket client = null;
        private byte[] data = new byte[tailleBuffer];
        private string réponse = null;
        private string finLigne = "\r\n";

        // delegati
        public delegate void writeLog(string log);

        public ClientTcp() {
            InitializeComponent();
        }
....................................
    private void connexion() {
            // verifica dati
            string nomServeur = textBoxNomServeur.Text.Trim();
            if (nomServeur == "") {
                logEvent("indiquez le nom du serveur");
                return;
            }
            // monitoraggio
            logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
            try {
                 // creazione socket
                client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                // connessione asincrona
                client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);

            } catch (Exception ex) {
                logEvent(String.Format("erreur de connexion : {0}", ex.Message));
                return;
            }
        }

         // la connessione è stata stabilita
        private void connecté(IAsyncResult résultat) {
            // si recupera il socket del client
            Socket client = résultat.AsyncState as Socket;
    ...
        }


        // monitoraggio del processo
        private void logEvent(string msg) {
....
        }
    }
}
  • riga 1: la classe Socket fa parte dello spazio dei nomi System.Net.Sockets.

Alcuni dati devono essere condivisi tra diversi metodi del modulo. Si tratta dei seguenti:

  • riga 7: client è il socket di comunicazione con il server
  • righe 6 e 8: il client riceverà i propri messaggi in un array di byte denominato data.
  • riga 9: «réponse» è la risposta inviata dal server.
  • riga 10: finLigne è il marcatore di fine riga utilizzato dal client TCP; è inizializzato per impostazione predefinita a RCLF, ma può essere modificato dall’utente tramite i pulsanti di opzione [3].

La procedura connexion alla riga 19 stabilisce la connessione al server TCP:

  • righe 21-25: si verifica che il nome del server non sia vuoto. In caso contrario, l'evento viene registrato in listBoxEvts tramite il metodo logEvent alla riga 49.
  • riga 27: si segnala che la connessione sta per avvenire
  • riga 30: si crea l’oggetto Socket necessario per la comunicazione TCP/IP. Il costruttore accetta tre parametri:
    • AddressFamily addressFamily: la famiglia di indirizzi IP del client e del server, in questo caso indirizzi IPv4 (AddressFamily.InterNetwork)
    • SocketType socketType: il tipo di socket. Il tipo SocketType.Stream è adatto alle connessioni TCP/IP
    • ProtocolType protocolType: il tipo di protocollo Internet utilizzato, in questo caso il protocollo TCP
  • riga 32: la connessione viene stabilita in modo asincrono. La connessione viene avviata, ma l'esecuzione prosegue senza attendere il suo completamento. Il metodo [Socket].BeginConnect accetta quattro parametri:
    • IPAddress ipAddress: l'indirizzo IP del computer su cui è in esecuzione il servizio a cui ci si deve connettere
    • Int32 port: la porta del servizio
    • AsyncCallBack asyncCallBack: AsyncCallBack è un tipo delegato:
public void AsyncCallBack(IAsyncResult ar);

Il metodo asyncCallBack passato come terzo parametro del metodo BeginConnect deve essere un metodo che accetta un tipo IAsyncCallBack e non restituisce alcun risultato. È il metodo che verrà chiamato una volta stabilita la connessione. Qui passiamo come terzo parametro il metodo connecté della riga 41.

  • (continua)
    • Oggetto state: un oggetto da passare al metodo asyncCallBack. Questo metodo riceve (cfr. delegato sopra) un parametro ar di tipo IAsyncResult. L'oggetto state potrà essere recuperato in ar.AsyncState (riga 43). Qui passiamo come quarto parametro il socket del client.
  • riga 38: il metodo è terminato. L’utente può nuovamente interagire con l’interfaccia grafica. La connessione avviene in background, parallelamente alla gestione degli eventi dell’interfaccia grafica. Sempre in parallelo, il metodo connecté della riga 41 verrà chiamato al termine della connessione, indipendentemente dal fatto che questa si concluda con esito positivo o negativo.

Il codice del metodo connecté è il seguente:


// la connessione è stata stabilita
        private void connecté(IAsyncResult résultat) {
            // si recupera il socket del client
            Socket client = résultat.AsyncState as Socket;
            try {
                // si conclude l'operazione asincrona
                client.EndConnect(résultat);
                // monitoraggio
                logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
                // modulo
                buttonConnexion.Text = "Déconnecter";
                // lettura asincrona dei dati provenienti dal server
                réponse = "";
                client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
            } catch (SocketException e) {
                logEvent(String.Format("erreur de connexion : {0}", e.Message));
                return;
            }
}

        // ricezione dei dati
        private void lecture(IAsyncResult résultat) {
            // si recupera il socket del client
            Socket client = résultat.AsyncState as Socket;
...
        }

  • riga 4: il socket del client viene recuperato dal parametro résultat ricevuto dal metodo. Si ricorda che questo oggetto è quello passato come quarto parametro del metodo BeginConnect.
  • riga 7: il tentativo di connessione viene terminato dal metodo EndConnect, al quale deve essere passato il parametro résultat ricevuto dal metodo.
  • riga 9: l'evento viene registrato nell'elenco degli eventi
  • riga 11: il pulsante [Connecter] diventa un pulsante [Déconnecter] affinché l’utente possa richiedere la disconnessione.
  • riga 13: viene inizializzata la risposta del server. Verrà aggiornata tramite ripetute chiamate al metodo asincrono BeginReceive.
  • riga 14: prima chiamata al metodo asincrono BeginReceive. Questo viene chiamato con i seguenti parametri:
    • byte[] buffer: il buffer in cui inserire i dati che verranno ricevuti – in questo caso il buffer è data
    • int offset: a partire da quale posizione del buffer inserire i dati che verranno ricevuti – in questo caso l’offset è 0, c.a.d, il che significa che i dati vengono inseriti a partire dal primo byte del buffer.
    • int size: la dimensione in byte del buffer - in questo caso la dimensione è tailleBuffer.
    • SocketFlags socketFlags: configurazione del socket – in questo caso nessuna configurazione
    • AsyncCallBack asyncCallBack: il metodo da richiamare al termine della ricezione. Ciò avverrà o perché il buffer ha ricevuto dei dati o perché la connessione è stata chiusa. In questo caso, il metodo di callback è il metodo lecture della riga 22.
    • Oggetto state: l’oggetto da passare al metodo di callback asyncCallBack. In questo caso, si passa nuovamente il socket del client.

Si noti che tutto ciò avviene senza alcun intervento da parte dell’utente, se non la richiesta iniziale di connessione tramite il pulsante [Connecter]. Al termine del metodo connecté, viene eseguito in background un altro metodo: il metodo lecture che esamineremo ora.


// ricezione dei dati
        private void lecture(IAsyncResult résultat) {
            // si recupera il socket del client
            Socket client = résultat.AsyncState as Socket;
            int nbOctetsReçus = 0;
            bool erreur = false;
            try {
                // numero di byte ricevuti
                nbOctetsReçus = client.EndReceive(résultat);
                if (nbOctetsReçus == 0) {
                    // il server non risponde più
                    logEvent("le serveur a fermé la connexion");
                }
            } catch (Exception e) {
                // si è verificato un problema di ricezione
                logEvent(String.Format("erreur de réception : {0}", e.Message));
                erreur = true;
            }
            // terminato?
            if (nbOctetsReçus == 0 || erreur) {
                // se necessario, si disconnette il client
                déconnexion();
                // si visualizza la fine della risposta
                afficherRéponseServeur(réponse, true);
                // fine lettura
                return;
            }
            // si recuperano i dati ricevuti
            string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
            // li si aggiunge ai dati già ricevuti
            réponse += données;
            // si visualizza la risposta
            afficherRéponseServeur(réponse, false);
            // si continua a leggere
            client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
        }
  • riga 2: il metodo lecture viene attivato in background quando il buffer data ha ricevuto dei dati o quando la connessione è stata chiusa dal server.
  • riga 9: la richiesta asincrona di lettura viene terminata da EndReceive. Anche in questo caso, questo metodo deve essere chiamato con il parametro ricevuto dalla funzione di callback. Il metodo EndReceive restituisce il numero di byte ricevuti nel buffer di lettura.
  • riga 10: se il numero di byte è pari a zero, significa che la connessione è stata chiusa dal server.
  • riga 12: si registra l’evento nell’elenco degli eventi
  • riga 14: si gestisce un'eventuale eccezione
  • righe 16-17: si registra l’evento nell’elenco degli eventi e si registra l’errore
  • riga 20: si verifica se è necessario chiudere la connessione
  • riga 22: si chiude la connessione lato client con un metodo déconnexion che vedremo in seguito.
  • riga 24: la risposta del server, c.a.d. La variabile globale réponse viene visualizzata nell’elenco di dialogo listBoxDialogue tramite un metodo privato afficherRéponseServeur.
  • riga 26: fine del metodo asincrono lecture
  • riga 29: i byte ricevuti vengono inseriti in una stringa di caratteri nel formato UTF8.
  • riga 31: vengono aggiunti alla risposta in fase di costruzione
  • riga 33: la risposta viene visualizzata nell’elenco listBoxDialogue.
  • riga 35: si riprende ad attendere i dati provenienti dal server

In definitiva, il metodo asincrono lecture non si interrompe mai. Continua a leggere i dati provenienti dal server e li visualizza nell’elenco listBoxDialogue. Si interrompe solo quando la connessione viene chiusa dal server o dall’utente stesso.

11.8.4. Disconnessione dal server

Premendo il pulsante [Déconnecter] viene eseguito il seguente metodo:


        private void buttonConnexion_Click(object sender, EventArgs e) {
            // connessione o disconnessione?
            if (buttonConnexion.Text == "Déconnecter")
                déconnexion();
            else
                connexion();
}
  • riga 3: il pulsante può avere l'etichetta [Connecter] o [Déconnecter].

Il metodo déconnexion garantisce la disconnessione del client:


private void déconnexion() {
            // chiusura del socket
            if (client != null && client.Connected) {
                try {
                    // monitoraggio
                    logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
                    // disconnessione
                    client.Shutdown(SocketShutdown.Both);
                    client.Close();
                    // modulo
                    buttonConnexion.Text = "Connecter";
                } catch (Exception ex) {
                    // monitoraggio
                    logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
                }
            }
        }
  • riga 3: se il client esiste ed è connesso
  • riga 6: si annuncia la disconnessione in listBoxEvts. La proprietà client.RemoteEndPoint fornisce la coppia (Indirizzo IP, porta) dell’altra estremità della connessione, in questo caso del server (c.a.d).
  • riga 8: il flusso di dati del socket viene chiuso con il metodo ShutDown. Il flusso di dati di un socket è bidirezionale: il socket trasmette e riceve dati. Il parametro del metodo ShutDown può essere: ShutDown.Receive per chiudere il flusso di ricezione, Shutdonw.Send per chiudere il flusso di trasmissione o ShutDown.Both per chiudere entrambi i flussi.
  • riga 9: si liberano le risorse associate al socket
  • riga 11: il pulsante [Déconnecter] diventa il pulsante [Connecter]
  • righe 12-15: gestione di un'eventuale eccezione

11.8.5. Invio asincrono di dati al server

Quando l’utente conferma il messaggio nel campo textBoxMsgToServeur, viene eseguito il seguente metodo:


        private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
            // tasto [Entrée] ?
            if (e.KeyChar == 13 && client.Connected) {
                envoyerMessage();
            }
}
  • righe 3-5: se l'utente ha premuto il tasto [Entrée] e se il socket del client è connesso, allora il messaggio del campo textBoxMsgToServeur viene inviato con il metodo envoyerMessage.

Il metodo envoyerMessage è il seguente:


        private void envoyerMessage() {
            // invia un messaggio in modo asincrono
            // il messaggio
            byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
            // è stato inviato
            client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
            // dialogo
            logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
            // reset messaggio
            textBoxMsgToServeur.Clear();
}
  • riga 4: si aggiunge al messaggio il carattere di fine riga del client e lo si inserisce nell'array di byte message.
  • riga 6: viene avviata una trasmissione asincrona con il metodo BeginSend. I parametri di BeginSend sono identici a quelli del metodo BeginReceive. Al termine dell'operazione di invio asincrono del messaggio verrà chiamato il metodo écriture.
  • riga 8: il messaggio inviato viene aggiunto alla lista listBoxDialogue per consentire il monitoraggio del dialogo client/server
  • riga 10: il messaggio inviato viene cancellato dall’interfaccia grafica

Il metodo di callback écriture è il seguente:


        private void écriture(IAsyncResult résultat) {
            // risultato dell'invio di un messaggio
            Socket client = résultat.AsyncState as Socket;
            try {
                client.EndSend(résultat);
            } catch (Exception e) {
                // si è verificato un problema di invio
                logEvent(String.Format("erreur d'émission : {0}", e.Message));
            }
}
  • riga 4: il metodo di callback écriture riceve un parametro risultato di tipo IAsyncResult.
  • riga 3: nel parametro résultat si recupera il socket del client. Questo socket era il quinto parametro del metodo BeginSend.
  • riga 5: si conclude l'operazione asincrona di invio.

Non si attende il completamento della trasmissione di un messaggio per restituire il controllo all’utente. Quest’ultimo può quindi inviare un secondo messaggio mentre la trasmissione del primo non è ancora terminata.

11.8.6. Visualizzazione degli eventi e del dialogo client/server

Gli eventi vengono visualizzati dal metodo logEvents:


        // monitoraggio del processo
        private void logEvent(string msg) {
            listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
        }

        private void logEventCallBack(string msg) {
            // visualizzazione del messaggio
            msg = msg.Replace(finLigne, " ");
            listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
  • riga 2: il metodo logEvents riceve come parametro il messaggio da aggiungere all’elenco listBoxEvts.
  • riga 3: non è possibile utilizzare direttamente il componente listBoxEvents. Infatti, il metodo logEvents viene chiamato da due tipi di thread:
    • il thread principale proprietario dell'interfaccia grafica, ad esempio quando segnala che è in corso un tentativo di connessione
    • un thread secondario che gestisce un'operazione asincrona. Questo tipo di thread non è proprietario dei componenti e il suo accesso a un componente C deve essere controllato da un'operazione C.Invoke. Questa operazione indica al controllo C che un thread intende eseguire un'operazione su di esso. Il metodo Invoke accetta due parametri:
      • una funzione di callback di tipo delegate. Questa funzione di callback verrà eseguita dal thread proprietario dell'interfaccia grafica e non dal thread che esegue il metodo C.Invoke.
      • un oggetto che verrà passato alla funzione di callback.

In questo caso, il primo parametro passato al metodo Invoke è un'istanza del seguente delegato:


        public delegate void writeLog(string log);

Il delegato writeLog ha un parametro di tipo string e non restituisce alcun risultato. Il parametro sarà il messaggio da registrare in listBoxEvts.

Riga 3: il primo parametro passato al metodo Invoke è il metodo logEventCallBack della riga 6. Esso corrisponde correttamente alla firma del delegato writeLog. Il secondo parametro passato al metodo Invoke è il messaggio che verrà passato come parametro al metodo logEventCallBack.

L'operazione Invoke è un'operazione sincrona. L'esecuzione del thread secondario rimane bloccata finché il thread proprietario del controllo non esegue il metodo di callback.

  • riga 6: il metodo di callback eseguito dal thread dell'interfaccia grafica riceve il messaggio da visualizzare nel controllo listBoxEvts.
  • riga 9: l’evento viene registrato al primo posto dell’elenco in modo che gli eventi più recenti appaiano in cima all’elenco.

I messaggi del dialogo client/server vengono visualizzati tramite il metodo logDialogue:


        // monitoraggio della conversazione
        private void logDialogue(string msg) {
            listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
        }
        private void logDialogueCallBack(string msg) {
            // visualizzazione del messaggio
            msg = msg.Replace(finLigne, " ");
            listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}

Il principio è lo stesso del metodo logEvent.

I messaggi ricevuti dal client vengono visualizzati tramite il metodo afficherRéponseServeur:


        private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}

Il primo parametro è il messaggio da visualizzare. Questo messaggio può essere costituito da una serie di righe. Infatti, il client legge i dati provenienti dal server in blocchi di tailleBuffer (1024) byte. In questi 1024 byte si possono trovare diverse righe, riconoscibili dal loro carattere di fine riga "\n". L'ultima riga può essere incompleta, poiché il suo carattere di fine riga si trova nei 1024 byte successivi. Il metodo individua nel messaggio le righe terminate da "\n" e chiede quindi a logDialogue di visualizzarle. Il secondo parametro del metodo indica se visualizzare l’ultima riga trovata o lasciarla nel buffer affinché venga completata dal messaggio successivo. Il codice è piuttosto complesso e non presenta interesse in questa sede; pertanto non verrà commentato.

11.8.7. Conclusione

Lo stesso esempio potrebbe essere gestito con operazioni sincrone. In questo caso, l’aspetto asincrono dell’interfaccia grafica apporta pochi vantaggi all’utente. Tuttavia, se l’utente si connette e poi si accorge che il server «non risponde più», ha la possibilità di disconnettersi grazie al fatto che l’interfaccia grafica continua a rispondere agli eventi durante l’esecuzione delle operazioni asincrone. Questo esempio piuttosto complesso ci ha permesso di presentare nuovi concetti:

  • l’uso dei socket
  • l'uso di metodi asincroni. Quanto visto fa parte di uno standard. Esistono altri metodi asincroni che funzionano secondo lo stesso modello.
  • l'aggiornamento dei controlli di un'interfaccia grafica tramite thread secondari.

La comunicazione TCP/IP asincrona offre vantaggi più significativi per un server rispetto a quelli illustrati nell’esempio precedente. È noto che il server serve i propri clienti tramite thread secondari. Se il suo pool di thread contiene N thread, ciò significa che può servire solo N clienti contemporaneamente. Se tutti i N thread eseguono un'operazione bloccante (sincrona), non ci sono più thread disponibili per un nuovo cliente finché una delle operazioni bloccanti non termina e libera un thread. Se sui thread si eseguono operazioni asincrone anziché sincrone, un thread non viene mai bloccato e può essere rapidamente riutilizzato per nuovi clienti.

11.9. Esempio di applicazione, versione 8: Server per il calcolo delle imposte

11.9.1. L’architettura della nuova versione

Riprendiamo l’applicazione per il calcolo delle imposte già trattata in varie forme. Ricordiamo la sua ultima versione, quella della versione 7 del paragrafo 9.8.

I dati erano contenuti in un database e il livello [ui] costituiva un’interfaccia grafica:

 

Riprenderemo questa architettura e la distribuiremo su due macchine:

  • una macchina [serveur] ospiterà i livelli [metier] e [dao] della versione 7. Verrà creato un livello TCP/IP [serveur] [1] per consentire agli utenti Internet di interrogare il servizio di calcolo delle imposte.
  • Un server [client] ospiterà il livello [ui] della versione 7. Verrà realizzato un livello TCP/IP [client] [2] per consentire al livello [ui] di interrogare il servizio di calcolo delle imposte.

L’architettura subisce qui un cambiamento radicale. La versione 7 era un’applicazione Windows monoutente. La versione 8 diventa un’applicazione client/server basata su Internet. Il server potrà servire più client contemporaneamente.

Per prima cosa scriveremo la parte [serveur] dell’applicazione.

11.9.2. Il server di calcolo delle imposte

11.9.2.1. Il progetto Visual Studio

Il progetto Visual Studio sarà il seguente:

  • in [1], il progetto. Al suo interno si trovano i seguenti elementi:
  • [ServeurImpot.cs]: il server TCP/IP per il calcolo delle imposte sotto forma di applicazione da console.
  • [dbimpots.sdf]: il database SQL Server Compact della versione 7 descritto al paragrafo 9.8.5.
  • [App.config]: il file di configurazione dell’applicazione.
  • In [2], la cartella [lib] contiene i file DLL necessari al progetto:
    • [ImpotsV7-dao]: il livello [dao] della versione 7
    • [ImpotsV7-metier]: il livello [metier] della versione 7
    • [antlr.runtime, CommonLogging, Spring.Core] per Spring
  • in [3], i riferimenti del progetto

11.9.2.2. Configurazione dell'applicazione

Il file [App.config] viene utilizzato da Spring. Il suo contenuto è il seguente:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
                <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
                <constructor-arg index="2" value="select data1, data2, data3 from data"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • righe 16-20: configurazione del livello [dao] associato al database SQL Server compact
  • righe 21-23: configurazione del livello [metier].

Si tratta del file di configurazione utilizzato nel livello [ui] della versione 7. È stato presentato al paragrafo 9.8.4.

11.9.2.3. Funzionamento del server

All’avvio del server, l’applicazione server istanzia i livelli [metier] e [dao], quindi visualizza un’interfaccia di console di amministrazione:

  

La console di amministrazione accetta i seguenti comandi:

start port
per avviare il servizio su una determinata porta
stop
per arrestare il servizio. Successivamente, è possibile riavviarlo sulla stessa porta o su un’altra.
echo start
per attivare l'eco del dialogo client/server sulla console
echo stop
per disattivare l'eco
status
per visualizzare lo stato attivo/inattivo del servizio
quit
per uscire dall'applicazione

Avviamo il server:

1
2
3
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt lancé sur le port 27
Serveur de calcul d'impôt >

Avviamo ora il client TCP grafico asincrono descritto in precedenza nel paragrafo 11.8.

Image

Il client è connesso. Può inviare i seguenti comandi al server di calcolo delle imposte:

aide
per ottenere l’elenco dei comandi consentiti
impot marié nbEnfants salaireAnnuel
per calcolare l'imposta di una persona con nbEnfants figli e uno stipendio di salaireAnnuel euro. marié è pari a o se la persona è sposata, n altrimenti.
aurevoir
per chiudere la connessione con il server

Ecco un esempio di dialogo:

Sul lato server, la console visualizza quanto segue:

1
2
3
4
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt >Serveur de calcul d'impôt lancé sur le port 27
Début du service au client 0
Fin du service au client 0

Attiviamo l’echo e ricominciamo una nuova sessione dal client grafico:

 

La console di amministrazione visualizza quindi quanto segue:

1
2
3
4
5
6
7
echo start
Serveur de calcul d'impôt >Début du service au client 1
<--- Client 1 : aide
---> Client 1 : Commandes acceptées
1-aide
2-impot marié(O/N) nbEnfants salaireAnnuel
3-aurevoir
  • riga 1: l'eco della comunicazione client/server è attivato
  • riga 2: è arrivata una richiesta da un client
  • riga 3: ha inviato il comando [aide]
  • righe 4-7: la risposta del server su 4 righe.

Arrestiamo il servizio:

1
2
3
stop
L'erreur suivante s'est produite sur le serveur : Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall
Serveur de calcul d'impôt >
  • riga 1: si richiede l'arresto del servizio (non dell'applicazione stessa)
  • riga 2: si verifica un'eccezione dovuta al fatto che il server, bloccato in attesa di una richiesta da parte del client, è stato bruscamente interrotto a causa della chiusura del servizio di ascolto.
  • riga 3: il servizio può ora essere riavviato tramite start port o arrestato tramite quit.

Prima che il servizio di ascolto venisse arrestato, un client era in servizio su un’altra connessione. Questa connessione non viene chiusa dalla chiusura del socket di ascolto. Il client può continuare a inviare comandi: il thread di servizio a cui era stato associato prima della chiusura del servizio di ascolto continua a rispondergli:

Image

11.9.3. Il codice del server TCP per il calcolo delle imposte

1
  

Il codice del server [ServeurImpot.cs] è il seguente:


...
namespace Chap9 {
    public class ServeurImpot {

        // dati condivisi tra thread e metodi
        private static IImpotMetier metier = null;
        private static int port;
        private static TcpListener service;
        private static bool actif = false;
        private static bool echo = false;

        // programma principale
        public static void Main(string[] args) {
            // istanze dei livelli [metier] e [dao]
            IApplicationContext ctx = null;
            metier = null;
            try {
                // contesto Spring
                ctx = ContextRegistry.GetContext();
                // viene richiesto un riferimento al livello [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");

                // configurazione del pool di thread
                ThreadPool.SetMinThreads(10, 10);
                ThreadPool.SetMaxThreads(10, 10);

                // legge i comandi di amministrazione del server digitati da tastiera in un ciclo infinito
                string commande = null;
                string[] champs = null;
                while (true) {
                    // prompt
                    Console.Write("Serveur de calcul d'impôt >");
                    // lettura comando
                    commande = Console.ReadLine().Trim().ToLower();
                    champs = Regex.Split(commande, @"\s+");
                    // esecuzione comando
                    switch (champs[0]) {
                        case "start":
                            // attivo?
                            if (actif) {
                                //errore
                                Console.WriteLine("Le serveur est déjà actif");
                            } else {
                                // verifica porta
                                if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
                                    Console.WriteLine("Syntaxe : start port. Port incorrect");
                                } else {
                                    // avvio del servizio di ascolto
                                    ThreadPool.QueueUserWorkItem(doEcoute, null);
                                }
                            }
                            break;
                        case "echo":
                            // echo start / stop
                            if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
                                Console.WriteLine("Syntaxe : echo start / stop");
                            } else {
                                echo = champs[1] == "start";
                            }
                            break;
                        case "stop":
                            // fine del servizio
                            if (actif) {
                                service.Stop();
                                actif = false;
                            }
                            break;
                        case "status":
                            // stato del server
                            if (actif) {
                                Console.WriteLine("Le service est lancé sur le port {0}", port);
                            } else {
                                Console.WriteLine("Le service n'est pas lancé}");
                            }
                            break;
                        case "quit":
                            // uscita dall'applicazione
                            Console.WriteLine("Fin du service");
                            Environment.Exit(0);
                            break;
                        default:
                            // comando non corretto
                            Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
                            break;
                    }
                }
            } catch (Exception e1) {
                // visualizzazione di un'eccezione
                Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
                return;
            }
        }


        private static void doEcoute(Object data) {
...
        }

....
    }
}
  • righe 18-21: i layer [metier] e [dao] vengono istanziati da Spring, configurato tramite [App.config]. Viene quindi inizializzata la variabile globale metier alla riga 6.
  • righe 24-25: si configura il pool di thread dell'applicazione con un numero minimo e massimo di 10 thread.
  • righe 30-86: il ciclo di immissione dei comandi di amministrazione del servizio (start, stop, quit, echo, status).
  • riga 32: prompt del server per ogni nuovo comando
  • riga 34: lettura del comando di amministrazione
  • riga 35: il comando viene suddiviso in campi per essere analizzato
  • righe 38-52: il comando start port, che ha lo scopo di avviare il servizio di ascolto
    • riga 40: se il servizio è già attivo, non occorre fare nulla
    • riga 45: si verifica che la porta sia effettivamente presente e corretta. In caso affermativo, viene impostata la variabile globale port della riga 7.
    • riga 49: il servizio di ascolto verrà gestito da un thread secondario in modo che il thread principale possa continuare a eseguire i comandi della console. Se il metodo doEcoute riesce a stabilire la connessione, vengono inizializzate le variabili globali service della riga 8 e actif della riga 9.
  • righe 53-60: il comando echo start / stop che attiva / disattiva l'eco del dialogo client/server sulla console
    • riga 58: viene impostata la variabile globale echo della riga 7
  • righe 61-67: il comando stop che arresta il servizio di ascolto.
    • riga 64: arresto del servizio di ascolto
  • righe 68-75: il comando status che visualizza lo stato attivo/inattivo del servizio
  • righe 76-80: il comando quit che arresta tutto.

Il thread incaricato di ascoltare le richieste dei client esegue il seguente metodo doEcoute:


        private static void doEcoute(Object data) {
            // thread di ascolto delle richieste dei client
            try {
                // si crea il servizio
                service = new TcpListener(IPAddress.Any, port);
                // si avvia
                service.Start();
                // il server è attivo
                actif = true;
                // monitoraggio
                Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
                // ciclo di servizio ai clienti
                TcpClient tcpClient = null;
                // n. cliente
                int numClient = 0;
                // ciclo infinito
                while (true) {
                    // in attesa di un cliente
                    tcpClient = service.AcceptTcpClient();
                    // il servizio è gestito da un'altra attività
                    ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
                    // cliente successivo
                    numClient++;
                }
            } catch (Exception ex) {
                // viene segnalato l'errore
                Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
            }
        }

        // informazioni sul cliente
        internal class Client {
            public TcpClient CanalTcp { get; set; }        // collegamento con il cliente
            public int NumClient { get; set; }            // numero cliente
}

Si tratta di un codice simile a quello del server echo esaminato nel paragrafo 11.6.1. Commentiamo solo le differenze:

  • riga 7: il servizio di ascolto è stato avviato
  • riga 9: si nota che il servizio è ora attivo

Linea 21: i clienti vengono gestiti da thread di servizio che eseguono il seguente metodo doService:


private static void doService(Object infos) {
            // si recupera il cliente da servire
            Client client = infos as Client;
            // si fornisce il servizio al cliente
            Console.WriteLine("Début du service au client {0}", client.NumClient);
            // elaborazione del collegamento TcpClient
            try {
                using (TcpClient tcpClient = client.CanalTcp) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flusso di uscita non bufferizzato
                                writer.AutoFlush = true;
                                // invio di un messaggio di benvenuto al cliente
                                writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
                                // ciclo di lettura richiesta/scrittura risposta
                                string demande = null;
                                bool serviceFini = false;
                                while (!serviceFini && (demande = reader.ReadLine()) != null) {
                                    // monitoraggio console
                                    if (echo) {
                                        Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
                                    }
                                    // analisi della richiesta
                                    demande = demande.Trim().ToLower();
                                    // richiesta vuota?
                                    if (demande.Length == 0) {
                                        // richiesta errata
                                        writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
                                        return;
                                    }

                                    // scomposizione della richiesta in campi
                                    string[] champs = Regex.Split(demande, @"\s+");
                                    // analisi
                                    switch (champs[0].ToLower()) {
                                        case "aide":
                                            writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
                                            break;
                                        case "impot":
                                            // calcolo dell'imposta
                                            writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
                                            break;
                                        case "aurevoir":
                                            serviceFini = true;
                                            writeClient(writer, client.NumClient, "Au revoir...");
                                            break;
                                        default:
                                            writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
                                            break;
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // errore
                Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
            } finally {
                Console.WriteLine("Fin du service au client {0}", client.NumClient);
            }
        }

        private static void writeClient(StreamWriter writer, int numClient, string message) {
            // output console?
            if (echo) {
                Console.WriteLine("---> Client {0} : {1}", numClient, message);
            }
            // invio messaggio al cliente
            writer.WriteLine(message);
}

Anche in questo caso, il codice è simile a quello del server di eco esaminato nel paragrafo 11.6.1. Commentiamo solo le differenze:

  • riga 15: una volta che il cliente si è connesso, il server gli invia un messaggio di benvenuto.
  • righe 19-52: il ciclo di lettura dei comandi del client. Il ciclo si interrompe quando il client invia il comando "aurevoir".
  • riga 27: caso di comando vuoto
  • riga 34: la richiesta viene scomposta in campi per essere analizzata
  • riga 37: comando aide: il cliente richiede l'elenco dei comandi autorizzati
  • riga 40: comando impot: il cliente richiede il calcolo dell’imposta. Si risponde con il messaggio restituito dal metodo calculImpot, che descriveremo in dettaglio tra poco.
  • riga 44: comando aurevoir: il cliente indica di aver terminato.
  • riga 45: ci si prepara a uscire dal ciclo di lettura delle richieste del cliente (righe 19-52)
  • riga 46: si risponde al cliente con un messaggio di saluto
  • riga 48: comando non corretto. Si invia al cliente un messaggio di errore.

L'elaborazione dell'ordine impot è gestita dal seguente metodo calculImpot:


private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
            // richiesta calcolo stato civile (S/N) nbEnfants salaireAnnuel
            // sono necessari 4 campi
            if (champs.Length != 4) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // campi [1]
            string marié = champs[1];
            if (marié != "o" && marié != "n") {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // campi [2]
            int nbEnfants;
            if (!int.TryParse(champs[2], out nbEnfants)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // campi [3]
            int salaireAnnuel;
            if (!int.TryParse(champs[3], out salaireAnnuel)) {
                return "Commande calcul incorrecte. Utilisez la commande aide.";
            }
            // Va bene - si calcola l'imposta
            int impot = 0;
            try {
                impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
                return impot.ToString();
            } catch (Exception ex) {
                return ex.Message;
            }
        }
  • riga 1: il metodo riceve come terzo parametro l’array dei campi dell’ordine impot. Se quest’ultimo è stato formulato correttamente, ha la forma «reddito coniugato nbEnfants salaireAnnuel». Il metodo restituisce come risultato la risposta da inviare al cliente.
  • riga 4: si verifica che l'ordine abbia 4 campi
  • riga 8: si verifica che il campo marié sia valido
  • riga 14: si verifica che il campo nbEnfants sia valido
  • riga 19: si verifica che il campo salaireAnnuel sia valido
  • riga 25: l'imposta viene calcolata utilizzando il metodo CalculerImpot del livello [metier]. Si ricorda che questo livello è incapsulato in un DLL.
  • riga 26: se il livello [metier] ha restituito un risultato, questo viene restituito al cliente.
  • riga 28: se il livello [metier] ha generato un'eccezione, il messaggio relativo a tale eccezione viene restituito al client.

11.9.4. Il client grafico del server TCP per il calcolo delle imposte

11.9.4.1. Il progetto Visual Studio

Il progetto Visual Studio del client grafico sarà il seguente:

  • in [1], i due progetti della soluzione, uno per ciascuno dei due livelli dell'applicazione
  • in [2], il client TCP che funge da livello [metier] per il livello [ui]. Utilizzeremo quindi entrambi i termini.
  • in [3], il livello [ui] della versione 7, con un unico dettaglio di cui parleremo

11.9.4.2. Lo strato [metier]

L’interfaccia IImpotMetier non è cambiata. È sempre quella della versione 7:


namespace Metier {
    public interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}

L'implementazione di questa interfaccia è la seguente classe [ImpotMetierTcp]:


using System.Net.Sockets;
using System.IO;
namespace Metier {
    public class ImpotMetierTcp : IImpotMetier {

        // informazioni [serveur]
        private string Serveur { get; set; }
        private int Port { get; set; }

        // calcolo dell'imposta
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
                // ci si connette al servizio
                using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
                    using (NetworkStream networkStream = tcpClient.GetStream()) {
                        using (StreamReader reader = new StreamReader(networkStream)) {
                            using (StreamWriter writer = new StreamWriter(networkStream)) {
                                // flusso di output non bufferizzato
                                writer.AutoFlush = true;
                                // si salta il messaggio di benvenuto
                                reader.ReadLine();
                                // richiesta
                                writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
                                // risposta
                                return int.Parse(reader.ReadLine());
                            }
                        }
                    }
                }
            }
        }
    }
  • riga 7: il nome o l'indirizzo IP del server TCP per il calcolo delle imposte
  • riga 8: la porta di ascolto di questo server
  • queste due proprietà saranno inizializzate da Spring al momento dell'istanziazione della classe [ImpotMetierTcp].
  • riga 11: il metodo di calcolo dell’imposta. Quando viene eseguito, le proprietà Serveur e Port sono già inizializzate. Nel codice si ritrova la procedura classica di un client TCP
  • riga 13: viene aperta la connessione con il server
  • righe 14-16: si recupera (riga 14) il flusso di rete associato a questa connessione, dal quale si ricavano un flusso di lettura (riga 15) e un flusso di scrittura (riga 16).
  • riga 18: il flusso di scrittura deve essere non bufferizzato
  • riga 20: qui occorre ricordare che all’apertura della connessione, il server invia al client una prima riga che costituisce il messaggio di benvenuto «Benvenuto sul server di calcolo delle imposte». Questo messaggio viene letto e ignorato.
  • riga 22: si invia al server il comando del tipo: impot o 2 60000 per chiedergli di calcolare l’imposta di una persona sposata con 2 figli e uno stipendio annuo di 60000 euro.
  • riga 24: il server risponde indicando l'importo dell'imposta nel formato "4282" oppure con un messaggio di errore se il comando era errato (cosa che qui non accadrà) o se il calcolo dell'imposta ha riscontrato un problema. In questo caso, quest'ultimo scenario non è gestito, ma sarebbe stato sicuramente più "pulito" farlo. Infatti, se la riga letta è un messaggio di errore, verrà generata un'eccezione perché la conversione in un numero intero fallirà. L'eccezione intercettata dall'interfaccia grafica risulterà essere un errore di conversione, mentre l'eccezione originale è di natura completamente diversa. Il lettore è invitato a migliorare questo codice.
  • righe 25-28: liberazione di tutte le risorse utilizzate con una clausola «using».

Il livello [metier] viene compilato in DLL ImpotsV8-metier.dll:

Image

11.9.4.3. Il livello [ui]

Il livello [ui] [1,3] è quello analizzato nella versione 7 al paragrafo 9.8.4, con tre differenze:

  • la configurazione del livello [metier] in [App.config] è diversa perché la sua implementazione è cambiata
  • l’interfaccia grafica [Form1.cs] è stata modificata per visualizzare un’eventuale eccezione
  • il livello [metier] si trova all’interno di DLL [ImpotsV8-metier.dll].

Il file [App.config] è il seguente:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
                <property name="Serveur" value="localhost"/>
                <property name="Port" value="27"/>
            </object>
        </objects>
    </spring>
</configuration>
  • riga 16: istanziazione del livello [metier] con la classe Metier.ImpotMetierTcp del livello DLL ImpotsV8-metier.dll
  • righe 17-18: vengono inizializzate le proprietà Server e Porta della classe Metier.ImpotMetierTcp. Il server sarà sul computer localhost e opererà sulla porta 27.

L'interfaccia grafica presentata all'utente è la seguente:

  • in [1] è stato aggiunto un TextBox per visualizzare un'eventuale eccezione. Questo campo non era presente nella versione precedente.

A parte questo dettaglio, il codice del modulo è quello già esaminato nel paragrafo 6.4.3. Si invita il lettore a fare riferimento a tale paragrafo. In [2] è riportato un esempio di esecuzione ottenuto con un server avviato nel modo seguente:

1
2
3
4
5
6
7
8
9
Serveur de calcul d'impôt >start 27
Serveur de calcul d'impôt lancé sur le port 27
Serveur de calcul d'impôt >echo start
Serveur de calcul d'impôt >
...
Début du service au client 9
<--- Client 9 : impot o 2 60000
---> Client 9 : 4282
Fin du service au client 9

La schermata [2] del client corrisponde alle righe del cliente 9 riportate sopra.

11.9.5. Conclusione

Ancora una volta, siamo riusciti a riutilizzare il codice esistente, senza modifiche (livelli [metier], [dao] del server) o con modifiche minime (livello [ui] del client). Ciò è stato reso possibile dal nostro utilizzo sistematico delle interfacce e dalla loro istanziazione con Spring. Se nella versione 7 avessimo inserito il codice di business direttamente nei gestori di eventi dell’interfaccia grafica, tale codice non sarebbe stato riutilizzabile. Questo è il principale svantaggio delle architetture a 1 livello.

Si noti infine che il livello [ui] non è a conoscenza del fatto che sia un server remoto a calcolare l’importo dell’imposta.