Skip to content

9. Programmazione TCP-IP

9.1. Informazioni generali

9.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. È opportuno che il lettore abbia una comprensione generale del funzionamento delle reti e, in particolare, dei protocolli TCP/IP prima di affrontare la realizzazione di applicazioni distribuite.

Il testo che segue è una traduzione parziale di un testo tratto dal documento "Lan Workplace for Dos - Administrator's Guide" di NOVELL, un documento risalente agli inizi degli anni '90.

-----------------------------------

Il concetto generale di creare 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

9.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 del sistema di codifica delle informazioni (analogico 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.

9.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: una macchina che trasmette invia le informazioni sul cavo indicando l’indirizzo della macchina destinataria. Tutte le macchine collegate ricevono quindi queste informazioni e solo quella a cui sono destinate le conserva.
  • Il metodo di accesso è il seguente: il trasmettitore che desidera trasmettere ascolta il cavo e rileva così la presenza o meno di un'onda portante, la cui presenza indicherebbe che è in corso una trasmissione. Si tratta della tecnica CSMA (Carrier Sense Multiple Access). In assenza di portante, un trasmettitore può decidere di trasmettere a sua volta. Possono essere diversi i trasmettitori a prendere questa decisione. I segnali emessi si sovrappongono: si parla allora di collisione. Il trasmettitore rileva questa situazione: mentre trasmette sul cavo, ascolta ciò che effettivamente vi transita. Se rileva che l’informazione che transita sul cavo non è quella che ha trasmesso, ne deduce che vi è una collisione e interromperà la trasmissione. Gli altri trasmettitori che stavano trasmettendo faranno lo stesso. Ciascuno riprenderà la propria trasmissione dopo un 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 vari 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

9.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 quel punto, il pacchetto IP viene a sua volta incapsulato in una trama fisica e inviato al destinatario tramite il cavo.
  • Sul computer destinatario, il livello di collegamento dati e collegamento fisico esegue l’operazione inversa: incapsula il pacchetto IP dalla trama fisica e lo passa al livello IP.
  • Il livello IP verifica che il pacchetto sia corretto: calcola una somma, in base ai bit ricevuti (checksum), somma che deve ritrovare nell’intestazione del pacchetto. In caso contrario, il pacchetto viene scartato.
  • Se il pacchetto viene dichiarato corretto, il livello IP decapsula il segmento TCP in esso contenuto e lo passa 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 di dati del segmento all’applicazione destinataria di tali dati nel livello superiore.

9.1.5. L'indirizzamento in Internet

Un nodo noeud 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.

9.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 di 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 è 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 vietati, rimangono solo 254 indirizzi consentiti.

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

Abbiamo visto che durante la trasmissione di informazioni da una macchina a un’altra, queste, attraversando il livello IP, venivano incapsulate in pacchetti. Questi hanno la seguente forma:

Il pacchetto IP contiene quindi gli indirizzi Internet dei 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 dispositivi 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, potrebbe trovarsi nella necessità di modificare gli indirizzi IP di tutti i nodi e quindi di modificare i vari file di configurazione dei singoli nodi. Ciò può risultare noioso e fonte di errori se il numero di macchine è elevato. Un metodo consiste nel non assegnare alcun indirizzo IP alle macchine: si inserisce quindi un codice speciale nel file in cui la macchina dovrebbe trovare il proprio indirizzo IP. Scoprendo di non avere un indirizzo IP, la macchina lo richiede secondo un protocollo denominato RARP (Reverse Address Resolution Protocol). Invia quindi sulla rete un pacchetto speciale chiamato pacchetto RARP, analogo al precedente pacchetto ARP, nel quale inserisce il proprio indirizzo fisico. Questo pacchetto viene inviato a tutti i nodi, che riconoscono quindi un pacchetto RARP. Uno di essi, denominato server RARP, possiede un file che riporta la corrispondenza tra indirizzo fisico e indirizzo IP di tutti i nodi. Risponde quindi al mittente del pacchetto RARP, rinviandogli il 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.

9.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.

9.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 e l’indirizzo 193.49.145.3 all’interno della rete n. 2.

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

9.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.

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

9.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.

9.1.7.2. Il protocollo TCP: Protocollo di controllo del trasferimento

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.

9.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 disponibilità di una directory che metta in corrispondenza macchina <--> indirizzo IP. Probabilmente in questa directory le macchine sarebbero designate con nomi simbolici quali:

macchina DPX2/320 dell’Università di Angers

macchina Sun dell'Università di Angers ISERPA

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

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

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

XDR: (Rappresentazione dei dati eXternal)

Creato da Sun MicroSystems, questo protocollo definisce una rappresentazione standard dei dati, indipendente dalla piattaforma.

RPC: (Remote Procedure Call)

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

NFS: Network File System

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.

9.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

9.2. Gestione degli indirizzi di rete

Un computer connesso a Internet è identificato in modo univoco da un indirizzo IP (Internet Protocol) della forma I1.I2.I3.I4, dove In è un numero compreso tra 1 e 254. Può essere identificato anche da un nome, anch'esso univoco. Questo nome non è obbligatorio, poiché le applicazioni utilizzano sempre, in definitiva, gli indirizzi IP dei dispositivi. Essi servono a semplificare 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. L'associazione tra l'indirizzo IP e nomMachine è garantita da un servizio distribuito su Internet chiamato DNS (Domain Name System). La piattaforma .NET offre la classe Dns per la gestione degli indirizzi Internet:

Image

La maggior parte dei metodi della classe sono statici. Esaminiamo quelli che ci interessano:

Overloads Public Shared Function
 GetHostByAddress(ByVal address As
 String) As IPHostEntry
Restituisce un indirizzo IPHostEntry a partire da un indirizzo IP nella forma "I1.I2.I3.I4". Genera un'eccezione se non è possibile trovare la macchina address.
Public Shared Function
 GetHostByName(ByVal hostName As
 String) As IPHostEntry
restituisce un indirizzo IPHostEntry a partire da un nome di macchina. Genera un'eccezione se non è possibile trovare la macchina name.
Public Shared Function 
GetHostName() As String
restituisce il nome del computer su cui è in esecuzione il programma che esegue questa istruzione

Gli indirizzi di rete di tipo IPHostEntry hanno la seguente forma:

Le proprietà che ci interessano:

Public Property AddressList 
As IPAddress ()
elenco degli indirizzi IP di una macchina. Se un indirizzo IP indica una e una sola macchina fisica, una macchina fisica può avere più indirizzi IP. Ciò si verifica se dispone di più schede di rete che la collegano a reti diverse.
Public Property Aliases 
As String ()
elenco degli alias di una macchina, che può essere indicata con un nome principale e degli alias
Public Property HostName 
As String
il nome del computer, se ne possiede uno

Dalla classe IPAddress prenderemo in considerazione il costruttore, le proprietà e i metodi seguenti:

Image

Un oggetto [IPAddress] può essere convertito in una stringa I1.I2.I3.I4 con il metodo ToString(). Viceversa, è possibile ottenere un oggetto IPAddress a partire da una stringa I1.I2.I3.I4 utilizzando il metodo statico IPAddress.Parse("I1.I2.I3.I4"). Consideriamo il seguente programma che visualizza il nome del computer su cui viene eseguito e poi, in modo interattivo, fornisce le corrispondenze tra l'indirizzo IP e il nome del computer:

dos>address1
Machine Locale=tahe

Machine recherchée (fin pour arrêter) : istia.univ-angers.fr
Machine : istia.univ-angers.fr
Adresses IP : 193.49.146.171

Machine recherchée (fin pour arrêter) : 193.49.146.171
Machine : istia.istia.univ-angers.fr
Adresses IP : 193.49.146.171
Alias : 171.146.49.193.in-addr.arpa

Machine recherchée (fin pour arrêter) : www.ibm.com
Machine : www.ibm.com
Adresses IP : 129.42.17.99,129.42.18.99,129.42.19.99,129.42.16.99

Machine recherchée (fin pour arrêter) : 129.42.17.99
Machine : www.ibm.com
Adresses IP : 129.42.17.99

Machine recherchée (fin pour arrêter) : x.y.z
Impossible de trouver la machine [x.y.z]

Machine recherchée (fin pour arrêter) : localhost
Machine : tahe
Adresses IP : 127.0.0.1

Machine recherchée (fin pour arrêter) : 127.0.0.1
Machine : tahe
Adresses IP : 127.0.0.1

Machine recherchée (fin pour arrêter) : tahe
Machine : tahe
Adresses IP : 127.0.0.1

Machine recherchée (fin pour arrêter) : fin

Il programma è il seguente:


' opzioni
Option Explicit On 
Option Strict On

' spazi dei nomi
Imports System
Imports System.Net
Imports System.Text.RegularExpressions

' modulo di test
Public Module adresses

    Sub Main()
        ' visualizza il nome del computer locale
        ' quindi fornisce in modo interattivo informazioni sui computer in rete
        ' identificate da un nome o da un indirizzo IP
        ' macchina locale
        Dim localHost As String = Dns.GetHostName()
        Console.Out.WriteLine(("Machine Locale=" + localHost))

        ' domande e risposte interattive
        Dim machine As String
        Dim adresseMachine As IPHostEntry
        While True
            ' inserimento del nome del computer ricercato
            Console.Out.Write("Machine recherchée (fin pour arrêter) : ")
            machine = Console.In.ReadLine().Trim().ToLower()
            ' finito?
            If machine = "fin" Then
                Exit While
            End If

            ' indirizzo I1.I2.I3.I4 o nome del computer?
            Dim isIPV4 As Boolean = Regex.IsMatch(machine, "^\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\s*$")
            ' gestione delle eccezioni
            Try
                If isIPV4 Then
                    adresseMachine = Dns.GetHostByAddress(machine)
                Else
                    adresseMachine = Dns.GetHostByName(machine)
                End If
                ' il nome
                Console.Out.WriteLine(("Machine : " + adresseMachine.HostName))
                ' gli indirizzi IP
                Console.Out.Write(("Adresses IP : " + adresseMachine.AddressList(0).ToString))
                Dim i As Integer
                For i = 1 To adresseMachine.AddressList.Length - 1
                    Console.Out.Write(("," + adresseMachine.AddressList(i).ToString))
                Next i
                Console.Out.WriteLine()
                ' gli alias
                If adresseMachine.Aliases.Length <> 0 Then
                    Console.Out.Write(("Alias : " + adresseMachine.Aliases(0)))
                    For i = 1 To adresseMachine.Aliases.Length - 1
                        Console.Out.Write(("," + adresseMachine.Aliases(i)))
                    Next i
                    Console.Out.WriteLine()
                End If
            Catch
                ' la macchina non esiste
                Console.Out.WriteLine("Impossible de trouver la machine [" + machine + "]")
            End Try
        End While
    End Sub
End Module

9.3. Programmazione TCP-IP

9.3.1. Informazioni generali

Consideriamo la comunicazione tra due macchine remote A e B:

Quando un'applicazione AppA su una macchina A vuole comunicare con un'applicazione AppB su una macchina B collegata a Internet, deve conoscere diverse informazioni:

  • l’indirizzo IP o il nome del computer B
  • il numero della porta su cui opera l’applicazione AppB. Infatti, il computer B può ospitare numerose applicazioni che operano su Internet. Quando riceve informazioni provenienti dalla rete, deve sapere a quale applicazione sono destinate tali informazioni. Le applicazioni del computer B accedono alla rete tramite interfacce chiamate anche porte di comunicazione. Questa informazione è contenuta nel pacchetto ricevuto dal computer B affinché possa essere consegnato all’applicazione corretta.
  • I protocolli di comunicazione supportati dal computer B. Nel nostro studio utilizzeremo esclusivamente i protocolli TCP-IP.
  • il protocollo di dialogo accettato dall’applicazione AppB. Infatti, i computer A e B «comunicheranno» tra loro. Ciò che si scambieranno sarà incapsulato nei protocolli TCP-IP. Tuttavia, quando, alla fine della catena, l’applicazione AppB riceverà le informazioni inviate dall’applicazione AppA, dovrà essere in grado di interpretarle. Ciò è analogo alla situazione in cui due persone, A e B, comunicano 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.

9.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
  • 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.
  • 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.

9.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.

9.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

9.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

9.3.6. La classe TcpClient

La classe TcpClient è la classe adatta a rappresentare il cliente di un servizio TCP. È definita come segue:

Image

I costruttori, i metodi e le proprietà che ci interessano sono i seguenti:

Public Sub New(ByVal hostname
 As String,ByVal port As Integer)
crea una connessione TCP con il server che opera sulla porta indicata (port) del computer indicato (hostname). Ad esempio, new TcpClient("istia.univ-angers.fr",80) per connettersi alla porta 80 del computer istia.univ-angers.fr
Public Sub Close()
chiude la connessione al server TCP
Public Function GetStream()
 As NetworkStream
ottiene un flusso NetworkStream di lettura e scrittura verso il server. È questo flusso che consente gli scambi client-server.

9.3.7. La classe NetworkStream

La classe NetworkStream rappresenta il flusso di rete tra il client e il server. La classe è definita come segue:

Image

La classe NetworkStream 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. Quando due macchine comunicano, a ciascuna estremità del collegamento è presente un oggetto TcpClient. Il metodo GetStream di questo oggetto consente di accedere al flusso di rete (NetworkStream) che collega le due macchine. Pertanto, se una macchina M1 ha stabilito una connessione con una macchina M2 tramite un oggetto TcpClient client1 e le due macchine si scambiano righe di testo, potrà creare i propri flussi di lettura e scrittura nel modo seguente:

Dim in1 as StreamReader=new StreamReader(client1.GetStream())
Dim out1 as StreamWriter=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, ne attende una risposta. Tale risposta non arriverà mai se la riga è stata in realtà memorizzata nel buffer della macchina M1 e non è mai stata inviata. Per inviare una riga di testo alla macchina M2, si scriverà:

client1.WriteLine("un texte")

Per leggere la risposta da M2, si scriverà:

Dim réponse as String=client1.ReadLine()

9.3.8. Architettura di base di un client Internet

Ora disponiamo degli elementi necessari per definire l’architettura di base di un client Internet:


    Dim client As TcpClient = Nothing     ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing     ' le flux d'écriture du client
    Dim demande As String = Nothing         ' demande du client
    Dim réponse As String = Nothing         ' réponse du serveur

    Try
      ' ci si connette al servizio in esecuzione sulla porta P del computer M
      client = New TcpClient(nomServeur, port)

      ' si creano i flussi di ingresso-uscita del client TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' ciclo richiesta-risposta
      While True
        ' si prepara la richiesta
        demande = ...
        ' la si invia al server
        OUT.WriteLine(demande)
        ' si legge la risposta del server
        réponse = [IN].ReadLine()
        ' si elabora la risposta
        ...
            End While
            ' è tutto
            client.Close()
        Catch ex As Exception
      ' si gestisce l'eccezione
...
        End Try

9.3.9. La classe TcpListener

La classe TcpListener è la classe adatta a rappresentare un servizio TCP. È definita come segue:

Image

I costruttori, i metodi e le proprietà che ci interessano sono i seguenti:

Public Sub New(ByVal localaddr 
As IPAddress,ByVal port As Integer)
crea un servizio TCP che attenderà (listen) le richieste dei client su una porta passata come parametro (port), denominata porta di ascolto della macchina locale con indirizzo IP localadr.
Public Function AcceptTcpClient() 
As TcpClient
accetta la richiesta di un cliente. Restituisce come risultato un oggetto TcpClient associato a un'altra porta, denominata porta di servizio.
Public Sub Start()
avvia l’ascolto delle richieste dei clienti
Public Sub Stop()
interrompe l'ascolto delle richieste dei clienti

9.3.10. Architettura di base di un server Internet

Da quanto visto in precedenza, è possibile dedurre la struttura di base di un server:


    ' si crea il servizio di ascolto
    Dim ecoute As TcpListener = Nothing
    Dim port As Integer = ...
    Try
      ' si crea il servizio
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' si avvia
      ecoute.Start()
      ' ciclo di servizio
      Dim liaisonClient As TcpClient = Nothing
            While not fini
                ' in attesa di un client
                liaisonClient = ecoute.AcceptTcpClient()
                ' il servizio viene gestito da un'altra attività
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf [méthode]))
                tache.Start()
            End While
        Catch ex As Exception
            ' viene segnalato l'errore
