Skip to content

9. Programmazione TCP/IP

9.1. Informazioni generali

9.1.1. Protocolli Internet

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

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

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

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

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

  • origine del pacchetto
  • destinazione
  • lunghezza
  • tipo

9.1.2. Il modello OSI

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

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

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

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

I ruoli dei diversi livelli sono i seguenti:

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

9.1.3. Il modello TCP/IP

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

Livello fisico

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

Ethernet

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

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

Le caratteristiche principali della tecnologia Ethernet sono le seguenti:

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

Livello di rete

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

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

Livelli di trasporto/sessione

Questo livello include i seguenti protocolli:

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

Livelli applicativo/di presentazione/di sessione

Qui si trovano vari protocolli:

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

9.1.4. Come funzionano i protocolli Internet

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

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

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

9.1.5. Indirizzamento su Internet

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

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

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

Tutti gli indirizzi IP devono essere univoci. In Francia, l'INRIA è responsabile dell'assegnazione degli indirizzi IP. Infatti, questa organizzazione 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 memorizzato in un file specifico su ogni macchina collegata alla rete.

9.1.5.1. Classi di indirizzi IP

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

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

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

Classe A

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

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

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

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

Classe B

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

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

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

L'indirizzo di rete è di 2 byte (esattamente 14 bit), così come l'indirizzo del nodo. Possono quindi esistere 2¹⁴ reti di Classe B, ciascuna contenente fino a 2¹⁶ nodi.

Classe C

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

R1.R2.R3
è l'indirizzo di rete
N1
è l'indirizzo di una macchina su quella rete

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

L'indirizzo di rete occupa 3 byte (meno 3 bit) e l'indirizzo host occupa 1 byte. Possono quindi esserci 2²¹ reti di Classe C, ciascuna contenente fino a 256 host.

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

Indirizzi riservati

  • Alcuni indirizzi IP sono indirizzi di rete piuttosto che indirizzi di nodo all'interno della rete. Si tratta di quelli in cui l'indirizzo del nodo è impostato su 0. Pertanto, l'indirizzo 193.49.144.0 è l'indirizzo IP della rete della Facoltà di Scienze di Angers. Di conseguenza, nessun nodo in una rete può avere l'indirizzo zero.
  • Quando l'indirizzo del nodo in un indirizzo IP è composto interamente da 1, si tratta di un indirizzo di broadcast: questo indirizzo si riferisce a tutti i nodi della rete.
  • In una rete di Classe C, che teoricamente consente 2⁸ = 256 nodi, se rimuoviamo i due indirizzi proibiti, ci rimangono solo 254 indirizzi autorizzati.

9.1.5.2. Protocolli di conversione tra indirizzi Internet e indirizzi fisici

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

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

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

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

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

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

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

9.1.6. Il livello di rete, noto come livello del protocollo Internet (IP)

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

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

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

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

9.1.6.1. Routing

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

Routing diretto

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

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

Routing indiretto

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

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

Nel nostro esempio sopra:

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

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

9.1.6.2. Messaggi di errore e di controllo

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

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 di dati inaffidabile tra due punti, il che significa che la consegna corretta di un pacchetto alla sua destinazione non è garantita. L'applicazione, se lo desidera, può gestire questo aspetto autonomamente, ad esempio attendendo un riconoscimento dopo l'invio di un messaggio prima di inviare quello successivo.

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

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

9.1.7.2. Il protocollo TCP: Transmission Control Protocol

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

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

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

9.1.8. Il livello applicativo

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

TELNET

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

FTP: (File Transfer Protocol)

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

TFTP: (Trivial File Transfer Protocol)

Questo protocollo è una variante dell'FTP. Si basa sul protocollo UDP ed è meno sofisticato dell'FTP.

DNS: (Domain Name System)

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

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

Macchina DPX2/320 presso l'Università di Angers

Macchina Sun presso l'ISERPA ad Angers

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

La macchina DPX2/320 dell'Università di Angers è stata denominata Lagaffe, mentre un PC 486DX50 è stato denominato liny. Come si fa a fare riferimento a queste macchine dall'esterno? Specificando la gerarchia dei domini a cui appartengono. Pertanto, il nome completo della macchina Lagaffe sarà:

Lagaffe.univ-Angers.fr

All'interno dei domini è possibile utilizzare nomi relativi. Pertanto, all'interno del dominio fr e all'esterno del dominio univ-Angers, è possibile fare riferimento alla macchina Lagaffe come

Lagaffe.univ-Angers

Infine, all'interno del dominio univ-Angers, è possibile fare riferimento ad essa semplicemente come

Lagaffe

Un'applicazione può quindi fare riferimento a una macchina tramite il suo nome. In definitiva, però, è comunque necessario ottenere l'indirizzo IP della macchina. Come si fa? Supponiamo di voler comunicare dalla macchina A alla macchina B.

  • Se la macchina B appartiene allo stesso dominio della macchina A, il suo indirizzo IP si troverà probabilmente in un file presente sulla macchina A.
  • Altrimenti, la macchina A troverà, in un altro file o nello stesso di prima, un elenco di diversi server dei nomi con i loro indirizzi IP. Un server dei nomi è responsabile della mappatura del nome di una macchina al suo indirizzo IP. La macchina A invierà una richiesta speciale al primo server dei nomi del suo elenco, nota come query DNS, che include il nome della macchina ricercata. Se il server interrogato ha quel nome nei propri record, invierà l'indirizzo IP corrispondente al computer A. Altrimenti, il server troverà nei propri file un elenco di server dei nomi che può interrogare. Lo farà quindi. In questo modo, verranno interrogati diversi server dei nomi, non a caso ma in modo da ridurre al minimo il numero di richieste. Se il computer viene finalmente trovato, la risposta verrà inviata al computer A.

XDR: (eXternal Data Representation)

Creato da Sun Microsystems, questo protocollo specifica una rappresentazione dei dati standard e indipendente dalla macchina.

RPC: (Remote Procedure Call)

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

NFS: Network File System

Definito anch'esso da Sun, questo protocollo permette a una macchina di "vedere" il file system di un'altra macchina. Si basa sul protocollo RPC menzionato sopra.

9.1.9. Conclusione

