Due computer collegati tramite una rete locale possono comunicare tra loro nella modalità Client/Server o nella modalità peer to peer.
I computer sono dotati di una scheda di rete che si collega alla LAN. I programmi per affidare alla scheda di rete i messaggi da trasferire alla rete, preparano una richiesta e la inviano al socket.
Con il termine socket si identifica lo zoccolo in cui si innesta una spina o i piedini di un dispositivo. Nel contesto della comunicazione su rete, il socket rappresenta un connettore software, quindi ideale, che si immagina che sia disponibile sulla scheda di rete e al quale si connette il programma che deve comunicare.
Il programma Server riceve i messaggi, li esamina e prepara una risposta.
Affinchè i due sistemi, mittente e destinatario, possano comunicare appoggiandosi sul protocollo TCP devono stabilire una connessione:
Il client invia un pacchetto di tipo Request con la richiesta di apertura connessione (CALL_REQUEST)
la rete smista il pacchetto e lo consegna al destinatario (CALL_INDICATION)
il server, se è disponibile, invia un pacchetto di tipo Response per accettare la connessione
il pacchetto viaggia attraverso la rete locale e viene consegnato al mittente (CALL_ACCEPTED)
A questo punto la connessione è stata stabilita e i pacchetti contenenti i dati da trasferire tra i due interlocutori transiteranno sulla connessione.
La connessione su cui viaggiano i pacchetti è rappresentata dai dispositivi di rete che partecipano allo smistamento dei pacchetti (router), ma le apparecchiature di rete gestiscono più comunicazioni sugli stessi canali fisici assegnando a ciascuna un numero di identificazione (canale logico).
Per mettere in comunicazione due sistemi si può scegliere tra due servizi:
Protocollo UDP: Datagramma, o senza connessione.
I pacchetti della comunicazione vengono trattati come se fossero tutti indipendenti, nel senso che ognuno di essi
quando giunge ad un sistema intermedio subisce un instradamento, che potrebbe, quindi, essere diverso per ciascun
pacchetto. Questo servizio non è affidabile perchè il destinatario potrebbe non accorgersi della
mancata ricezione di un pacchetto, ed inoltre i pacchetti, seguendo percorsi diversi, potrebbero giungere in ordine
invertito, costringendo il destinatario ad aspettare la ricezione di tutti i pacchetti e ricostruirli
nell'ordine corretto.
Protocollo TCP: circuito virtuale o con connessione
Consiste di 3 fasi: Apertura connessione, trasferimento dati e chiusura connessione.
Durante la fase di apertura, il mittente invia un pacchetto per avvertire il destinatario che intende inviare dei dati.
Lungo il percorso, i sistemi intermedi che ricevono il pacchetto creano un identificativo della connessione,
in modo che tutti i successivi pacchetti dati, subiranno lo stesso instradamento.
Il sistema destinatario che riceve il pacchetto di richiesta risponde con un pacchetto di accettazione.
Durante la fase di trasferimento
dati i sistemi intermedi leggono, per ogni pacchetto, l'identificativo della connessione e smistano il pacchetto.
Al termine la connessione viene rilasciata, che corrisponde a liberare il canale virtuale (o l'identificativo)
che era stato assegnato alla comunicazione.
Un'applicazione che vuole comunicare, per mezzo della rete, con un'altra applicazione deve creare un socket.
Si può pensare che un socket sia l'alloggiamento in cui si innesta un connettore ideale di un cavo di comunicazione che parte dal processo (o Thread) e termina sulla scheda di rete. Il processo è un'attività in esecuzione al livello Applicazione che offre vari servizi: invia messaggi, riceve messaggi, apre una connessione, ecc…
Il processo viene identificato mediante l'indirizzo con cui il computer che lo ospita è raggiungibile sulla rete e mediante l'indirizzo che il processo possiede all'interno del computer. Questo indirizzo è chiamato numero di porta.
Ad esempio i numeri di porta assegnati ad alcuni tra i più comuni servizi di rete sono:
Porta | Servizio |
21 | ftp - file transfer protocol |
23 | telnet - il servizio terminale virtuale |
25 | SMTP - Posta in uscita |
80 | http - server web |
Un utente, ad esempio, apre il browser che invia richieste di pagine web e mostra quelle che riceve usando la porta 80. Contemporaneamente l'utente può aprire un programma per leggere la posta elettronica usando la porta 110, può inviare una mail usando la porta 25, e può avviare una chat. Per le chat non esiste un numero di porta convenzionale, ogni programma può sceglierne uno.
L'indirizzo con cui il computer, che ospita il processo, è raggiungibile attraverso la rete è l'indirizzo IP (Internet Protocol). È formato da quattro gruppi di cifre decimali, nessun gruppo dei quali può essere maggiore di 255.
Ogni scheda di rete possiede un indirizzo di "loop-back". Questo viene usato per verificare e correggere un programma di comunicazione senza essere connessi a Internet. Questo indirizzo è 127.0.0.1. Quando un'applicazione invia un pacchetto a questo indirizzo, i bit del pacchetto attraversano tutti i circuiti della scheda di rete ma, giunti alla fine, vengono smistati sui circuiti di ricezione, anzichè sul connettore di uscita, e risalgono verso l'applicazione destinataria. In questo modo, si può avere il mittente e il destinatario del messaggio in esecuzione sullo stesso computer, allo scopo di verificare il funzionamento del programma e poi, solo dopo che sono stati eliminati gli errori dal programma, si possono separare le applicazioni: il mittente su una macchina e il destinatario su un'altra. In questo caso però le due applicazioni devono usare l'indirizzo IP pubblico.
Le persone preferiscono ricordare nomi anzichè numeri, mentre le macchine lavorano bene con i numeri. Quindi ad un computer viene assegnato un indirizzo IP ed un nome di dominio. L'utente che vuole collegarsi ad un computer, scrive il nome nella barra dell'indirizzo del browser e invia la richiesta. La rete provvede a determinare il numero di IP corrispondente al nome specificato dall'utente.
Un indirizzo di rete è un numero di 32 bit, di cui ciascun byte viene letto in decimale. Nel pacchetto che viaggerà sulla rete, l'indirizzo IP non è rappresentato con 4 gruppi di numeri decimali, ma deve essere portato nella forma a 32 bit. Si tratta quindi di trasformare i quattro gruppi di cifre decimali dell'IP in un numero a 32 bit, in trasmissione, e viceversa (da 32 bit a 4 numeri decimali) in ricezione.
Vi sono 4 funzioni che consentono di ottenere questa trasformazione:
Avviare Dev-Cpp
Scegliere File → Nuovo → Progetto...
Si apre un riquadro di dialogo:
Selezionare l'icona Empty Project
Assegnare il nome Server al progetto
Premere il pulsante √ Ok
Scegliere File → Nuovo → File Sorgente
Aggiungere la libreria Winsock.h
Progetto → Opzioni di Progetto
Nel riquadro di dialogo:
Selezionare la scheda Parametri
Premere il pulsante Aggiungi Libreria o Oggetto
Sfogliare la cartella LIB
fare clic sul file libwsock32.a
Fare clic su Ok per chiudere il riquadro.
Nell'area Programma dell'ambiente Dev-Cpp inserire le linee:
#include <iostream> #include <winsock.h> using namespace std;
La libreria iostream viene inclusa per poter richiamare gli oggetti cout e cin, mentre la libreria winsock.h mette a disposizione le funzioni per gestire una connessione di rete.
Aprire la sezione main del programma e dichiarare le variabili seguenti:
int main(int argc, char *argv[]) { SOCKET listenSocket; SOCKET remoteSocket; SOCKADDR_IN Server_addr; SOCKADDR_IN Client_addr; int sin_size; short port; char buffer[256]; int wsastartup; int ls_result;
Creare una WORD per contenere il numero di versione della libreria che fornisce il socket, e inizializzare i suoi due byte con i valori 2 e 2. In questo modo si verifica che la versione della libreria sia 2.2. Creare anche una variabile di tipo WSADATA:
WORD wVersionRequested = 0x0202; WSADATA wsaData;
il tipo WSADATA è una struttura definita nella libreria winsock.h. Quando si avvia il socket, i campi della variabile wsadata verranno completati con le informazioni di versione e altri dati relativi all'uso della connessione.
Avviare il socket richiamando la funzione wsastartup e verificando l'esito dell'operazione:
wsastartup = WSAStartup(wVersionRequested, &wsaData); if (wsastartup != NO_ERROR) cout << "Errore WSAStartup()" << endl;
La funzione WSAStartup riceve il numero di versione richiesto del socket e il riferimento alla struttura wsaData, restituisce l'esito dell'inizializzazione del socket.
Creare il socket.
L'intestazione della funzione socket è la seguente:
Se la creazione del socket riesce, la funzione restituisce il riferimento a un descrittore di SOCKET, altrimenti restituisce un numero negativo che rappresenta un codice di errore. Ogni descrittore di socket identifica una connessione.
Il primo parametro, af (Address Family) indica la famiglia di indirizzi da utilizzare. I possibili valori sono:
Famiglia | Descrizione |
AF_INET | IPv4 |
AF_INET6 | IPv6 |
AF_LOCAL | Unix Domain Protocol |
AF_ROUTE | Routing Sockets |
AF_KEY | Key Socket |
Il secondo parametro, type indica il tipo di socket. I possibili valori sono:
Type | Descrizione |
SOCKET_STREAM - Stream Socket (TCP). | Fornisce una connessione affidabile, ordinata e bidirezionale. Utilizza un protocollo connesso. |
SOCK_DGRAM - Datagram Socket (UDP). | La comunicazione avviene attraverso datagram. Utilizza un protocollo senza connessione. |
Il terzo parametro, protocol, può essere uno tra i seguenti valori: IPPROTO_IP, IPPROTO_IPV6, IPPROTO_TCP, IPPROTO_UDP, SOL_SOCKET.
listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSocket < 0) cout << "Server: errore nella creazione del socket." << endl; else cout << "il Socket e' pronto" << endl;
A questo punto il programma che ha creato il socket deve possedere un numero di porta. Un numero di porta rappresenta un identificatore del socket a cui è collegato il programma. Un client che intende comunicare con l'applicazione server deve inviare i suoi pacchetti all'indirizzo della macchina che ospita il server e deve specificare il numero della porta su cui il server è in ascolto.
La funzione bind associa indirizzo e porta al socket
Si sceglie un numero di porta, che non sia già usato da un'altra applicazione.
Dopo aver inizializzato i campi della struttura Server_addr di tipo SOCKADDR_IN
si chiama la funzione bind.
I parametri di connessione, Indirizzo del computer Host e numero di porta del socket, vengono memorizzati nei campi della struttura Server_addr:
struct SOCKADDR_IN {
Il campo sin_family contiene la famiglia di indirizzi utilizzati (Internet Family)
Il campo sin_port contiene il numero della porta
Il campo sin_addr contiene l'indirizzo dell'host
Il campo sin_zero è inutile e verrà ignorato.
port = 4000; Server_addr.sin_family = AF_INET; Server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); Server_addr.sin_port = htons(port); if (bind(listenSocket,(LPSOCKADDR) &Server_addr,sizeof(struct sockaddr)) < 0) { cout << "Server: errore durante la bind." << endl; closesocket(listenSocket); return -1; }
Il numero di porta (4000) viene rappresentato nella memoria del computer con i byte in ordine invertito rispetto all'ordine con cui vengono interpretati nel pacchetto. La funzione htons ha lo scopo di disporre i byte del numero di porta nell'ordine usato nel pacchetto.
La funzione inet_addr trasforma l'indirizzo dell'host, espresso come stringa, in un numero a 32 bit
La funzione bind associa l'indirizzo e il numero della porta contenuti nei capi del record Server_addr al socket listenSocket.
Riepilogando:
Nel modello Client/Server un computer agisce da server: accetta le richieste di connessione e fornisce un servizio; gli altri si comportano da client: utilizzatori di un servizio.
Il server, quindi viene messo in ascolto di richieste di servizio
La funzione listen riceve come parametri il socket creato e il numero massimo di connessioni ammesse. Questo secondo parametro viene usato per fissare la lunghezza della coda di richieste di connessioni entranti.
Se l'operazione non riesce, la funzione restituisce un codice di errore. Per questa ragione, dopo aver richiamato listen si interroga l'esito dell'operazione.
ls_result = listen(listenSocket, SOMAXCONN); if (ls_result < 0) cout << "il Server non riesce a passare in ascolto" << endl; else cout << "Il Server e' in Ascolto" << endl;
A questo punto il programma non procede, resta in uno stato di attesa e verrà risvegliato dal sistema operativo quando lo informerà che è stato ricevuto un pacchetto ad esso destinato.
Il primo pacchetto è di tipo "richiesta apertura connessione" e il server deve rispondere con un pacchetto di accettazione.
sin_size = sizeof(struct sockaddr_in); remoteSocket = accept(listenSocket, (struct sockaddr *) &Client_addr, &sin_size); cout << "Accettata la Connessione con Client: " << inet_ntoa(Client_addr.sin_addr) << endl;
Nella funzione accept il primo parametro è il descrittore del socket. Il secondo parametro è il riferimento ad una struttura che contiene l'intestazione del pacchetto ricevuto, tra cui, quindi, l'indirizzo del mittente. Il terzo parametro (facoltativo), è il riferimento ad una variabile contenente la lunghezza dell'indirizzo del computer sorgente.
La funzione accept estrae la prima richiesta di apertura connessione dalla coda e restituisce un riferimento ad un nuovo socket che servirà per ricevere i dati.
Dopo aver accettato la connessione, il server si aspetta di ricevere un pacchetto dati.
La funzione recv usando la connessione assegnata al client estrae i dati dal pacchetto e li memorizza
in una variabile di tipo stringa.
Il primo parametro della funzione recv è il riferimento al socket creato per questa connessione,
il secondo parametro è il riferimento alla stringa in cui memorizzare i dati, il terzo parametro è
la dimensione della stringa.
recv(remoteSocket, buffer, sizeof(buffer), 0); cout << "Messaggio Arrivato: " << buffer << endl;
In questo esempio, il server dopo aver ricevuto un pacchetto termina.
cout << "Chiudo il Server" << endl; WSACleanup(); system("pause"); return 0; }
Creare un nuovo progetto, denominandolo Client, nel menu Progrtto scegliere Opzioni del Progetto ed includere la libreria libwsock32.a, come fatto per il server.
Includere nel progetto un nuovo file sorgente e scrivere le seguenti righe:
#include <iostream> #include <winsock.h>
Nella funzione main dichiarare le seguenti variabili:
using namespace std; int main() { SOCKET clientsocket; SOCKADDR_IN addr; char messaggio[80]; short port; WORD wVersionRequested = MAKEWORD(2,2); WSADATA wsaData;
quindi inizializzare il socket:
WSAStartup(wVersionRequested, &wsaData);
Assegnare, alla variabile port il numero della porta su cui è in ascolto il server, e inizializzare i campi della struttura addr con l'indirizzo del computer destinazione e il numero di porta del server.
port = 4000; addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); addr.sin_port = htons(port);
Creare il socket per inviare i pacchetti e passargli i parametri di connessione:
clientsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (connect(clientsocket, (LPSOCKADDR) &addr, sizeof(addr)) < 0) cout << "Errore nella connessione con il Server" << endl;
Il messaggio da inviare viene acquisito da tastiera e memorizzato nella variabile messaggio
cout << "Messaggio da Inviare: " << endl; gets(messaggio);
Il messaggio viene inviato con la funzione send. il primo parametro della funzione send è il riferimento al socket, il secondo parametro è il riferimento alla stringa contenente il messaggio da inviare, il terzo parametro è il numero di byte del messaggio.
send(clientsocket, messaggio, sizeof(messaggio), 0);
Infine la connessione viene rilasciata:
WSACleanup(); system("pause"); return 0; }
Lanciare prima l'applicazione Server. Si aprirà una finestra dell'applicazione che mostra il messaggio "il server è in attesa di una richiesta di connessione".
Lanciare l'applicazione client. Si aprirà una finestra dell'applicazione che invita a scrivere un messaggio da spedire.
dopo aver spedito il messaggio il Server mostra il messaggio ricevuto e termina.