....
        End Try
        ' fine del servizio
        ecoute.Stop()

La classe Service è un thread che potrebbe presentarsi come segue:


Public Class Service

    Private liaisonClient As TcpClient    ' liaison avec le client
    Private [IN] As StreamReader    ' flux d'entrée
    Private OUT As StreamWriter    ' flux de sortie

    ' costruttore
    Public Sub New(ByVal liaisonClient As TcpClient, ...)
        Me.liaisonClient = liaisonClient
        ...
    End Sub

    ' metodo run
    Public Sub Run()
        ' restituisce il servizio al client
        Try
            ' flusso in ingresso
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' flusso di uscita
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' ciclo di lettura richiesta/scrittura risposta
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' si elabora la richiesta
                ...
                ' si invia la risposta
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' richiesta successiva
                demande = [IN].ReadLine
            End While
            ' fine connessione
            liaisonClient.Close()
        Catch e As Exception
            ...
        End Try
        ' fine del servizio
    End Sub

9.4. Esempi

9.4.1. Server echo

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

serveurEcho port

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:


' opzioni
Option Explicit On 
Option Strict On

' spazi dei nomi
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

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

Public Class serveurEcho
  Private Shared syntaxe As String = "Syntaxe : serveurEcho port"

  ' programma principale
  Public Shared Sub Main(ByVal args() As String)

    ' c'è un argomento
    If args.Length <> 1 Then
      erreur(syntaxe, 1)
    End If
    ' questo argomento deve essere un numero intero >0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(0))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If
    ' si crea il servizio di ascolto
    Dim ecoute As TcpListener = Nothing
    Dim nbClients As Integer = 0 ' nbre de clients traités
    Try
      ' si crea il servizio
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' lo si avvia
      ecoute.Start()
      ' monitoraggio
      Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & port))
      Console.Out.WriteLine(ecoute.LocalEndpoint)

      ' ciclo del servizio
      Dim liaisonClient As TcpClient = Nothing
            While True
                ' ciclo infinito - verrà interrotto con Ctrl-C
                ' in attesa di un client
                liaisonClient = ecoute.AcceptTcpClient()

                ' il servizio è gestito da un altro processo
                nbClients += 1
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf New traiteClientEcho(liaisonClient, nbClients).Run))
                tache.Start()
            End While
            ' si torna ad ascoltare le richieste
        Catch ex As Exception
            ' viene segnalato l'errore
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
        ' fine del servizio
        ecoute.Stop()
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' arresto con errore
        Environment.Exit(exitCode)
    End Sub