In questa introduzione abbiamo illustrato alcune delle caratteristiche principali dei protocolli Internet. Per approfondire l'argomento, i lettori possono consultare 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 su Internet è identificato in modo univoco da un indirizzo IP (Internet Protocol) nella forma I1.I2.I3.I4, dove I1 è un numero compreso tra 1 e 254. Può anche essere identificato da un nome univoco. Questo nome non è obbligatorio, poiché le applicazioni utilizzano sempre gli indirizzi IP dei computer. Questi nomi servono a semplificare la vita agli utenti. Pertanto, è più semplice utilizzare un browser per richiedere l'URL http://www.ibm.com piuttosto che l'URL http://129.42.17.99, sebbene entrambi i metodi siano possibili. La mappatura tra indirizzo IP e nome del computer è gestita da un servizio Internet distribuito denominato DNS (Domain Name System). La piattaforma .NET fornisce la classe Dns per la gestione degli indirizzi Internet:

Image

La maggior parte dei metodi della classe sono statici. Diamo un'occhiata a quelli che ci interessano:

Sovraccarichi Funzione pubblica condivisa
 GetHostByAddress(ByVal address As
 String) As IPHostEntry
Restituisce un oggetto IPHostEntry a partire da un indirizzo IP nella forma "I1.I2.I3.I4". Genera un'eccezione se l'indirizzo del computer non viene trovato.
Funzione pubblica condivisa
 GetHostByName(ByVal hostName As
 String) As IPHostEntry
restituisce un oggetto IPHostEntry a partire dal nome di un computer. Genera un'eccezione se il nome del computer non viene trovato.
Funzione pubblica condivisa
GetHostName() As String
restituisce il nome del computer su cui è in esecuzione il programma che esegue questa istruzione

Gli indirizzi di rete IPHostEntry hanno il seguente formato:

Le proprietà che ci interessano:

Proprietà pubblica AddressList
Come IPAddress ()
Elenco degli indirizzi IP di una macchina. Mentre un indirizzo IP identifica una e una sola macchina fisica, una macchina fisica può avere più indirizzi IP. Questo accade se dispone di più schede di rete che la collegano a reti diverse.
Proprietà pubblica Aliases
Come String ()
Elenco degli alias di una macchina, che può essere identificata da un nome principale e da alias
Proprietà pubblica HostName
Come stringa
Il nome del computer, se ne possiede uno

Dalla classe IPAddress, useremo il seguente costruttore, le proprietà e i metodi:

Image

Un oggetto [IPAddress] può essere convertito nella stringa I1.I2.I3.I4 utilizzando il metodo ToString(). Viceversa, è possibile ottenere un oggetto IPAddress dalla stringa I1.I2.I3.I4 utilizzando il metodo statico IPAddress.Parse("I1.I2.I3.I4"). Si consideri il seguente programma, che visualizza il nome del computer su cui è in esecuzione e fornisce in modo interattivo le mappature tra indirizzo IP e 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:


' options
Option Explicit On 
Option Strict On
 
' namespaces
Imports System
Imports System.Net
Imports System.Text.RegularExpressions
 
' test module
Public Module adresses
 
    Sub Main()
        ' displays the name of the local machine
        ' then interactively provides information on network machines
        ' identified by name or address IP
        ' local machine
        Dim localHost As String = Dns.GetHostName()
        Console.Out.WriteLine(("Machine Locale=" + localHost))
 
        ' interactive Q&A
        Dim machine As String
        Dim adresseMachine As IPHostEntry
        While True
            ' enter the name of the machine you are looking for
            Console.Out.Write("Machine recherchée (fin pour arrêter) : ")
            machine = Console.In.ReadLine().Trim().ToLower()
            ' finished?
            If machine = "fin" Then
                Exit While
            End If
 
            ' address I1.I2.I3.I4 or machine name?
            Dim isIPV4 As Boolean = Regex.IsMatch(machine, "^\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\s*$")
            ' management exception
            Try
                If isIPV4 Then
                    adresseMachine = Dns.GetHostByAddress(machine)
                Else
                    adresseMachine = Dns.GetHostByName(machine)
                End If
                ' the name
                Console.Out.WriteLine(("Machine : " + adresseMachine.HostName))
                ' IP addresses
                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()
                ' aliases
                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
                ' the machine doesn't exist
                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

Si consideri la comunicazione tra due macchine remote A e B:

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

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

Pertanto, le due applicazioni che comunicano devono concordare il tipo di protocollo di comunicazione che useranno. Ad esempio, la comunicazione con un servizio FTP non è la stessa di quella con un servizio POP: questi due servizi non accettano gli stessi comandi. Hanno protocolli di comunicazione diversi.

9.3.2. Caratteristiche del protocollo TCP

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

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

9.3.3. Il rapporto client-server

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

9.3.4. Architettura client

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

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

9.3.5. Architettura del server

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

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

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

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

9.3.6. La classe TcpClient

La classe TcpClient è la classe appropriata per rappresentare un client di servizio TCP. È definita come segue:

Image

I costruttori, i metodi e le proprietà di nostro interesse sono i seguenti:

Public Sub New(ByVal hostname
 As String, ByVal port As Integer)
crea una connessione TCP con il server in esecuzione sulla porta specificata (port) della macchina specificata (hostname). Ad esempio, new TcpClient("istia.univ-angers.fr", 80) per connettersi alla porta 80 della macchina istia.univ-angers.fr
Public Sub Close()
chiude la connessione al server TCP
Public Function GetStream()
 Come NetworkStream
Ottiene un NetworkStream per la lettura e la scrittura sul server. Questo flusso consente la comunicazione 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". Pertanto, è utile utilizzare gli oggetti StreamReader e StreamWriter per leggere e scrivere queste righe nel flusso di rete. Quando due macchine comunicano, è presente un oggetto TcpClient a ciascuna estremità della connessione. Il metodo GetStream di questo oggetto fornisce l'accesso al flusso di rete (NetworkStream) che collega le due macchine. Pertanto, se una macchina M1 ha stabilito una connessione con una macchina M2 utilizzando un oggetto TcpClient client1 e stanno scambiando righe di testo, può creare i propri flussi di lettura e scrittura come segue:

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 proveniente da client1 non passerà attraverso un buffer intermedio, ma andrà direttamente alla rete. Questo punto è importante. Generalmente, quando client1 invia una riga di testo al suo partner, si aspetta una risposta. Questa risposta non arriverà mai se la riga è stata effettivamente bufferizzata sulla macchina M1 e non è mai stata inviata. Per inviare una riga di testo alla macchina M2, scriviamo:

