In questa prima parte si farà riferimento alle socket di Berkeley, costruendo degli esempi di programmi commentati e dando spiegazione sulle strutture e sulle chiamate che vengono messe a disposizione.
Per stabilire una connessione tra due processi è necessario specificare una quintupla di proprietà che permetta di identificarla univocamente:
{ Protocollo, Inidirizzo-Locale, Processo-Locale, Indirizzo-Remoto, Processo-Remoto }.
Vedremo ora in dettaglio le diverse chiamate di sistema che ci permettono di specificare queste proprieta'.
SOCKET
int socket( int famiglia, int tipo, int protocollo ) ;
La chiamata socket specifica il primo elemento della quintupla: il protocollo. La chiamata socket può ritornare:
Analiziamo i vari parametri della chiamata:
famiglia:
per svolgere I/O di rete, un sistema deve innanzitutto effetuare la chiamata di sistema socket specificando il tipo di protocollo desiderato. Le famiglie di protocolli a disposizione sono le seguenti:
tipo: il tipo di socket può essere di diversi tipi:
protocollo:
specifica il protocollo utilizzato dal socket, le principali costanti per i protocolli sono:
Le costanti IPPROTO_XXX sono definite nel file <netinet/in.h>, mentre le costanti NSPROTO_XXX sono definite nel file <nets/ns.h>.
BIND
int bind( int sockfd, struct sockaddr *mioindir, int lunghindr) ;
La chiamata di sistema bind definisce gli elementi inidirizzo-locale e processo-locale della quintupla che costituisce l'associazione. La chiamata bind può essere utilizzata in 3 modi diversi:
Analiziamone i parametri:
sockfd: descrittore di socket precedentemente creato con la chiamata di sistema socket.
mioindr: è un puntatore all'indirizzo specifico della struttura del protocollo.
lunghdir: dimensione della struttura del protocollo.
LISTEN
int listen( int sockfd, int backlog ) ;
Questa chiamata di sistema è usata da un server orientato alla connessione per indicare che è disposto a ricevere le connessioni. Solitamente è eseguita dopo le chiamate di sistema socket e bind ed immediatamente prima della chiamata accept. I parametri sono:
sockfd: descrittore di socket precedentemente creato con la chiamata di sistema socket.
backlog: specifica il numero di richieste di connessione che possono essere accodate dal sistema mentre è in attesa che il server esegua la chiamata accept.
ACCEPT
int accept( int sockfd, struct sockaddr *pari, int lunghindir ) ;
Dopo che un server orientato alla connessione ha eseguito la chiamata di sistema listen, si pone in attesa di una eventuale connessione da parte di un processoclient. La chiamata accept prende in esame la richiesta di connessione che si ha in testa alla coda specificata dalla listen e crea un altro socket con le stesse propietà di sockfd. Se non ci sono richieste di connessione in sospeso, questachiamata blocca il chiamante finche non ne arriva una.
sockfd: Descrittore di socket fd.
lunghindr: Contiene la lunghezza della struttura dell'indirizzo del processo connesso (client).Il lunghindir viene prima posto uguale alla dimensione della struttura e successivamente prende il valore della dimensione della struttura effettivamente occupata dal client connesso.
pari: struttura di tipo sockaddr, che contiene l'indirizzo del client appena connesso (client).
Questa chiamata restituisce fino a tre valori:
CONNECT
int connect( int sockfd, struct sockaddr *indirserver, int lunghindr ) ;
Mediante questa chiamata, un processo client connette un descrittore di socket facendo seguito ad una chiamata di sistema socket, al fine di stabilire una connessione con un server.Per la maggior parte dei protocolli la chiamata di sistema connect risulta l'effettiva attivazione di una connessione tra sistema locale e sistema remoto, la connessione causa l'assegnazione dei quattro elementi della 5-tupla dell'associazione: indirizzo-locale, processo-locale, indirizzo-remoto, processo-remoto. Nel caso che la chiamata venga usata con un protocollo non orientato alla connessione(UDP), la chiamata connect permette di memorizzare l'indirserver che il processo scriverà nel descrittore sockfd, in questo caso la chiamata fa immediatamente ritorno e non c'è un effettivo scambio di messaggi fra il sistema locale e il sistema remoto.
sockfd: Descrittore di socket.
indiserver: Questa struttura di tipo sockaddr contiene l'indirizzo del server al quale il client vuole connettersi.
lunghindr: Questa variabile contiene l'effettiva grandezza della struttura sockaddr che contiene l'indirizzo del server.
SEND, SENDTO, RECV, RECVFROM
int send( int sockfd, char *buff, int nbytes, int flags ) ;
int sendto( int sockfd, char *buff, int nbytes, int flags, struct sockaddr *to,int lunghindir) ;
int recv( int sockfd, char *buff, int nbytes, int flags ) ;
int recvfrom(int sockfd, char *buff, int nbytes, int flags, struct sockaddr *from, int lunghindir) ;
sockfd: Rappresenta il descrittore di socket.
buff: Questo buffer serve per prelevare o inserire i dati(struttura d'appoggio).
flags: a variabile flags può assumere i seguenti significati:
Tutte e quattro le chiamate restituiscono come valore della funzione la lunghezza dei dati che sono stati scritti o letti.Nell'uso tipico di recvfrom con un protocollo senza connessione, il valore di ritorno è la lunghezza del datagramma che è stato ricevuto.
SELECT
int select( int maxfdpl, fd_set *readfds, fd_set *writefds, fd_set *expectfds, struct timeval *timeout ) ;
La select è una delle più importanti chiamate del sistema dal lato server, infatti è usata per simulare server del tipo: concorrente singolo-processo (altri tipi di server sono: server iterativo, e server concorrente multi-processo).La prima volta che un client si collega al server viene settata in una maschera di bit il socketdescriptor associato a quella connessione, successivamente quando arriva una richiesta sul socketdescriptor di lettura del server si controllerà la maschera contenente tutti sockdescriptor delle connessioni per stabilire e servire chi ha fatto la richiesta. La maschera può essere riferita ad un array che come indice usa il socketdescriptor della connessione, e come contenuto del vettore un valore bit, 1 se il client associato a quel socketdescriptor ha fatto richiesta al server, 0 se il client non sta facendo richieste al server. Le macro per manipolare la maschera di bit sono:
Se si è resa bloccante la chiamata della select( puntatore della struttura timeval a NULL), il risveglio dipende dai seguenti eventi:
maxfdpl: numero massimo di descrittori esaminati.
readfds: maschera di bit riguardante la lettura, contenente i socket descriptor delle connessioni dei client.
writefds: maschera di bit riguardante la scrittura, contenente i socket descriptor delle connessioni dei client.
readfds: maschera di bit riguardante la lettura, contenente i socket descriptor delle connessioni dei client.
exceptfds: maschera di bit riguardante le eccezioni che si possono verificare, sulle varie connessioni dei vari client.
struttura timeval è:
struct timeval{
long tv_sec; / secondi /
long tv_usec;/ microsecondi /
}
ROUTINE IMPORTANTI
STRUTTURE PRINCIPALI E VARI TIPI DI SERVER
Dopo aver introdotto le principali system call nel punto precedente, analizzeremo ora le strutture che sono indispensabili nella progettazione di un sistema in rete con le socket BSD e che permettono di definire gli indirizzi utili alla comunicazione in rete. Per le famiglie di indirizzi internet, le strutture sono definite nell'header <netinet/in.h >:
struct sockaddr{
short sa_family ; /* Famiglia d'indirizzo: valore AF_xxx */
short sa_data[14] ; /* fino a 14 byte d'indirizzo specifico del protocollo */
}
struct in_addr {
u_long s_addr; /* IDrete/IDhost 32 bit */
/* ordinato per byte di rete */
}
struct sockaddr_in{
short sin_family ; /* AF_INET */
u_short sin_port ; /* numero di porta di 16 bit */
struct in_addr sin_addr ; /* struttura definita precedentemente */
char sin_zero[8] ; /* non usato */
}
Questa struttura verrà successivamente passata alla funzione BIND che associerà la socket aperta a un numero di porta e a un indirizzo. Dopo aver descritto queste strutture che definiscono gli indirizzi di comunicazione tra diversi Host, descriviamo adesso le istruzioni di cooperazione tra client e server per avere una comunicazione tra due host con un collegamento orientato alla connessione:
SERVER CLIENT
--------------- ---------------
| sd = socket | | sd = socket |
| | | | | |
| V | | | |
| Bind | | | |
| | | | | |
| V | | | |
| listen | | | |
| | | | | |
| V | connessione | V |
| accept <------------------------> connect |
| | | | | |
| V | dati richiesta V |
| -->read <------------------------ write<-- |
| | | | | | | |
| | V | dati risposta | V | |
| ---write ------------------------> read--- |
| | | | | |
| V | | V |
| close | | close |
| | | |
--------------- ---------------
Vediamo adesso il codice del server iterativo per una comunicazione con connessione:
int sockfd, newsockfd;
...
if ( (sockfd = socket(...) ) < 0)
err_sys("socket error");
if ( (bind(sockfd, my_addr, my_ll) < 0)
err_sys("bind error");
if ( (listen(sockfd, 5) < 0)
err_sys("listen error");
for( ; ; ) {
newsockfd=accept(sockfd,cli_addr,cli_ll);
if (newsockfd < 0)
err_sys("accept error");
/* process request on newsockfd */
read(newsockfd, ...);
write(newsockfd, ...);
close (newsockfd);
}
In questo tipo di connessione, il server è di tipo iterativo cioè è lui stesso che si occupa della comunicazione con il client, con questo metodo il server riesce solo a gestire un client alla volta; vediamo adesso il codice di un server detto concorrente che permette di poter gestire più richieste attraverso la system call fork() lasciando cosi la comunicazione al figlio; il server ritorna dopo essersi sdoppiato ad ascoltare sulla porta di ricezione:
int sockfd, newsockfd;
...
if ( (sockfd = socket(...) ) < 0)
err_sys("socket error");
if ( (bind(sockfd, myaddr, myll) < 0)
err_sys("bind error");
if ( (listen(sockfd, 5) < 0)
err_sys("listen error");
for( ; ; ) {
newsockfd=accept(sockfd,cliaddr,clill);
if (newsockfd < 0)
err_sys("accept error");
if (fork() == 0) { /* child */
close(sockfd);
/* process request on newsockfd */
read(newsockfd, ...);
write(newsockfd, ...);
exit (0);
}
close (newsockfd); /* parent */
}
Il server di tipo concorrente che abbiamo descritto sopra ha però un difetto che è quello di occupare troppe risorse di sistema; abbiamo infatti un processo per ogni richiesta, in questo modo le prestazioni della macchina vengono compromesse;La soluzione è l'utilizzo di un server concorrente che usa la chiamata select.Il codice del server con la select è questo:
int sockfd, newsockfd;
int nfds;
// STRUTTURA PER INDIRIZZI INTERNET
fd_set rfds ;
fd_set afds ;
...
if ( (sockfd = socket(...) ) < 0)
err_sys("socket error");
if ( (bind(sockfd, my_addr, my_ll) < 0)
err_sys("bind error");
if ( (listen(sockfd, 5) < 0)
err_sys("listen error");
nfds = FOPEN_MAX ;//NUMERO MASSIMO DI DESCRITTORI CONSENTITI
// INIZIALIZZA LA MSCHERA DEI DESCRITTORI PER LA SELECT
FD_ZERO( &afds ) ;
//SETTA NELLA MASCHERA IL DESCRITTORE DELLA SOCKET PASSIVA
FD_SET( sockfd, &afds) ;
// CICLO PER LA GESTIONI DELLE CONNESSIONI
while( 1 ){
// COPIA DELLA STRUTTURA DALLA SORGENTE ALLA DESTINAZIONE
bcopy( (char*) &afds, (char*) &rfds, sizeof(rfds) ) ;
if(select( nfds, &rfds, (fd_set *) 0, (fd_set *) 0,
(struct timeval*) 0) < 0)
{
perror("select") ;
exit( 0 );
}//fine if
// CONTROLLO DI NUOVE CONNESSIONI
//GESTIONE DELL'INPUT SULLA SOCKET PASSIVA
if(FD_ISSET(sockfd, &rfds)){
clilen = sizeof( cli_addr ) ;
newsockfd = accept(sockfd,(struct sockaddr *) &cli_addr, &clilen ) ;
if (newsockfd < 0 )
{
perror( "accept") ;
}
else
FD_SET( newsockfd, &afds ) ;
}//fine if
// GESTIONE DEI CLIENTI CONNESSI
for (fd=0; fd<FOPEN_MAX; fd++)
{
if(fd != sockfd && FD_ISSET(fd, &rfds))
if( function_communication()>0)
{
FD_CLR(fd, &afds) ;
close(fd);
printf("Chiusa connessione con partecipante\n");
}
}//fine for
}//fine while
Per maggiori informazione sul funzionamento della select consultare il punto precedente Chiamate principali delle socket di Berkeley.Vediamo adesso il codice del client per una comunicazione con connessione:
int sockfd;
...
if ( (sockfd=socket(...))<0)
err_sys("socket error");
if ( (connect(sockfd,servaddr,servll)>0)
err_sys("connect error");
/* process request to server */
write(sockfd, ...);
read(sockfd, ...);
close(sockfd);
}
Passiamo adesso allo schema di una comunicazione senza connessione che rappresenta un tipo di server chiamato connection-less:
SERVER CLIENT
--------------- ---------------
| sd = socket | | sd = socket |
| | | | | |
| V | | V |
| Bind | | Bind |
| | | | | |
| V | dati richiesta | V |
| --> recvfrom <----------------------- sendto <-- |
| | | | | | | |
| | V | dati risposta | V | |
| -- sendto ------------------------> recvfrom -- |
| | | | | |
| | | | | |
| V | | V |
| close | | close |
| | | |
--------------- ----------------
Questi schemi riportati sono schemi standard per la comunicazione in rete, tutte le applicazioni di tipo network possono essere riportate ad un modello client-server che si divide come già sopra citato in due tipo di comunicazione uno orientato alla connessione e uno non orientato alla connessione.
TESTO DEL PROGETTO DELLA CONFERENZA DISTRIBUITA
(Autori: Lanzi Andrea: shadow.net@tiscalinet.it, Giampaolo Fresi Roglia: gian_fresi@iol.it)
Lo scopo del progetto è quello di costruire un client-server per il distribuited conferencing in ambiente UNIX/internet.Il sistema Permette a n (n>=3) utenti di partecipare ad una sessione di distribuited conferencing.Il sistema è composto da n processi partecipanti potenzialmente attivi su diversi elaboratori in internet e da un processo coordinatore, attivo su un elaboratore ad un indirizzo (host e porta) noto. Il coordinatore conosce in ogni momento le identità dei partecipanti e degli iscritti a parlare; esso assegna il diritto di parola agli iscritti in ordine FIFO. Un partecipante che assume il ruolo di oratore invia il testo del propio intervento agli altri partecipanti e al coordinatore, che lo memorizza in un file di log.
DESCRIZIONE DEL PROGETTO:
Dopo la compilazione deve essere eseguito prima il server con il numero di porte(Es. server.e 16000) e poi i vari client con l'indirizzo ip del server e la porta(Es. client.e 192.168.100.1 16000).
L'iscrizione dei client non viene effettuata automaticamente ma viene effettuata da menù.
Le nostre scelte progettuali di usare la bufferizzazione dell'input o meno derivano dall'ambiente di rete su cui si fa girare il programma: su una rete veloce su cui spedire un carattere per pacchetto non sia considerato uno spreco usare l'input non bufferizzato può essere una cosa accettabile, mentre se ciò dovesse rappresentare un problema la scelta più indicata sarebbe l'utilizzo dell'input bufferizzato.
La scelta di usare il vettore vector di dimensione FOPEN_MAX è stata dettata dalla necessità di individuare direttamente tramite l'fd di ogni socket aperta le informazioni sul client relativo ad essa. Tutte le operazioni effettuate sulle liste del server (inserimento e cancellazione) vengono effettuate direttamente tramite l'accesso al vettore evitando così l'aspetto negativo della gestione delle liste(gestione lineare).
Per quanto riguarda le strutture di memoria del server abbiamo utilizzato un vettore e due liste bidirezionali una per la gestione dei client iscritti a parlare e una per il mantenimento delle informazioni dei vari client iscritti alla conferenza(IP ADDRESS e numero di porta d'ascolto del client). IL vettore usato e costituito da 2 puntatori per ogni elemento, uno usato per puntare alla lista delle informazioni e l'altro usato per puntare alla lista degli iscritti a parlare. Ogni client è identificato dal numero intero del descrittore della socket aperta con il coordinatore e questo descrittore serve per indicizzare il vettore e recuperare le varie informazioni su di esso. Ogni operazione fatta sul client (cancellazione, aggiunta dalla conferenza ) viene effetuta tramite la socket descriptor.
I messaggi tra client e server sono definiti nel file messages.h e sono rappresentati da numeri interi.
gestione errori:
nel gestire le eventuali cadute dei client connessi abbiamo riscontrato che ad ogni caduta di socket il processo riceve un segnale di SIGPIPE che noi abbiamo gestito ignorandolo; abbiamo così potuto gestire le eventuali cadute come se fossero semplici disconnessioni.
Gli indirizzi IP dei client che si connettono al coordinatore, vengono ricavati automaticamente dai loro socket descriptor attraverso la funzione GETPEERNAME() le porte d'ascolto dei client vengono invece fatte spedire.
Per quanto riguarda il client ogni volta che gli viene data la parola gli vengono forniti tutti gli indirizzi e le porte d'ascolto dei vari partecipanti che vengono inseriti in una struttura locale.
Per la compilazione del progetto è stato fatto un Makefile, ci sono 2 possibilità di compilazione la prima con il comando:
make
la seconda:
make unbuffered
La seconda compilazione permette durante la scrittura del testo da parte dell'oratore di avere un input non bufferizzato, e quindi l'immediata scrittura dei caratteri nella finestra del client; la prima compilazione effetua invece l'input bufferizzato.
Il progetto è stato provato tramite due macchine collegate a INTERNET con il server su una macchina e vari client aperti tra le due macchine, sono state fatte diverse prove tutte con esito positivo. E' stato inoltre provato tra due macchine che erano collegate con due schede di rete e i vari protocolli TCP/IP montati sopra, anche qui l'esito delle prove è stato positivo.