End Class

' -------------------------------------------------------
' fornisce il servizio a un client del server di eco
Public Class traiteClientEcho

    Private liaisonClient As TcpClient    ' liaison avec le client
    Private numClient As Integer    ' n° de client
    Private [IN] As StreamReader    ' flux d'entrée
    Private OUT As StreamWriter    ' flux de sortie

    ' costruttore
    Public Sub New(ByVal liaisonClient As TcpClient, ByVal numClient As Integer)
        Me.liaisonClient = liaisonClient
        Me.numClient = numClient
    End Sub

    ' metodo run
    Public Sub Run()
        ' fornisce il servizio al client
        Console.Out.WriteLine(("Début de service au client " & numClient))
        Try
            ' flusso di input
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' flusso di uscita
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' ciclo di lettura richiesta/scrittura risposta
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' monitoraggio
                Console.Out.WriteLine(("Client " & numClient & " : " & demande))
                ' il servizio si interrompe quando il cliente invia un indicatore di fine file
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' il servizio si arresta quando il client invia "fine"
                If demande.Trim().ToLower() = "fin" Then
                    Exit While
                End If
                ' richiesta successiva
                demande = [IN].ReadLine
            End While
            ' fine connessione
            liaisonClient.Close()
        Catch e As Exception
            erreur("Erreur lors de la fermeture de la liaison client (" + e.ToString + ")", 2)
        End Try
        ' fine del servizio
        Console.Out.WriteLine(("Fin de service au client " & numClient))
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' arresto con errore
        Environment.Exit(exitCode)
    End Sub
End Class

La struttura del server è conforme all'architettura generale dei server TCP.

9.4.2. Un client per il server echo

Ora scriviamo un client per il server precedente. Verrà chiamato nel modo seguente:

clientEcho nomServeur port

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


' opzioni
Option Explicit On 
Option Strict On

' spazi dei nomi
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

Public Class clientEcho

  ' si connette a un server di eco
  ' ogni riga digitata sulla tastiera viene quindi ricevuta in eco
  Public Shared Sub Main(ByVal args() As String)
    ' sintassi
    Const syntaxe As String = "pg machine port"

    ' numero di argomenti
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' si annota il nome del server
    Dim nomServeur As String = args(0)

    ' la porta deve essere un numero intero maggiore di 0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(1))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If

        ' si può procedere
    Dim client As TcpClient = Nothing ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing ' le flux d'écriture du client
    Dim demande As String = Nothing ' demande du client
    Dim réponse As String = Nothing ' réponse du serveur
    Try
      ' ci si connette al servizio in esecuzione sulla porta P del computer M
      client = New TcpClient(nomServeur, port)

      ' si creano i flussi di ingresso-uscita del client TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True

      ' ciclo richiesta-risposta
      While True
        ' la richiesta proviene dalla tastiera
        Console.Out.Write("demande (fin pour arrêter) : ")
        demande = Console.In.ReadLine()
        ' la si invia al server
        OUT.WriteLine(demande)
        ' si legge la risposta del server
        réponse = [IN].ReadLine()
        ' si elabora la risposta
        Console.Out.WriteLine(("Réponse : " + réponse))
        ' finito?
        If demande.Trim().ToLower() = "fin" Then
          Exit While
                End If
            End While
            ' è finito
            client.Close()
        Catch ex As Exception
      ' si gestisce l'eccezione
      erreur(ex.Message, 3)
        End Try
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' arresto con errore
        Environment.Exit(exitCode)
    End Sub
End Class

La struttura di questo client è conforme all’architettura generale dei client tcp.Voici; i risultati ottenuti nella 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 1 si ottengono i seguenti risultati:

dos>clientEcho localhost 100
demande (fin pour arrêter) : ligne1
Réponse : [ligne1]
demande (fin pour arrêter) : ligne1B
Réponse : [ligne1B]
demande (fin pour arrêter) : ligne1C
Réponse : [ligne1C]
demande (fin pour arrêter) : fin
Réponse : [fin]

In quella del cliente 2:

dos>clientEcho localhost 100
demande (fin pour arrêter) : ligne2A
Réponse : [ligne2A]
demande (fin pour arrêter) : ligne2B
Réponse : [ligne2B]
demande (fin pour arrêter) : fin
Réponse : [fin]

In quella del server:

dos>serveurEcho 100
Serveur d'écho lancé sur le port 100
0.0.0.0:100
Début de service au client 1
Client 1 : ligne1
Début de service au client 2
Client 2 : ligne2A
Client 2 : ligne2B
Client 1 : ligne1B
Client 1 : ligne1C
Client 2 : fin
Fin de service au client 2
Client 1 : fin
Fin de service au client 1
^C

Si noti che il server è stato in grado di servire due client contemporaneamente.

9.4.3. Un client generico TCP

Molti servizi creati agli albori di Internet funzionano secondo il modello del server di eco esaminato in precedenza: gli scambi client-server avvengono tramite lo scambio di righe di testo. Scriveremo un client TCP generico che verrà avviato nel modo seguente: cltgen server porta

Questo client TCP si connetterà alla porta port del server serveur. Una volta fatto ciò, creerà due thread:

  1. un thread incaricato di leggere i comandi digitati sulla tastiera e di inviarli al server
  2. un thread incaricato di leggere le risposte del server e di visualizzarle sullo schermo

Perché due thread, visto che nell'applicazione precedente non se ne era sentita la necessità? In quest'ultima, 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. Creeremo quindi due cicli distinti:

  • un ciclo di lettura dei comandi digitati sulla tastiera da inviare al server. L’utente segnalerà la fine dei comandi con la parola chiave fin.
  • un ciclo di ricezione e visualizzazione delle risposte del server. Si tratterà di un ciclo infinito che verrà interrotto solo dalla chiusura del flusso di rete da parte del server o dall’utente tramite la tastiera, digitando il comando fin.

Per avere questi due cicli separati, abbiamo bisogno di due thread indipendenti. Vediamo un esempio di esecuzione in cui il nostro client TCP generico si connette a un servizio SMTP (SendMail Transfer Protocol). Questo servizio è responsabile dell’inoltro della posta elettronica ai destinatari. Funziona sulla porta 25 e utilizza un protocollo di comunicazione basato sullo scambio di righe di testo.

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

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

Commentiamo questi scambi client-server:

  • il servizio SMTP invia un messaggio di benvenuto quando un client si connette ad esso:
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
  • alcuni servizi dispongono di un comando help che fornisce indicazioni sui comandi utilizzabili con il servizio. In questo caso non è così. I comandi SMTP utilizzati nell'esempio sono i seguenti:
    • mail from: expéditeur, per indicare l’indirizzo e-mail del mittente del messaggio
    • rcpt to: destinataire, per indicare l’indirizzo e-mail del destinatario del messaggio. Se ci sono più destinatari, il comando rcpt to: va ripetuto tante volte quante necessarie per ciascuno di essi.
    • data, che segnala al server SMTP che si sta per inviare il messaggio. Come indicato nella risposta del server, il messaggio è costituito da una sequenza di righe terminata da una riga contenente il solo carattere punto. Un messaggio può avere delle intestazioni separate dal corpo del messaggio da una riga vuota. Nel nostro esempio, abbiamo inserito un oggetto con la parola chiave Subject:
  • una volta inviato il messaggio, è possibile indicare al server che si è terminato con il comando quit. Il server chiude quindi la connessione réseau.Le; il thread di lettura può rilevare questo evento e arrestarsi.
  • L’utente digita quindi «fin» sulla tastiera per arrestare anche il thread di lettura dei comandi digitati sulla tastiera.

Se controlliamo la posta in arrivo, troviamo quanto segue (Outlook):

Image

Si noti che il servizio SMTP non è in grado di rilevare se un mittente sia valido o meno. Pertanto, non ci si può mai fidare del campo from di un messaggio. In questo caso, il mittente machin@univ-angers.fr non esisteva. Questo client TCP generico ci permette di scoprire il protocollo di comunicazione dei servizi Internet e, partendo da lì, di creare classi specializzate per i client di tali servizi. Scopriamo il protocollo di comunicazione del servizio POP (Post Office Protocol), che consente di recuperare le proprie e-mail archiviate su un server. Funziona sulla porta 110.

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

I comandi principali sono i seguenti:

  • user login, dove si inserisce il proprio nome utente sul computer che ospita le nostre e-mail
  • pass password, dove si inserisce la password associata al login precedente
  • list, per ottenere l'elenco dei messaggi sotto forma di numero e dimensione in byte
  • retr i, per leggere il messaggio n. i
  • quit, per terminare la sessione.

Scopriamo ora il protocollo di comunicazione tra un client e un server Web che solitamente opera sulla porta 80:

dos>cltgen istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0

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

Un client Web invia i propri comandi al server secondo lo schema seguente:

commande1
commande2
...
commanden
[ligne vide]

Il server Web risponde solo dopo aver ricevuto la riga vuota. Nell'esempio abbiamo utilizzato un solo comando:

GET /index.html HTTP/1.0

che richiede al server il file URL /index.html e indica che sta operando con il protocollo HTTP versione 1.0. La versione più recente di questo protocollo è la 1.1. L'esempio mostra che il server ha risposto inviando il contenuto del file index.html e poi ha chiuso la connessione, poiché si vede che il thread di lettura delle risposte si è concluso. Prima di inviare il contenuto del file index.html, il server web ha inviato una serie di intestazioni terminata da una riga vuota:

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

La riga <html> è la prima riga del file /index.html. Quanto sopra è denominato intestazioni HTTP (HyperText Transfer Protocol). Non entreremo qui nei dettagli di queste intestazioni, ma ricordiamo che il nostro client generico ne consente l’accesso, il che può essere utile per comprenderle. Ad esempio, la prima riga:

<-- HTTP/1.1 200 OK

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

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

indicano al client che riceverà 11251 byte corrispondenti al testo HTML (HyperText Markup Language) e che, al termine dell'invio, la connessione verrà chiusa. Abbiamo quindi a disposizione un client TCP molto pratico. In realtà, questo client esiste già sui computer, dove si chiama telnet, ma era interessante scriverlo da soli. Il programma del client TCP generico è il seguente:


' spazi dei nomi
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

' la classe
Public Class clientTcpGénérique
   
    
   ' riceve come parametro le caratteristiche di un servizio nella forma
   ' server porta
   ' si connette al servizio
   ' crea un thread per leggere i comandi digitati sulla tastiera
   ' questi verranno inviati al server
   ' crea un thread per leggere le risposte del server
   ' questi saranno visualizzati sullo schermo
   ' il tutto termina con il comando «fin» digitato sulla tastiera
  Public Shared Sub Main(ByVal args() As String)

    ' sintassi
    Const syntaxe As String = "pg serveur port"

    ' numero di argomenti
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' si annota il nome del server
    Dim serveur As String = args(0)

    ' la porta deve essere un numero intero >0
    Dim port As Integer = 0
    Dim erreurPort As Boolean = False
    Dim E As Exception = Nothing
    Try
      port = Integer.Parse(args(1))
    Catch ex As Exception
      E = ex
      erreurPort = True
    End Try
    erreurPort = erreurPort Or port <= 0
    If erreurPort Then
      erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
    End If
    Dim client As TcpClient = Nothing
    ' potrebbero verificarsi dei problemi
    Try
      ' ci si connette al servizio
      client = New TcpClient(serveur, port)
    Catch ex As Exception
      ' errore
      Console.Error.WriteLine(("Impossible de se connecter au service (" & serveur & "," & port & "), erreur : " & ex.Message))
      ' fine
      Return
        End Try
        ' si creano i thread di lettura/scrittura
        Dim thReceive As New Thread(New ThreadStart(AddressOf New clientReceive(client).Run))
        Dim thSend As New Thread(New ThreadStart(AddressOf New clientSend(client).Run))

        ' si avvia l'esecuzione dei due thread
        thSend.Start()
        thReceive.Start()

        ' fine del thread principale
        Return
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' arresto con errore
        Environment.Exit(exitCode)
    End Sub