client1.WriteLine("un texte")

Per leggere la risposta da M2, scriviamo:

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

9.3.8. Architettura di base di un client Internet

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


    Dim client As TcpClient = Nothing     ' the customer
    Dim [IN] As StreamReader = Nothing ' the customer's reading flow
    Dim OUT As StreamWriter = Nothing     ' the customer's writing flow
    Dim demande As String = Nothing         ' customer request
    Dim réponse As String = Nothing         ' server response
 
    Try
      ' connect to the service running on port P of machine M
      client = New TcpClient(nomServeur, port)
 
      ' create customer input/output flows TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True
 
      ' request-response loop
      While True
        ' preparing the application
        demande = ...
        ' we send it to the server
        OUT.WriteLine(demande)
        ' we read the server response
        réponse = [IN].ReadLine()
        ' the answer is processed
        ...
            End While
            ' it's over
            client.Close()
        Catch ex As Exception
      ' we handle the exception
...
        End Try

9.3.9. La classe TcpListener

La classe TcpListener è la classe appropriata per rappresentare un servizio TCP. È definita come segue:

Image

I costruttori, i metodi e le proprietà di nostro interesse sono i seguenti:

Public Sub New(ByVal localaddr
As IPAddress, ByVal port As Integer)
crea un servizio TCP che ascolterà le richieste dei client su una porta passata come parametro (port), nota come porta di ascolto della macchina locale con indirizzo IP localaddr.
Public Function AccettaClienteTcp()
As TcpClient
accetta una richiesta del client. Restituisce un oggetto TcpClient associato a un'altra porta, denominata porta di servizio.
Sub Start()
Avvia l'ascolto delle richieste dei client
Public Sub Stop()
interrompe l'ascolto delle richieste dei client

9.3.10. Architettura di base di un server Internet

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


    ' we create the listening service
    Dim ecoute As TcpListener = Nothing
    Dim port As Integer = ...
    Try
      ' create the service
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' launch it
      ecoute.Start()
      ' service loop
      Dim liaisonClient As TcpClient = Nothing
            While not fini
                ' waiting for a customer
                liaisonClient = ecoute.AcceptTcpClient()
                ' the service is provided by another task
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf [méthode]))
                tache.Start()
            End While
        Catch ex As Exception
            ' we report the error
....
        End Try
        ' end of service
        ecoute.Stop()

La classe Service è un thread che potrebbe apparire così:


Public Class Service
 
    Private liaisonClient As TcpClient    ' customer liaison
    Private [IN] As StreamReader    ' iNPUTS
    Private OUT As StreamWriter    ' output flow
 
    ' manufacturer
    Public Sub New(ByVal liaisonClient As TcpClient, ...)
        Me.liaisonClient = liaisonClient
        ...
    End Sub
 
    ' run method
    Public Sub Run()
        ' renders service to the customer
        Try
            ' iNPUTS
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' output flow
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' loop read request/write response
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' we process demand
                ...
                ' we send the answer
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' following request
                demande = [IN].ReadLine
            End While
            ' end link
            liaisonClient.Close()
        Catch e As Exception
            ...
        End Try
        ' end of service
    End Sub

9.4. Esempi

9.4.1. Server Echo

Scriveremo un server echo che verrà avviato da una finestra DOS utilizzando il comando:

serveurEcho port

Il server funziona sulla porta passata come parametro. Si limita a rispedire al client la richiesta che il client gli ha inviato. Il programma è il seguente:


' options
Option Explicit On 
Option Strict On
 
' namespaces
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic
 
' call: serveurEcho port
' echo server
' returns the line sent to the customer
 
Public Class serveurEcho
  Private Shared syntaxe As String = "Syntaxe : serveurEcho port"
 
  ' main program
  Public Shared Sub Main(ByVal args() As String)
 
    ' is there an argument
    If args.Length <> 1 Then
      erreur(syntaxe, 1)
    End If
    ' this argument must be integer >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
    ' we create the listening service
    Dim ecoute As TcpListener = Nothing
    Dim nbClients As Integer = 0 ' of customers handled
    Try
      ' create the service
      ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
      ' launch it
      ecoute.Start()
      ' follow-up
      Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & port))
      Console.Out.WriteLine(ecoute.LocalEndpoint)
 
      ' service loop
      Dim liaisonClient As TcpClient = Nothing
            While True
                ' infinite loop - will be stopped by Ctrl-C
                ' waiting for a customer
                liaisonClient = ecoute.AcceptTcpClient()
 
                ' the service is provided by another task
                nbClients += 1
                Dim tache As Thread = New Thread(New ThreadStart(AddressOf New traiteClientEcho(liaisonClient, nbClients).Run))
                tache.Start()
            End While
            ' back to listening to requests
        Catch ex As Exception
            ' we report the error
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
        ' end of service
        ecoute.Stop()
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class
 
' -------------------------------------------------------
' provides service to an echo server client
Public Class traiteClientEcho
 
    Private liaisonClient As TcpClient    ' customer liaison
    Private numClient As Integer    ' customer no
    Private [IN] As StreamReader    ' iNPUTS
    Private OUT As StreamWriter    ' output flow
 
    ' manufacturer
    Public Sub New(ByVal liaisonClient As TcpClient, ByVal numClient As Integer)
        Me.liaisonClient = liaisonClient
        Me.numClient = numClient
    End Sub

    ' run method
    Public Sub Run()
        ' renders service to the customer
        Console.Out.WriteLine(("Début de service au client " & numClient))
        Try
            ' iNPUTS
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' output flow
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' loop read request/write response
            Dim demande As String = Nothing
            Dim reponse As String = Nothing
            demande = [IN].ReadLine
            While Not (demande Is Nothing)
                ' follow-up
                Console.Out.WriteLine(("Client " & numClient & " : " & demande))
                ' the service stops when the client sends an end-of-file marker
                reponse = "[" + demande + "]"
                OUT.WriteLine(reponse)
                ' service stops when client sends "end
                If demande.Trim().ToLower() = "fin" Then
                    Exit While
                End If
                ' following request
                demande = [IN].ReadLine
            End While
            ' end link
            liaisonClient.Close()
        Catch e As Exception
            erreur("Erreur lors de la fermeture de la liaison client (" + e.ToString + ")", 2)
        End Try
        ' end of service
        Console.Out.WriteLine(("Fin de service au client " & numClient))
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        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 scriveremo un client per il server precedente. Si chiamerà come segue:

clientEcho nomServeur port

Si connette alla macchina servername sulla porta port e poi invia righe di testo al server, che le ripete.


' options
Option Explicit On 
Option Strict On
 
' namespaces
Imports System.Net.Sockets
Imports System.Net
Imports System
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic
 
Public Class clientEcho
 
  ' connects to an echo server
  ' any line typed on the keyboard is then received as an echo
  Public Shared Sub Main(ByVal args() As String)
    ' syntax
    Const syntaxe As String = "pg machine port"
 
    ' number of arguments
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' note the server name
    Dim nomServeur As String = args(0)
 
    ' port must be integer >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
 
        ' we can work
    Dim client As TcpClient = Nothing ' the customer
    Dim [IN] As StreamReader = Nothing ' the customer's reading flow
    Dim OUT As StreamWriter = Nothing ' the customer's writing flow
    Dim demande As String = Nothing ' customer request
    Dim réponse As String = Nothing ' server response
    Try
      ' connect to the service running on port P of machine M
      client = New TcpClient(nomServeur, port)
 
      ' create customer input/output flows TCP
      [IN] = New StreamReader(client.GetStream())
      OUT = New StreamWriter(client.GetStream())
      OUT.AutoFlush = True
 
      ' request-response loop
      While True
        ' demand comes from the keyboard
        Console.Out.Write("demande (fin pour arrêter) : ")
        demande = Console.In.ReadLine()
        ' we send it to the server
        OUT.WriteLine(demande)
        ' we read the server response
        réponse = [IN].ReadLine()
        ' the answer is processed
        Console.Out.WriteLine(("Réponse : " + réponse))
        ' finished?
        If demande.Trim().ToLower() = "fin" Then
          Exit While
                End If
            End While
            ' it's over
            client.Close()
        Catch ex As Exception
      ' we handle the exception
      erreur(ex.Message, 3)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class

La struttura di questo client è conforme all'architettura generale dei client TCP. Di seguito sono riportati i risultati ottenuti con la seguente configurazione:

  • Il server è in esecuzione sulla porta 100 in una finestra DOS
  • Sulla stessa macchina, vengono avviati due client in altre due finestre DOS

Nella finestra del Client 1, otteniamo 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]

Nel client 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]

Sul lato 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 effettivamente in grado di servire due clienti contemporaneamente.

9.4.3. Un client TCP generico

Molti servizi creati agli albori di Internet funzionano secondo il modello del server echo discusso in precedenza: gli scambi client-server consistono nello scambio di righe di testo. Scriveremo un client TCP generico che verrà avviato come segue: cltgen server porta

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

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

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

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

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

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

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

dos>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 informazioni sui comandi disponibili per quel servizio. In questo caso non è così. I comandi SMTP utilizzati nell'esempio sono i seguenti:
    • mail from: mittente, per specificare l'indirizzo e-mail del mittente
    • rcpt to: destinatario, per specificare l'indirizzo e-mail del destinatario del messaggio. Se ci sono più destinatari, il comando rcpt to: viene ripetuto tante volte quante necessarie per ciascun destinatario.
    • data che segnala al server SMTP che sta per essere inviato un messaggio. Come indicato nella risposta del server, questi dati consistono in una serie di righe che terminano con una riga contenente solo un punto. Un messaggio può avere intestazioni separate dal corpo del messaggio da una riga vuota. Nel nostro esempio, abbiamo incluso un oggetto utilizzando la parola chiave Subject:
  • Una volta inviato il messaggio, è possibile comunicare al server che si è terminato utilizzando il comando quit. Il server chiude quindi la connessione di rete. Il thread di lettura può rilevare questo evento e arrestarsi.
  • L'utente digita quindi "end" sulla tastiera per arrestare anche il thread che legge i comandi digitati sulla tastiera.

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

Image

Si noti che il servizio SMTP non è in grado di rilevare se un mittente sia valido o meno. Pertanto, non ci si può mai fidare del campo "Da" di un messaggio. In questo caso, il mittente machin@univ-angers.fr non esisteva. Questo client TCP generico ci permette di scoprire il protocollo di comunicazione dei servizi Internet e, da lì, costruire classi specializzate per i client di questi servizi. Esploriamo il protocollo di comunicazione del servizio POP (Post Office Protocol), che consente agli utenti di recuperare le proprie e-mail memorizzate su un server. Opera 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 login sul server che ospita le e-mail
  • pass password, dove si inserisce la password associata al login precedente
  • list, per ottenere un elenco dei messaggi nel formato numero, dimensione in byte
  • retr i, per leggere il messaggio numero i
  • quit, per terminare la sessione.

Ora esploriamo il protocollo di comunicazione tra un client e un server web, che in genere funziona 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 il seguente schema:

commande1
commande2
...
commanden
[ligne vide]

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

GET /index.html HTTP/1.0

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

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

La riga <html> è la prima riga del file /index.html. Il testo che la precede è denominato "intestazioni HTTP" (HyperText Transfer Protocol). In questa sede non approfondiremo questi dettagli, ma è bene tenere presente che il nostro client generico consente di accedervi, il che può essere utile per comprenderli. Ad esempio, la prima riga:

<-- HTTP/1.1 200 OK

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

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

comunicano al client che riceverà 11.251 byte di testo HTML (HyperText Markup Language) e che la connessione verrà chiusa una volta inviati i dati. Ecco quindi un client TCP molto utile. In realtà, questo client esiste già su alcune macchine con il nome di telnet, ma è stato interessante scriverlo da soli. Il programma generico del client TCP è il seguente:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic
 