End Class

Public Class clientSend
    ' classe incaricata di leggere i comandi digitati sulla tastiera
    ' e di inviarli a un server tramite un client TCP passato al costruttore
    Private client As TcpClient    ' le client tcp

    ' costruttore
    Public Sub New(ByVal client As TcpClient)
        ' si specifica il client TCP
        Me.client = client
    End Sub

    ' metodo Run del thread
    Public Sub Run()

        ' dati locali
        Dim OUT As StreamWriter = Nothing        ' flux d'écriture réseau
        Dim commande As String = Nothing        ' commande lue au clavier
        ' gestione degli errori
        Try
            ' creazione del flusso di scrittura di rete
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' ciclo di immissione e invio dei comandi
            Console.Out.WriteLine("Commandes : ")
            While True
                ' lettura del comando digitato sulla tastiera
                commande = Console.In.ReadLine().Trim()
                ' Fine?
                If commande.ToLower() = "fin" Then
                    Exit While
                End If
                ' invio del comando al server
                OUT.WriteLine(commande)
            End While
        Catch ex As Exception
            ' errore
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fine - chiusura dei flussi
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' si segnala la fine del thread
        Console.Out.WriteLine("[fin du thread d'envoi des commandes au serveur]")
    End Sub
End Class


Public Class clientReceive
    ' classe incaricata di leggere le righe di testo destinate a un 
    ' cliente TCP passato al costruttore
    Private client As TcpClient    ' le client tcp

    ' costruttore
    Public Sub New(ByVal client As TcpClient)
        ' si registra il client TCP
        Me.client = client
    End Sub

    'costruttore
    ' metodo Run del thread
    Public Sub Run()

        ' dati locali
        Dim [IN] As StreamReader = Nothing        ' flux lecture réseau
        Dim réponse As String = Nothing        ' réponse serveur
        ' gestione degli errori
        Try
            ' creazione del flusso di lettura di rete
            [IN] = New StreamReader(client.GetStream())
            ' ciclo di lettura delle righe di testo dal flusso IN
            While True
                ' lettura flusso di rete
                réponse = [IN].ReadLine()
                ' flusso chiuso?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' visualizzazione
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' errore
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fine - si chiudono i flussi
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' si segnala la fine del thread
        Console.Out.WriteLine("[fin du thread de lecture des réponses du serveur]")
    End Sub
End Class

9.4.4. Un server TCP generico

Ora ci occupiamo di un server

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

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

  • un thread dedicato esclusivamente alla lettura delle righe di testo inviate dal cliente
  • un thread dedicato esclusivamente alla lettura delle risposte digitate dall’utente sulla tastiera. Quest’ultimo segnalerà, tramite il comando fin, che sta chiudendo la connessione con il client.

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

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

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

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

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

Le righe che iniziano con <-- sono quelle inviate dal client al server. Le righe N: sono quelle inviate dal server al client n. N. Il server sopra indicato è ancora attivo, mentre il client 1 ha terminato l'operazione. Si avvia un secondo client per lo stesso server:

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

La finestra del server appare quindi così:

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

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

dos>srvgen 88
Serveur générique lancé sur le port 88

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

Image

Diamo ora un’occhiata alla finestra del nostro server:

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

Si scoprono così le intestazioni HTTP inviate dal browser. Questo ci permette di scoprire poco a poco il protocollo HTTP. In un esempio precedente, avevamo creato un client Web che inviava solo il comando GET. Ciò era stato sufficiente. Qui vediamo che il browser invia altre informazioni al server. Il loro scopo è indicare al server quale tipo di client ha di fronte. Si nota inoltre che le intestazioni HTTP terminano con una riga vuota. Elaboriamo una risposta per il nostro client. L’utente alla tastiera è in questo caso il vero e proprio server e può elaborare una risposta manualmente. Ricordiamo la risposta fornita da un server Web in un esempio precedente:

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

Proviamo a dare una risposta analoga:

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

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

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

Non specifichiamo la dimensione del file che stiamo per inviare (Content-Length), ma ci limitiamo a indicare che chiuderemo la connessione (Connection: close) dopo averlo inviato. Questo è sufficiente per il browser. Quando rileva che la connessione è stata chiusa, il browser capirà che la risposta del server è terminata e visualizzerà la pagina HTML che gli è stata inviata. Quest’ultima è la seguente:

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

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

Image

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

Image

ovvero esattamente ciò che è stato inviato dal server generico. Il codice del server generico TCP è il seguente:


' spazi dei nomi
Imports System
Imports System.Net
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic

Public Class serveurTcpGénérique

    ' programma principale
    Public Shared Sub Main(ByVal args() As String)

        ' riceve la porta di ascolto delle richieste dei clienti
        ' crea un thread per leggere le richieste del client
        ' queste verranno visualizzate sullo schermo
        ' crea un thread per leggere i comandi digitati sulla tastiera
        ' questi saranno inviati come risposta al cliente
        ' il tutto termina con il comando «fine» digitato sulla tastiera

        Const syntaxe As String = "Syntaxe : pg port"

        ' c'è un argomento?
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' questo argomento deve essere un numero intero >0
        Dim port As Integer = 0
        Dim erreurPort As Boolean = False
        Dim E As Exception = Nothing
        Try
            port = Integer.Parse(args(0))
        Catch ex As Exception
            E = ex
            erreurPort = True
        End Try
        erreurPort = erreurPort Or port <= 0
        If erreurPort Then
            erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
        End If
        ' si crea il servizio di ascolto
        Dim ecoute As TcpListener = Nothing
        Dim nbClients As Integer = 0     ' nbre de clients traités
        Try
            ' si crea il servizio
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
            ' lo si avvia
            ecoute.Start()
            ' monitoraggio
            Console.Out.WriteLine(("Serveur générique lancé sur le port " & port))

            ' ciclo di servizio ai clienti
            Dim client As TcpClient = Nothing
            While True        ' boucle infinie - sera arrêtée par Ctrl-C
                ' in attesa di un cliente
                client = ecoute.AcceptTcpClient()

                ' il servizio è garantito da thread separati
                nbClients += 1
                ' thread di lettura delle richieste dei clienti
                Dim thReceive As New Thread(New ThreadStart(AddressOf New serveurReceive(client, nbClients).Run))
                ' thread di lettura delle risposte digitate dall'utente
                Dim thSend As New Thread(New ThreadStart(AddressOf New serveurSend(client, nbClients).Run))

                ' si avvia l'esecuzione dei due thread
                thSend.Start()
                thReceive.Start()
            End While
            ' si torna ad ascoltare le richieste
        Catch ex As Exception
            ' viene segnalato l'errore
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' arresto con errore
        Environment.Exit(exitCode)
    End Sub
End Class


Public Class serveurSend
    ' classe incaricata di leggere le risposte digitate sulla tastiera
    ' e di inviarle a un client tramite un client TCP passato al costruttore
    Private client As TcpClient    ' le client tcp
    Private numClient As Integer    ' n° de client

    ' costruttore
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' si registra il client TCP
        Me.client = client
        ' e il suo numero
        Me.numClient = numClient
    End Sub

    ' metodo Run del thread
    Public Sub Run()

        ' dati locali
        Dim OUT As StreamWriter = Nothing        ' flux d'écriture réseau
        Dim réponse As String = Nothing        ' réponse lue au clavier
        ' monitoraggio
        Console.Out.WriteLine(("Thread de lecture des réponses du serveur au client " & numClient & " lancé"))
        ' gestione degli errori
        Try
            ' creazione del flusso di scrittura di rete
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' ciclo di immissione e invio dei comandi
            While True
                ' identificazione cliente
                Console.Out.Write((numClient & " : "))
                ' lettura della risposta digitata sulla tastiera
                réponse = Console.In.ReadLine().Trim()
                ' finito?
                If réponse.ToLower() = "fin" Then
                    Exit While
                End If
                ' invio della risposta al server
                OUT.WriteLine(réponse)
            End While
            ' risposta successiva
        Catch ex As Exception
            ' errore
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fine - chiusura dei flussi
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' si segnala la fine del thread
        Console.Out.WriteLine(("[fin du Thread de lecture des réponses du serveur au client " & numClient & "]"))
    End Sub
End Class

Public Class serveurReceive
    ' classe incaricata di leggere le righe di testo inviate al server 
    ' tramite un client TCP passato al costruttore
    Private client As TcpClient     ' le client tcp
    Private numClient As Integer    ' n° de client

    ' costruttore
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' si nota il client TCP
        Me.client = client
        ' e il suo numero
        Me.numClient = numClient
    End Sub

    ' metodo Run del thread
    Public Sub Run()
        ' dati locali
        Dim [IN] As StreamReader = Nothing        ' flux lecture réseau
        Dim réponse As String = Nothing        ' réponse serveur
        ' monitoraggio
        Console.Out.WriteLine(("Thread de lecture des demandes du client " & numClient & " lancé"))
        ' gestione degli errori
        Try
            ' creazione del flusso di lettura di rete
            [IN] = New StreamReader(client.GetStream())
            ' ciclo di lettura delle righe di testo dal flusso IN
            While True
                ' lettura flusso di rete
                réponse = [IN].ReadLine()
                ' flusso chiuso?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' visualizzazione
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' errore
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' fine - si chiudono i flussi
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' si segnala la fine del thread
        Console.Out.WriteLine(("[fin du Thread de lecture des demandes du client " & numClient & "]"))
    End Sub
End Class

9.4.5. Un client Web

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

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

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

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • la prima intestazione indica quale pagina desideriamo
  • la seconda, quale server stiamo interrogando
  • il terzo che desideriamo che il server chiuda la connessione dopo averci risposto.

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

Il nostro client web verrà 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. Avviamo il server IIS e poi il client web sulla stessa macchina:

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

La risposta

HTTP/1.1 302 Object moved

significa che la pagina richiesta è stata spostata (quindi da URL). Il nuovo URL è indicato dall'intestazione Location:

Location: /IISSamples/Default/welcome.htm

Se utilizziamo GET al posto di HEAD nella chiamata al client Web:

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

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

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


' spazi dei nomi
Imports System
Imports System.Net.Sockets
Imports System.IO


Public Class clientWeb1

    ' richiede un URL
    ' visualizza il contenuto di quest'ultima sullo schermo
    Public Shared Sub Main(ByVal args() As String)
        ' sintassi
        Const syntaxe As String = "pg URI GET/HEAD"

        ' numero di argomenti
        If args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' si prende nota dell'URI richiesto
        Dim URIstring As String = args(0)
        Dim commande As String = args(1).ToUpper()

        ' verifica della validità di URI
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI non corretto
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try
        ' verifica dell'ordine
        If commande <> "GET" And commande <> "HEAD" Then
            ' ordine non corretto
            erreur("Le second paramètre doit être GET ou HEAD", 3)
        End If

        ' si può procedere
        Dim client As TcpClient = Nothing        ' le client
        Dim [IN] As StreamReader = Nothing        ' le flux de lecture du client
        Dim OUT As StreamWriter = Nothing        ' le flux d'écriture du client
        Dim réponse As String = Nothing        ' réponse du serveur
        Try
            ' ci si connette al server
            client = New TcpClient(uri.Host, uri.Port)

            ' si creano i flussi di entrata-uscita del cliente TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True

            ' si richiede l'URL - invio delle intestazioni HTTP
            OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
            OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
            OUT.WriteLine("Connection: close")
            OUT.WriteLine()
            ' si legge la risposta
            réponse = [IN].ReadLine()
            While Not (réponse Is Nothing)
                ' si elabora la risposta
                Console.Out.WriteLine(réponse)
                ' si legge la risposta
                réponse = [IN].ReadLine()
            End While
            ' operazione completata
            client.Close()
        Catch e As Exception
            ' gestione dell'eccezione
            erreur(e.Message, 4)
        End Try
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' arresto con errore
        Environment.Exit(exitCode)
    End Sub
End Class

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. Viene creato un oggetto Uri a partire dalla stringa URIstring ricevuta come parametro:


        ' verifica della validità di URI
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI non corretto
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

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 tale Uri. Pertanto, se l’oggetto uri del codice precedente è stato costruito a partire dalla stringa http://serveur:port/cheminPageHTML?param1=val1;param2=val2;... si avrà:

uri.Host=serveur, uri.Port=port, uri.Path=cheminPageHTML, uri.Query=param1=val1;param2=val2;..., uri.pathAndQuery= cheminPageHTML?param1=val1;param2=val2;..., uri.Scheme=http.

9.4.6. Client Web che gestisce i reindirizzamenti

Il client Web precedente non gestisce un eventuale reindirizzamento dell'URL che ha richiesto. Il client successivo lo gestisce.

  1. Legge la prima riga delle intestazioni HTTP inviate dal server per verificare se vi sia presente la stringa «302 Object moved», che indica un reindirizzamento
  2. legge le intestazioni successive. Se c’è un reindirizzamento, cerca la riga «Location: url» che fornisce il nuovo URL della pagina richiesta e ne prende nota.
  3. visualizza il resto della risposta del server. Se è presente un reindirizzamento, i passaggi da 1 a 3 vengono ripetuti con il nuovo URL. Il programma non accetta più di un reindirizzamento. Questo limite è definito da una costante che può essere modificata.

Ecco un esempio:

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

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

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

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

<html>

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

Il programma è il seguente:


' spazi dei nomi
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