' the class
Public Class clientTcpGénérique
   
    
   ' receives the characteristics of a service as a parameter in the form
   ' server port
   ' connects to the service
   ' creates a thread to read keyboard commands
   ' these will be sent to the
   ' creates a thread to read server responses
   ' these will be displayed on the screen
   ' the whole thing ends with the command end typed on the keyboard
  Public Shared Sub Main(ByVal args() As String)
 
    ' syntax
    Const syntaxe As String = "pg serveur port"

    ' number of arguments
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' note the server name
    Dim serveur As String = args(0)
 
    ' port must be integer >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
    ' there may be problems
    Try
      ' connect to the service
      client = New TcpClient(serveur, port)
    Catch ex As Exception
      ' error
      Console.Error.WriteLine(("Impossible de se connecter au service (" & serveur & "," & port & "), erreur : " & ex.Message))
      ' end
      Return
        End Try
        ' create read/write threads
        Dim thReceive As New Thread(New ThreadStart(AddressOf New clientReceive(client).Run))
        Dim thSend As New Thread(New ThreadStart(AddressOf New clientSend(client).Run))
 
        ' start execution of both threads
        thSend.Start()
        thReceive.Start()
 
        ' end of main thread
        Return
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class
 
Public Class clientSend
    ' class for reading keyboard commands
    ' and send them to a server via a tcp client passed to the
    Private client As TcpClient    ' tcp client
 
    ' manufacturer
    Public Sub New(ByVal client As TcpClient)
        ' we note the tcp client
        Me.client = client
    End Sub
 
    ' thread Run method
    Public Sub Run()
 
        ' local data
        Dim OUT As StreamWriter = Nothing        ' network write streams
        Dim commande As String = Nothing        ' command read from keyboard
        ' error management
        Try
            ' create network write stream
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' order entry-send loop
            Console.Out.WriteLine("Commandes : ")
            While True
                ' read command typed on keyboard
                commande = Console.In.ReadLine().Trim()
                ' finished?
                If commande.ToLower() = "fin" Then
                    Exit While
                End If
                ' send order to server
                OUT.WriteLine(commande)
            End While
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' end - we close the feeds
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' signals the end of the thread
        Console.Out.WriteLine("[fin du thread d'envoi des commandes au serveur]")
    End Sub
End Class

 
Public Class clientReceive
    ' class responsible for reading lines of text intended for a 
    ' tcp client passed to builder
    Private client As TcpClient    ' tcp client
 
    ' manufacturer
    Public Sub New(ByVal client As TcpClient)
        ' we note the tcp client
        Me.client = client
    End Sub
 
    'manufacturer
    ' thread Run method
    Public Sub Run()
 
        ' local data
        Dim [IN] As StreamReader = Nothing        ' network read stream
        Dim réponse As String = Nothing        ' server response
        ' error management
        Try
            ' create network read stream
            [IN] = New StreamReader(client.GetStream())
            ' loop read text lines from IN stream
            While True
                ' network streaming
                réponse = [IN].ReadLine()
                ' closed flow?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' display
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' end - close flows
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' signals the end of the 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 vedremo un server

  • che visualizza sullo schermo i comandi inviati dai propri client
  • invia loro il testo digitato da un utente tramite la tastiera. L'utente funge quindi da server.

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

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

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

Il server è in esecuzione sulla porta 100 e usiamo il client generico per comunicare con esso. La finestra del client appare così:

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 provengono 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. Il server sopra indicato è ancora attivo anche se il client 1 ha terminato. Avviamo 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 ora appare 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

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

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

Ora apriamo un browser e inviamo una richiesta all'URL http://localhost:88/exemple.html. Il browser si connetterà quindi alla porta 88 del computer localhost e richiederà la pagina /example.html:

Image

Ora diamo 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
<--

Ecco come vediamo le intestazioni HTTP inviate dal browser. Questo ci permette di imparare gradualmente il protocollo HTTP. In un esempio precedente, abbiamo creato un client web che inviava solo una singola richiesta GET. Era sufficiente. Qui vediamo che il browser invia altre informazioni al server. Queste informazioni hanno lo scopo di comunicare al server con quale tipo di client ha a che fare. Vediamo anche che le intestazioni HTTP terminano con una riga vuota. Creiamo una risposta per il nostro client. L'utente alla tastiera è il vero e proprio server in questo caso e può creare una risposta manualmente. Ricordiamo la risposta inviata da un server web in un esempio precedente:

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

Proviamo a fornire una risposta simile:

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

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

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

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

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

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

Image

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

Image

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


' namespaces
Imports System
Imports System.Net
Imports System.Net.Sockets
Imports System.IO
Imports System.Threading
Imports Microsoft.VisualBasic
 
Public Class serveurTcpGénérique
 
    ' main program
    Public Shared Sub Main(ByVal args() As String)
 
        ' receives the port of listening to customer requests
        ' creates a thread to read client requests
        ' these will be displayed on the screen
        ' creates a thread to read keyboard commands
        ' these will be sent as a reply to the customer
        ' the whole thing ends with the command end typed on the keyboard
 
        Const syntaxe As String = "Syntaxe : pg port"
 
        ' is there an argument
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' this argument must be integer >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
        ' we create the listening service
        Dim ecoute As TcpListener = Nothing
        Dim nbClients As Integer = 0     ' of customers handled
        Try
            ' create the service
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), port)
            ' launch it
            ecoute.Start()
            ' follow-up
            Console.Out.WriteLine(("Serveur générique lancé sur le port " & port))
 
            ' customer service loop
            Dim client As TcpClient = Nothing
            While True        ' infinite loop - will be stopped by Ctrl-C
                ' waiting for a customer
                client = ecoute.AcceptTcpClient()
 
                ' the service is provided by separate threads
                nbClients += 1
                ' thread for reading customer requests
                Dim thReceive As New Thread(New ThreadStart(AddressOf New serveurReceive(client, nbClients).Run))
                ' thread for reading responses typed on the keyboard by the user
                Dim thSend As New Thread(New ThreadStart(AddressOf New serveurSend(client, nbClients).Run))
 
                ' start execution of both threads
                thSend.Start()
                thReceive.Start()
            End While
            ' back to listening to requests
        Catch ex As Exception
            ' we report the error
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class
 
 
Public Class serveurSend
    ' class responsible for reading typed responses
    ' and send them to a client via a tcp client passed to the
    Private client As TcpClient    ' tcp client
    Private numClient As Integer    ' customer no
 
    ' manufacturer
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' we note the tcp client
        Me.client = client
        ' and its
        Me.numClient = numClient
    End Sub
 
    ' thread Run method
    Public Sub Run()
 
        ' local data
        Dim OUT As StreamWriter = Nothing        ' network write streams
        Dim réponse As String = Nothing        ' answer read from keyboard
        ' follow-up
        Console.Out.WriteLine(("Thread de lecture des réponses du serveur au client " & numClient & " lancé"))
        ' error management
        Try
            ' network write stream creation
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
            ' order entry-send loop
            While True
                ' customer identification
                Console.Out.Write((numClient & " : "))
                ' read response typed on keyboard
                réponse = Console.In.ReadLine().Trim()
                ' finished?
                If réponse.ToLower() = "fin" Then
                    Exit While
                End If
                ' send response to server
                OUT.WriteLine(réponse)
            End While
            ' following response
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' end - close flows
        Try
            OUT.Close()
            client.Close()
        Catch
        End Try
        ' signals the end of the thread
        Console.Out.WriteLine(("[fin du Thread de lecture des réponses du serveur au client " & numClient & "]"))
    End Sub
End Class
 
Public Class serveurReceive
    ' class responsible for reading text lines sent to the server 
    ' via a tcp client passed to the builder
    Private client As TcpClient     ' tcp client
    Private numClient As Integer    ' customer no
 
    ' manufacturer
    Public Sub New(ByVal client As TcpClient, ByVal numClient As Integer)
        ' we note the tcp client
        Me.client = client
        ' and its
        Me.numClient = numClient
    End Sub
 
    ' thread Run method
    Public Sub Run()
        ' local data
        Dim [IN] As StreamReader = Nothing        ' network read stream
        Dim réponse As String = Nothing        ' server response
        ' follow-up
        Console.Out.WriteLine(("Thread de lecture des demandes du client " & numClient & " lancé"))
        ' error management
        Try
            ' create network read stream
            [IN] = New StreamReader(client.GetStream())
            ' loop read text lines from IN stream
            While True
                ' network streaming
                réponse = [IN].ReadLine()
                ' closed flow?
                If réponse Is Nothing Then
                    Exit While
                End If
                ' display
                Console.Out.WriteLine(("<-- " + réponse))
            End While
        Catch ex As Exception
            ' error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
        ' end - close flows
        Try
            [IN].Close()
            client.Close()
        Catch
        End Try
        ' signals the end of the 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 che accetta un URL come parametro e visualizza sullo schermo il testo inviato dal server. Supporremo che il server supporti il protocollo HTTP 1.1. Tra le intestazioni elencate sopra, useremo solo le seguenti:

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

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

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

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

La risposta

HTTP/1.1 302 Object moved

indica che la pagina richiesta è stata spostata (e quindi ha un nuovo URL). Il nuovo URL è fornito dall'intestazione Location:

Location: /IISSamples/Default/welcome.htm

Se utilizziamo GET invece 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, più il corpo della pagina HTML. Il programma è il seguente:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
 
 
Public Class clientWeb1
 
    ' requests a URL
    ' displays its contents on the screen
    Public Shared Sub Main(ByVal args() As String)
        ' syntax
        Const syntaxe As String = "pg URI GET/HEAD"
 
        ' number of arguments
        If args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' note the URI required
        Dim URIstring As String = args(0)
        Dim commande As String = args(1).ToUpper()
 
        ' URI validity check
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try
        ' order verification
        If commande <> "GET" And commande <> "HEAD" Then
            ' incorrect order
            erreur("Le second paramètre doit être GET ou HEAD", 3)
        End If
 
        ' we can work
        Dim client As TcpClient = Nothing        ' the customer
        Dim [IN] As StreamReader = Nothing        ' the customer's reading flow
        Dim OUT As StreamWriter = Nothing        ' the customer's writing flow
        Dim réponse As String = Nothing        ' server response
        Try
            ' connect to the server
            client = New TcpClient(uri.Host, uri.Port)
 
            ' create customer input/output flows TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
 
            ' request URL - send HTTP headers
            OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
            OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
            OUT.WriteLine("Connection: close")
            OUT.WriteLine()
            ' we read the answer
            réponse = [IN].ReadLine()
            While Not (réponse Is Nothing)
                ' the answer is processed
                Console.Out.WriteLine(réponse)
                ' we read the answer
                réponse = [IN].ReadLine()
            End While
            ' it's over
            client.Close()
        Catch e As Exception
            ' we handle the exception
            erreur(e.Message, 4)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class

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


        ' URI validity check
        Dim uri As Uri = Nothing
        Try
            uri = New Uri(URIstring)
        Catch ex As Exception
            ' URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

Se la stringa URI ricevuta come parametro non è un URI valido (protocollo mancante, server, ecc.), viene generata un'eccezione. Questo ci permette di verificare la validità del parametro ricevuto. Una volta costruito l'oggetto URI, abbiamo accesso ai suoi vari elementi. Pertanto, se l'oggetto URI del codice precedente è stato costruito dalla stringa http://serveur:port/cheminPageHTML?param1=val1;param2=val2;..., avremo:

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

9.4.6. Gestione dei reindirizzamenti da parte del client web

Il client web precedente non gestisce eventuali reindirizzamenti dell'URL richiesto. Il client seguente invece li gestisce.

  1. Legge la prima riga delle intestazioni HTTP inviate dal server per verificare se contiene la stringa "302 Object moved", che indica un reindirizzamento
  2. Legge le intestazioni seguenti. Se è presente un reindirizzamento, cerca la riga "Location: url" che fornisce il nuovo URL della pagina richiesta e lo registra.
  3. Visualizza il resto della risposta del server. Se c'è un reindirizzamento, i passaggi da 1 a 3 vengono ripetuti con il nuovo URL. Il programma non accetta più di un reindirizzamento. Questo limite è definito da una costante che può essere modificata.

Ecco un esempio:

dos>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:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic
 
' web client class
Public Class clientWeb
   
  ' requests a URL and displays its contents on the screen
  Public Shared Sub Main(ByVal args() As String)
    ' syntax
    Const syntaxe As String = "pg URI GET/HEAD"
 
    ' number of arguments
    If args.Length <> 2 Then
      erreur(syntaxe, 1)
    End If
    ' note the URI required
    Dim URIstring As String = args(0)
    Dim commande As String = args(1).ToUpper()
 
    ' URI validity check
    Dim uri As Uri = Nothing
    Try
      uri = New Uri(URIstring)
    Catch ex As Exception
      ' URI incorrect
      erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
    End Try 'catch
    ' order verification
    If commande <> "GET" And commande <> "HEAD" Then
      ' incorrect order
      erreur("Le second paramètre doit être GET ou HEAD", 3)
    End If
 
    ' we can work
    Dim client As TcpClient = Nothing ' the customer
    Dim [IN] As StreamReader = Nothing ' the customer's reading flow
    Dim OUT As StreamWriter = Nothing ' the customer's writing flow
    Dim réponse As String = Nothing ' server response
    Const nbRedirsMax As Integer = 1 ' no more than one redirection accepted
    Dim nbRedirs As Integer = 0 ' number of redirects in progress
    Dim premièreLigne As String ' 1st line of the answer
    Dim redir As Boolean = False ' indicates redirection or not
    Dim locationString As String = "" ' the URI string of a possible redirection
    ' regular expression to find a URL redirect
    Dim location As New Regex("^Location: (.+?)$") '
 
        ' error management
    Try
      ' you may have several URL to request if there are redirections
      While nbRedirs <= nbRedirsMax
        ' connect to the server
        client = New TcpClient(uri.Host, uri.Port)
 
        ' create customer input/output flows TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True
 
        ' we send HTTP headers to request URL
        OUT.WriteLine((commande + " " + uri.PathAndQuery + " HTTP/1.1"))
        OUT.WriteLine(("Host: " + uri.Host + ":" & uri.Port))
        OUT.WriteLine("Connection: close")
        OUT.WriteLine()
 
        ' read the first line of the answer
        premièreLigne = [IN].ReadLine()
        ' screen echo
        Console.Out.WriteLine(premièreLigne)
 
        ' redirection?
        If Regex.IsMatch(premièreLigne, "302 Object moved$") Then
          ' there is a redirection
          redir = True
          nbRedirs += 1
                End If
 
                ' next HTTP headers until you find the empty line signalling the end of the headers
                Dim locationFound As Boolean = False
                réponse = [IN].ReadLine()
                While réponse <> ""
                    ' the answer is displayed
                    Console.Out.WriteLine(réponse)
                    ' if there is a redirection, we search for the Location header
                    If redir And Not locationFound Then
                        ' compare the line with the relational expression location
                        Dim résultat As Match = location.Match(réponse)
                        If résultat.Success Then
                            ' if found, note the URL of redirection
                            locationString = résultat.Groups(1).Value
                            ' we note that we found
                            locationFound = True
                        End If
                    End If
                    ' next line
                    réponse = [IN].ReadLine()
                End While
 
                ' following lines of the answer
                Console.Out.WriteLine(réponse)
                réponse = [IN].ReadLine()
                While Not (réponse Is Nothing)
                    ' the answer is displayed
                    Console.Out.WriteLine(réponse)
                    ' next line
                    réponse = [IN].ReadLine()
                End While
 
                ' close the connection
                client.Close()
                ' are we done?
                If Not locationFound Or nbRedirs > nbRedirsMax Then
                    Exit While
                End If
 
                ' there is a redirection to be made - we build the new Uri
                URIstring = uri.Scheme + "://" & uri.Host & ":" & uri.Port & locationString
                uri = New Uri(URIstring)
                ' follow-up
                Console.Out.WriteLine((ControlChars.Lf + "<--Redirection vers l'URL " + URIstring + "-->" + ControlChars.Lf))
            End While
        Catch e As Exception
      ' we handle the exception
      erreur(e.Message, 4)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class

9.4.7. Server di calcolo delle imposte

Stiamo riesaminando l'esercizio TAXES, che è già stato trattato in varie forme. Rivediamo l'ultima versione. È stata creata una classe tax. I suoi attributi sono tre array di numeri:


Public Class impôt
    ' les données nécessaires au calcul de l'impôt
    ' proviennent d'une source extérieure
    Private limites(), coeffR(), coeffN() as double

La classe ha due costruttori:

  • un costruttore che accetta i tre array di dati necessari per calcolare l'imposta
    // constructeur 1
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' initializes the three limit arrays, coeffR, coeffN from
        ' parameters passed to the constructor
  • un costruttore a cui passiamo il nome DSN di un database ODBC
    ' builder 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception

È 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 fiscale si trovavano sulla stessa macchina. Proponiamo di collocare il programma di test e l'oggetto fiscale su macchine diverse. Avremo un'applicazione client-server in cui l'oggetto fiscale remoto fungerà da server. La nuova classe si chiama TaxServer ed è derivata dalla classe tax:


Public Class ServeurImpots
    Inherits impôt
 
    ' attributes
    Private portEcoute As Integer    ' the ability to listen to customer requests
    Private actif As Boolean    ' server status
 
    ' manufacturer
    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)
        ' we note the listening port
        Me.portEcoute = portEcoute
        ' currently inactive
        actif = False
        ' creates and launches a thread for reading keyboard commands
        ' the server will be managed using these commands
        Dim threadLecture As Thread = New Thread(New ThreadStart(AddressOf admin))
        threadLecture.Start()
    End Sub

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


    Public Sub admin()
        ' reads server administration commands typed from the keyboard
        ' in an endless loop
        Dim commande As String = Nothing
        While True
            ' invite
            Console.Out.Write("Serveur d'impôts>")
            ' read command
            commande = Console.In.ReadLine().Trim().ToLower()
            ' order execution
            If commande = "start" Then
                ' active?
                If actif Then
                    'error
                    Console.Out.WriteLine("Le serveur est déjà actif")
                Else
                    ' we launch the listening service
                    Dim threadEcoute As Thread = New Thread(New ThreadStart(AddressOf ecoute))
                    threadEcoute.Start()
                End If
            Else
                If commande = "stop" Then
                    ' end of all execution threads
                    Environment.Exit(0)
                Else
                    ' error
                    Console.Out.WriteLine("Commande incorrecte. Utilisez (start,stop)")
                End If
            End If
        End While
    End Sub

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


    Public Sub ecoute()
        ' thread for listening to customer requests
        ' we create the listening service
        Dim ecoute As TcpListener = Nothing
        Try
            ' create the service
            ecoute = New TcpListener(IPAddress.Parse("127.0.0.1"), portEcoute)
            ' launch it
            ecoute.Start()
            ' follow-up
            Console.Out.WriteLine(("Serveur d'écho lancé sur le port " & portEcoute))
 
            ' service loop
            Dim liaisonClient As TcpClient = Nothing
            While True            ' infinite loop
                ' waiting for a customer
                liaisonClient = ecoute.AcceptTcpClient()
                ' the service is provided by another task
                Dim threadClient As Thread = New Thread(New ThreadStart(AddressOf New traiteClientImpots(liaisonClient, Me).Run))
                threadClient.Start()
            End While
            ' back to listening to requests
        Catch ex As Exception
            ' we report the error
            erreur("L'erreur suivante s'est produite : " + ex.Message, 3)
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub

Qui abbiamo un classico server TCP in ascolto sulla porta portEcoute. Le richieste del client vengono gestite dal metodo Run di un oggetto a cui vengono passati due parametri:

  1. l'oggetto TcpClient, che consente l'accesso al client
  2. l'oggetto tax this, che fornisce l'accesso al metodo di calcolo delle imposte this.calculate.

' -------------------------------------------------------
' provides service to a tax server client
Public Class traiteClientImpots
 
    Private liaisonClient As TcpClient    ' customer liaison
    Private [IN] As StreamReader    ' iNPUTS
    Private OUT As StreamWriter    ' output flow
    Private objImpôt As impôt     ' object Tax
 
    ' manufacturer
    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 assumere due forme:

  1. calcolaConiugi(sì/no) n.Figli stipendioAnnuale
  2. endCalculations

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


    ' run method
    Public Sub Run()
        ' renders service to the customer
        Try
            ' iNPUTS
            [IN] = New StreamReader(liaisonClient.GetStream())
            ' output flow
            OUT = New StreamWriter(liaisonClient.GetStream())
            OUT.AutoFlush = True
            ' send a welcome message to the customer
            OUT.WriteLine("Bienvenue sur le serveur d'impôts")
 
            ' loop read request/write response
            Dim demande As String = Nothing
            Dim champs As String() = Nothing            ' elements of the request
            Dim commande As String = Nothing            ' customer order: calculation or fincalculs
            demande = [IN].ReadLine()
            While Not (demande Is Nothing)
                ' demand is broken down into fields
                champs = Regex.Split(demande.Trim().ToLower(), "\s+")
                ' two successful applications: calcul and fincalculs
                commande = champs(0)
                Dim erreur As Boolean = False
                If commande <> "calcul" And commande <> "fincalculs" Then
                    ' customer error
                    OUT.WriteLine("Commande incorrecte. Utilisez (calcul,fincalculs).")
                End If
                If commande = "calcul" Then
                    calculerImpôt(champs)
                End If
                If commande = "fincalculs" Then
                    ' good-bye message to customer
                    OUT.WriteLine("Au revoir...")
                    ' freeing up resources
                    Try
                        OUT.Close()
                        [IN].Close()
                        liaisonClient.Close()
                    Catch
                    End Try
                    ' end
                    Return
                End If
                ' new request
                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 eseguito dal metodo CalculateTax, che accetta come parametro l'array di campi della richiesta del cliente. Viene verificata la validità della richiesta e, se valida, l'imposta viene calcolata e restituita al cliente.


    ' tax calculation
    Public Sub calculerImpôt(ByVal champs() As String)
        ' processing the application: calculation married nbEnfants salaireAnnuel
        ' broken down into fields in the fields table
        Dim marié As String = Nothing
        Dim nbEnfants As Integer = 0
        Dim salaireAnnuel As Integer = 0
 
        ' validity of arguments
        Try
            ' at least 4 fields are required
            If champs.Length <> 4 Then
                Throw New Exception
            End If
            ' married
            marié = champs(1)
            If marié <> "o" And marié <> "n" Then
                Throw New Exception
            End If
            ' children
            nbEnfants = Integer.Parse(champs(2))
            ' salary
            salaireAnnuel = Integer.Parse(champs(3))
        Catch
            OUT.WriteLine(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel")
            ' finish
            Exit Sub
        End Try
        ' tax can be calculated
        Dim impot As Long = objImpôt.calculer(marié = "o", nbEnfants, salaireAnnuel)
        ' we send the response to the client
        OUT.WriteLine(impot.ToString)
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub

Questa classe è compilata da

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

dove impots.dll contiene il codice per la classe impôt. Un programma di prova potrebbe apparire così:


' namespaces
Imports System
Imports System.IO
Imports Microsoft.VisualBasic
 
Public Class testServeurImpots
    Public Shared syntaxe As String = "Syntaxe : pg port dsnImpots Timpots colLimites colCoeffR colCoeffN"
 
    ' main program
    Public Shared Sub Main(ByVal args() As String)
 
        ' you need6 arguments
        If args.Length <> 6 Then
            erreur(syntaxe, 1)
        End If
        ' port must be integer >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
        ' we create the tax server
        Try
            Dim srvimots As ServeurImpots = New ServeurImpots(port, args(1), args(2), args(3), args(4), args(5))
        Catch ex As Exception
            'error
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
        End Try
    End Sub
 
    ' error display
    Public Shared Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Class

Passiamo i dati necessari per costruire un oggetto ServeurImpots al programma di test, che provvederà a creare tale oggetto. Questo programma di test viene compilato da:

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

Ecco un test iniziale:

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 TaxServer che non è ancora in ascolto delle richieste dei client. È il comando start digitato sulla tastiera che avvia questo processo di ascolto. Il comando stop arresta il server. Utilizziamo ora un client. Useremo il client generico creato in precedenza. Il server viene 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

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

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

Torniamo alla finestra del server per interromperlo:

dos>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