' classe client web
Public Class clientWeb
   
  ' richiede un URL e ne visualizza il contenuto sullo schermo
  Public Shared Sub Main(ByVal args() As String)
    ' sintassi
    Const syntaxe As String = "pg URI GET/HEAD"

    ' numero di argomenti
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' si prende nota dell'URI richiesto
    Dim URIstring As String = args(0)
    Dim commande As String = args(1).ToUpper()

    ' verifica della validità di URI
    Dim uri As Uri = Nothing
    Try
      uri = New Uri(URIstring)
    Catch ex As Exception
      ' URI non corretto
      erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
    End Try 'catch
    ' verifica dell'ordine
    If commande <> "GET" And commande <> "HEAD" Then
      ' ordine non corretto
      erreur("Le second paramètre doit être GET ou HEAD", 3)
    End If

    ' si può procedere
    Dim client As TcpClient = Nothing ' le client
    Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
    Dim OUT As StreamWriter = Nothing ' le flux d'écriture du client
    Dim réponse As String = Nothing ' réponse du serveur
    Const nbRedirsMax As Integer = 1 ' pas plus d'une redirection acceptée
    Dim nbRedirs As Integer = 0 ' nombre de redirections en cours
    Dim premièreLigne As String ' 1ère ligne de la réponse
    Dim redir As Boolean = False ' indique s'il y a redirection ou non
    Dim locationString As String = "" ' la chaîne URI d'une éventuelle redirection
    ' espressione regolare per trovare un URL di reindirizzamento
    Dim location As New Regex("^Location: (.+?)$") '

        ' gestione degli errori
    Try
      ' è possibile avere più URL da richiedere in caso di reindirizzamenti
      While nbRedirs <= nbRedirsMax
        ' si effettua la connessione al server
        client = New TcpClient(uri.Host, uri.Port)

        ' si creano i flussi di ingresso-uscita del client TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' si inviano le intestazioni HTTP per richiedere l'URL
        OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
        OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
        OUT.WriteLine("Connection: close")
        OUT.WriteLine()

        ' si legge la prima riga della risposta
        premièreLigne = [IN].ReadLine()
        ' output su schermo
        Console.Out.WriteLine(premièreLigne)

        ' reindirizzamento?
        If Regex.IsMatch(premièreLigne, "302 Object moved$") Then
          ' c'è un reindirizzamento
          redir = True
          nbRedirs += 1
                End If

                ' si leggono le intestazioni HTTP successive fino a trovare la riga vuota che segnala la fine delle intestazioni
                Dim locationFound As Boolean = False
                réponse = [IN].ReadLine()
                While réponse <> ""
                    ' si visualizza la risposta
                    Console.Out.WriteLine(réponse)
                    ' se c'è un reindirizzamento, si cerca l'intestazione Location
                    If redir And Not locationFound Then
                        ' si confronta la riga con l'espressione relazionale location
                        Dim résultat As Match = location.Match(réponse)
                        If résultat.Success Then
                            ' se è stata trovata, si annota il codice di reindirizzamento URL
                            locationString = résultat.Groups(1).Value
                            ' si registra che è stato trovato
                            locationFound = True
                        End If
                    End If
                    ' riga successiva
                    réponse = [IN].ReadLine()
                End While

                ' righe successive della risposta
                Console.Out.WriteLine(réponse)
                réponse = [IN].ReadLine()
                While Not (réponse Is Nothing)
                    ' visualizzare la risposta
                    Console.Out.WriteLine(réponse)
                    ' riga successiva
                    réponse = [IN].ReadLine()
                End While

                ' si chiude la connessione
                client.Close()
                ' Abbiamo finito?
                If Not locationFound Or nbRedirs > nbRedirsMax Then
                    Exit While
                End If

                ' è necessario effettuare un reindirizzamento - si crea la nuova URI
                URIstring = uri.Scheme + "://" & uri.Host & ":" & uri.Port & locationString
                uri = New Uri(URIstring)
                ' monitoraggio
                Console.Out.WriteLine((ControlChars.Lf + "<--Redirection vers l'URL " + URIstring + "-->" + ControlChars.Lf))
            End While
        Catch e As Exception
      ' gestiamo l'eccezione
      erreur(e.Message, 4)
        End Try
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' interruzione con errore
        Environment.Exit(exitCode)
    End Sub
End Class

9.4.7. Server di calcolo delle imposte

Riprendiamo l'esercizio IMPOTS già trattato in varie forme. Ricordiamo l'ultima versione. È stata creata una classe «imposta». I suoi attributi sono tre tabelle di numeri:


Public Class impôt
    ' i dati necessari per il calcolo dell'imposta
    ' provengono da una fonte esterna
    Private limites(), coeffR(), coeffN() as double

La classe dispone di due costruttori:

  • un costruttore a cui vengono passati i tre array di dati necessari per il calcolo dell'imposta
     // costruttore 1
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
         ' inizializza i tre array di limiti, coeffR, coeffN a partire
         ' dei parametri passati al costruttore
  • un generatore a cui viene passato il nome DSN da un database ODBC

    ' costruttore 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' inizializza i tre array di limiti, coeffR, coeffN, a partire da
        ' del contenuto della tabella Timpots del database ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN sono le tre colonne di questa tabella
        ' può generare un'eccezione

Era stato scritto un programma di test:

dos>vbc /r:impots.dll testimpots.vb

dos>test mysql-impots timpots limites coeffr coeffn
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22506 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 2 200000
impôt=33388 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 3 200000
impôt=16400 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 3 300000
impôt=50082 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 3 200000
impôt=22506 F

In questo caso, il programma di test e l'oggetto impôt si trovavano sulla stessa macchina. Intendiamo collocare il programma di test e l'oggetto impôt su macchine diverse. Avremo un’applicazione client-server in cui l’oggetto remoto impôt fungerà da server. La nuova classe si chiama ServeurImpots ed è derivata dalla classe impôt:


Public Class ServeurImpots
    Inherits impôt

    ' attributi
    Private portEcoute As Integer    ' le port d'écoute des demandes clients
    Private actif As Boolean    ' état du serveur

    ' costruttore
    Public Sub New(ByVal portEcoute As Integer, ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        MyBase.New(DSNimpots, Timpots, colLimites, colCoeffR, colCoeffN)
        ' si nota la porta di ascolto
        Me.portEcoute = portEcoute
        ' per ora inattivo
        actif = False
        ' crea e avvia un thread per la lettura dei comandi digitati sulla tastiera
        ' il server verrà gestito tramite questi comandi
        Dim threadLecture As Thread = New Thread(New ThreadStart(AddressOf admin))
        threadLecture.Start()
    End Sub

L’unico nuovo parametro nel costruttore è la porta di ascolto delle richieste dei client. Gli altri parametri vengono passati direttamente alla classe base impôt. Il server fiscale è controllato da comandi digitati sulla tastiera. Pertanto, si crea un thread per leggere tali comandi. Ce ne saranno due possibili: start per avviare il servizio, stop per arrestarlo definitivamente. Il metodo admin che gestisce questi comandi è il seguente:


    Public Sub admin()
        ' legge i comandi di amministrazione del server digitati dalla tastiera
        ' in un ciclo infinito
        Dim commande As String = Nothing
        While True
            ' prompt
            Console.Out.Write("Serveur d'impôts>")
            ' lettura comando
            commande = Console.In.ReadLine().Trim().ToLower()
            ' esecuzione comando
            If commande = "start" Then
                ' attivo?
                If actif Then
                    'errore
                    Console.Out.WriteLine("Le serveur est déjà actif")
                Else
                    ' si avvia il servizio di ascolto
                    Dim threadEcoute As Thread = New Thread(New ThreadStart(AddressOf ecoute))
                    threadEcoute.Start()
                End If
            Else
                If commande = "stop" Then
                    ' fine di tutti i thread di esecuzione
                    Environment.Exit(0)
                Else
                    ' errore
                    Console.Out.WriteLine("Commande incorrecte. Utilisez (start,stop)")
                End If
            End If
        End While
    End Sub

Se il comando digitato sulla tastiera è start, viene avviato un thread di ascolto delle richieste dei client. Se il comando digitato è stop, tutti i thread vengono arrestati. Il thread di ascolto esegue il metodo ecoute:


    Public Sub ecoute()
        ' thread di ascolto delle richieste dei client
        ' si crea il servizio di ascolto
        Dim ecoute As TcpListener = Nothing
        Try
            ' si crea il servizio
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), portEcoute)
            ' si avvia
            ecoute.Start()
            ' monitoraggio
            Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & portEcoute))

            ' ciclo di servizio
            Dim liaisonClient As TcpClient = Nothing
            While True            ' boucle infinie
                ' in attesa di un cliente
                liaisonClient = ecoute.AcceptTcpClient()
                ' il servizio è gestito da un'altra attività
                Dim threadClient As Thread = New Thread(New ThreadStart(AddressOf New traiteClientImpots(liaisonClient, Me).Run))
                threadClient.Start()
            End While
            ' si torna in ascolto delle richieste
        Catch ex As Exception
            ' si segnala l'errore
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' arresto con errore
        Environment.Exit(exitCode)
    End Sub

È presente un server TCP classico in ascolto sulla porta portEcoute. Le richieste dei client vengono elaborate dal metodo Run di un oggetto a cui vengono passati due parametri:

  1. l’oggetto TcpClient, che consentirà di raggiungere il client
  2. l’oggetto impôt this che fornirà l’accesso al metodo this.calculer per il calcolo dell’imposta.

' -------------------------------------------------------
' garantisce il servizio a un cliente del server fiscale
Public Class traiteClientImpots

    Private liaisonClient As TcpClient    ' liaison avec le client
    Private [IN] As StreamReader    ' flux d'entrée
    Private OUT As StreamWriter    ' flux de sortie
    Private objImpôt As impôt     ' objet Impôt

    ' costruttore
    Public Sub New(ByVal liaisonClient As TcpClient, ByVal objImpôt As impôt)
        Me.liaisonClient = liaisonClient
        Me.objImpôt = objImpôt
    End Sub

Il metodo Run elabora le richieste dei clienti. Queste possono presentarsi in due forme:

  1. calcolo coniugato (sì/no) nbEnfants salaireAnnuel
  2. calcoli finanziari

La forma 1 consente il calcolo di un'imposta, la forma 2 chiude la connessione client-server.


    ' metodo Run
    Public Sub Run()
        ' fornisce il servizio al client
        Try
            ' flusso in entrata
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' flusso di uscita
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' invio di un messaggio di benvenuto al cliente
            OUT.WriteLine("Bienvenue sur le serveur d'impôts")

            ' ciclo di lettura della richiesta/scrittura della risposta
            Dim demande As String = Nothing
            Dim champs As String() = Nothing            ' les éléments de la demande
            Dim commande As String = Nothing            ' la commande du client : calcul ou fincalculs
            demande = [IN].ReadLine()
            While Not (demande Is Nothing)
                ' scomposizione della richiesta in campi
                champs = Regex.Split(demande.Trim().ToLower(), "\s+")
                ' due richieste accettate: calcolo e fine calcolo
                commande = champs(0)
                Dim erreur As Boolean = False
                If commande <> "calcul" And commande <> "fincalculs" Then
                    ' errore del cliente
                    OUT.WriteLine("Commande incorrecte. Utilisez (calcul,fincalculs).")
                End If
                If commande = "calcul" Then
                    calculerImpôt(champs)
                End If
                If commande = "fincalculs" Then
                    ' messaggio di chiusura al cliente
                    OUT.WriteLine("Au revoir...")
                    ' liberazione delle risorse
                    Try
                        OUT.Close()
                        [IN].Close()
                        liaisonClient.Close()
                    Catch
                    End Try
                    ' fine
                    Return
                End If
                ' nuova richiesta
                demande = [IN].ReadLine()
            End While
        Catch e As Exception
            erreur("L'erreur suivante s'est produite (" + e.ToString + ")", 2)
        End Try
    End Sub

Il calcolo dell’imposta viene effettuato tramite il metodo calculerImpôt, che riceve come parametro la tabella dei campi della richiesta effettuata dal cliente. Viene verificata la validità della richiesta e, se del caso, l’imposta viene calcolata e restituita al cliente.


    ' calcolo delle imposte
    Public Sub calculerImpôt(ByVal champs() As String)
        ' elabora la richiesta: calcolo per coniugi nbEnfants salaireAnnuel
        ' scomposto in campi nella tabella dei campi
        Dim marié As String = Nothing
        Dim nbEnfants As Integer = 0
        Dim salaireAnnuel As Integer = 0

        ' validità degli argomenti
        Try
            ' sono necessari almeno 4 campi
            If champs.Length <> 4 Then
                Throw New Exception
            End If
            ' sposato
            marié = champs(1)
            If marié <> "o" And marié <> "n" Then
                Throw New Exception
            End If
            ' figli
            nbEnfants = Integer.Parse(champs(2))
            ' stipendio
            salaireAnnuel = Integer.Parse(champs(3))
        Catch
            OUT.WriteLine(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel")
            ' fine
            Exit Sub
        End Try
        ' è possibile calcolare l'imposta
        Dim impot As Long = objImpôt.calculer(marié = "o", nbEnfants, salaireAnnuel)
        ' si invia la risposta al cliente
        OUT.WriteLine(impot.ToString)
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' interruzione con errore
        Environment.Exit(exitCode)
    End Sub

Questa classe viene compilata da

dos>vbc /r:impots.dll /r:system.dll /t:library srvimpots.vb

dove impots.dll contiene il codice della classe impôt. Un programma di test potrebbe essere il seguente:


' spazi dei nomi
Imports System
Imports System.IO
Imports Microsoft.VisualBasic

Public Class testServeurImpots
    Public Shared syntaxe As String = "Syntaxe : pg port dsnImpots Timpots colLimites colCoeffR colCoeffN"

    ' programma principale
    Public Shared Sub Main(ByVal args() As String)

        ' sono necessari 6 argomenti
        If args.Length <> 6 Then
            erreur(syntaxe, 1)
        End If
        ' la porta deve essere un numero intero >0
        Dim port As Integer = 0
        Dim erreurPort As Boolean = False
        Dim E As Exception = Nothing
        Try
            port = Integer.Parse(args(0))
        Catch ex As Exception
            E = ex
            erreurPort = True
        End Try
        erreurPort = erreurPort Or port <= 0
        If erreurPort Then
            erreur(syntaxe + ControlChars.Lf + "Port incorrect (" + E.ToString + ")", 2)
        End If
        ' si crea il server delle imposte
        Try
            Dim srvimots As ServeurImpots = New ServeurImpots(port, args(1), args(2), args(3), args(4), args(5))
        Catch ex As Exception
            'errore
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
    End Sub

    ' visualizzazione degli errori
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualizzazione dell'errore
        System.Console.Error.WriteLine(msg)
        ' arresto con errore
        Environment.Exit(exitCode)
    End Sub
End Class

Al programma di test vengono passati i dati necessari per la creazione di un oggetto ServeurImpots e, a partire da lì, il programma crea tale oggetto. Questo programma di test viene compilato da:

dos>vbc /r:srvimpots.dll /r:impots.dll testimpots.vb

Ecco un primo test:

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn
Serveur d'impôts>Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

La riga

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn

crea un oggetto ServeurImpots che non è ancora in ascolto delle richieste dei client. È il comando start digitato dalla tastiera che avvia l'ascolto. Il comando stop arresta il server. Utilizziamo ora un client. Useremo il client generico creato in precedenza. Il server è avviato:

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn
Serveur d'impôts>Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124

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

dos> clttcpgenerique localhost 124Commandes :
<-- Bienvenue sur le serveur d'impôts

Si nota che il client ha correttamente ricevuto il messaggio di benvenuto dal server. Si inviano altri comandi:

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

Torniamo alla finestra del server per arrestarlo:

dos>testimpots 124 odbc-mysql-dbimpots impots limites coeffr coeffn
Serveur d'impôts>Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop