| Chiamate principali delle socket di Berkeley |
La premessa di quest'articolo è trasmettere le mie conoscenze sulle Socket di Berkeley.
L'utilizzo di questo materiale è libero per un qualsiasi utilizzo. Non è codice che può
arrecare danni fisici o logici alle vostre macchine. Spero che sia di aiuto agli studenti
e agli appassionati di Reti di Calcolatori.
Per stabilire una connessione tra due processi è necessario specificare una quintupla
di proprietà che permetta di identificarla univocamente:
{ Protocollo, Indirizzo-Locale, Processo-Locale, Indirizzo-Remoto, Processo-Remoto }.
Preambolo
Definizione delle strutture
Viene qui definita una struttura sockaddr richiesta nelle chiamate. Tutte
le funzioni dei socket che usano gli indirizzi sono definite usando nel prototipo
un puntatore a questa struttura. Essendo una struttura generica, quando si invocano
dette funzioni passando l'indirizzo di un protocollo specifico occorrerà eseguire
una conversione del relativo puntatore.
struct sockaddr {
sa_family_t sa_family; /* address family AF_xxx */
char sa_data[14]; /* address (protocol-specific) */
};
A noi interessa la struttura degli indirizzi dei socket IPv4. Per utilizzare questa
struttura, occorrerà fare un'operazione di cast che, nel nostro caso, sarà la conversione
della struttura generica sockaddr nella struttura sockaddr_in.
struct sockaddr_in {
sa_family_t sin_family; /* address family AF_INET */
in_port_t sin_port; /* port in network byte order (2 bytes)*/
struct in_addr sin_addr; /* internet address (4 bytes) */
char sin_zero[8]; /* non usato */
};
/* Internet address. */
struct in_addr {
in_addr_t s_addr; /* 32 bit IPv4 - address in network byte order */
};
bzero(mia_sockaddr_in, sizeof(sockaddr_in));
Per azzerare una struttura sockaddr_in occorre la funzione bzero.
Funzioni utili
Solo IPv4
inet_addr
mio_sockaddr_in.sin_addr.s_addr = inet_addr("10.1.45.8");
Se stiamo passando l'ip con una stringa, allora l'ip deve essere
convertito in network order dalla funzione inet_addr.
inet_ntoa
char* ip = inet_ntoa(mio_sockaddr_in.sin_addr);
Se vogliamo recuperare l'indirizzo ip come testo, dobbiamo
utilizzare la funzione inet_ntoa. Ovvero, quando otteniamo una struttura
sockaddr_in con i parametri del contattato (o contattante) possiamo ottenere
il suo indirizzo ip passando il valore sin_addr della struttura sockaddr_in.
inet_aton
inet_aton("10.1.45.8", &mio_servaddr.sin_addr);
Converte l'indirizzo ip passato come stringa, in una struttura in_addr.
IPv4 (AF_INET) e IPv6 (AF_INET6)
inet_pton
inet_pton(AF_INET, "10.1.45.8", &servaddr.sin_addr);
Converte l'indirizzo espresso dalla stringa in un indirizzo
espresso con una struttura sin_addr.
inet_ntop
inet_ntop(AF_INET, &servaddr.sin_addr, ip, sizeof(ip));
Converte l'indirizzo espresso nella struttura sin_addr in una stringa leggibile
con la nota notazione.
Chiamate a funzione
Definite le strutture che utilizzeremo, passiamo in dettaglio le diverse chiamate
di sistema che ci permettono di far comunicare due processi utilizzando l'architettura
Client/Server.
socket (non bloccante)
int socket(int famiglia, int tipo, int protocollo);
E' la prima chiamata eseguita dal client e dal server.
La chiamata socket specifica il primo elemento della quintupla: il protocollo. La
chiamata socket può ritornare:
- il socket descriptor se la chiamata è andata a buon fine.
- -1 se la chiamata non ha avuto successo.
I parametri della chiamata sono:
famiglia:
per svolgere I/O di rete, un sistema deve innanzitutto effettuare la chiamata di
sistema socket specificando il tipo di protocollo desiderato. Le famiglie di protocolli
a disposizione sono le seguenti:
- AF_INET: Protocolli di internet IPv4.
- AF_INET6: Protocolli di internet IPv6.
- AF_LOCAL: Protocollo locale (client e server sullo stesso host).
- AF_UNIX: Protocolli interni di Unix.
Il prefisso AF sta per "address family" (famiglia di indirizzi). Esiste poi un altro
insieme di termini, con prefisso PF che sta per "protocol family" (famiglia di protocolli:
PF_UNIX, PF_INET, PF_NS, PF_IMPLINK). Usare AF o PF è equivalente.
tipo: il tipo di socket può essere di diversi tipi:
- SOCK_STREAM : socket stream (TCP protocol).
- SOCK_DGRAM : socket di datagramma (UDP protocol).
- SOCK_RAW : socket per applicazioni dirette su IP.
Il protocollo TCP è un protocollo affidabile per la consegna dei dati. Ha un controllo
del flusso, non satura la rete e non sovraccarica il ricevitore. Se un pacchetto viene perso
durante la trasmissione, esso viene rispedito. Il destinatario avrà una copia identica
dell'informazione inviata dal mittente.
Al contrario, UDP (User Datagram Protocol) è un protocollo non affidabile. Viene detto
un protocollo "best effort" in quanto fa del suo meglio per portare a termine la trasmissione.
Avendo minor controllo sulla trasmissione, presenta come pregio quello di generare un minore
overhead (carico inutile) nella rete, ma come difetto quello di non tentare il recupero delle
informazioni perse durante la trasmissione. Consigliato per Video-Chat, flusso video, informazioni
di poco interesse (se si perde un riquadro di una trasmissione probabilmente potremmo non accorgercene).
protocollo:
specifica il protocollo utilizzato dal socket, le principali costanti dei protocolli sono:
- IPPROTO_UDP : UDP.
- IPPROTO_TCP : TCP.
- IPPROTO_ICMP : ICMP (Internet Control Message Protocol (protocollo ausiliario di IP).
- IPPROTO_RAW : IP.
- 0 : specifica il protocollo di default indotto dalla coppia family e type (tranne per
SOCK_RAW). Per esempio, AF_INET e SOCK_STREAM determinano un protocollo IPPROTO_TCP, mentre
AF_INET + SOCK_DGRAM determinano un protocollo IPPROTO_UDP.
Le costanti IPPROTO_XXX sono definite nel file <netinet/in.h>, mentre le costanti
NSPROTO_XXX sono definite nel file <nets/ns.h>.
bind (non bloccante)
int bind(int sockfd, struct sockaddr *mio_indir, int lungh_mio_indir);
Utilizzata, in genere, dal server per usare una porta prefissata,
la chiamata di sistema bind definisce gli elementi indirizzo-locale e processo-locale
della quintupla che costituisce l'associazione.
Se non eseguiamo una chiamata alla funzione bind, il sistema operativo assegnerà al socket
una porta qualunque ed uno degli indirizzi IP dell'host.
La chiamata bind può essere utilizzata in 3 modi diversi:
- I server registrano il loro indirizzo, ben noto, nel sistema. In pratica dicono al
sistema: "Questo è il mio indirizzo e qualsiasi messaggio ricevuto adesso indirizzato
deve essere consegnato a me!". Questa procedura deve essere fatta prima di accettare
richieste da un client sia dai server orientati alla connessione sia da quelli che
non lo sono.
- Un client può registrare un indirizzo specifico per se stesso, in modo che il server
risponda alle richieste su quel indirizzo.
- Un client senza connessione deve accertarsi che il sistema assegni un certo indirizzo
unico, affinchè l'altra estremità abbia un indirizzo di ritorno valido su cui inviare
le risposte.
Analizziamone i parametri:
sockfd: descrittore di socket precedentemente creato con la chiamata di sistema
socket.
mio_indir: è un puntatore all'indirizzo specifico della struttura del protocollo.
E' una struttura sockaddr di cui devono essere specificati l'indirizzo IP e il numero
di porta (indirizzo locale). Se questi due valori non vengono specificati, allora
verranno posti a zero, e il server accetterà richieste su qualsiasi interfaccia, utilizzando
una porta qualunque.
lungh_mio_indir: dimensione della struttura del protocollo.
Questa chiamata potrebbe restituire errore:
- EADDRINUSE : l'indirizzo non è utilizzabile.
- -1 : se la chiamata non ha esito positivo (altri errori).
listen (server, non bloccante, TCP)
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.
listen imposta il socket "in ascolto". Da questo socket il server riceverà
le chiamate che potrà gestire successivamente. Il vero socket (quello utilizzato per la
comunicazione con un client sarà creato da accept.
Le connessioni vengono accettate e rifiutate dal sistema operativo senza
interrogare il server. 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 (server, bloccante se non ci sono socket in coda)
int accept(int sockfd, struct sockaddr *suo_indir, int lungh_suo_indir);
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 processo
client. 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 proprietà di sockfd. Se non ci sono richieste di
connessione in sospeso, questa chiamata blocca il chiamante finchè non ne
arriva una (si potrà fermare il processo digitando Ctrl+C).
Parametri:
sockfd : Descrittore di socket fd.
suo_indir : struttura di tipo sockaddr, che contiene l'indirizzo del client
appena connesso.
lungh_suo_indir : Contiene la lunghezza della struttura dell'indirizzo del
processo connesso. Il lungh_suo_indir viene prima posto uguale alla dimensione
della struttura e successivamente prende il valore della dimensione della struttura
effettivamente occupata dal client connesso.
Questa chiamata restituisce fino a tre valori:
- Intero che indica un errore.
- Intero che indica il nuovo socket descriptor.
- L'indirizzo del processo client e la dimensione di questo indirizzo.
connect (Client)
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.
Parametri:
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.
Questa chiamata restituisce fino a tre valori:
- ETIMEDOUT : è scaduto il timeout del SYN (pacchetto utilizzato del
avviare una connessione).
- ECONNREFUSED : il server ha rifiutato la connessione (vedi chiamata a funzione
listen).
- EHOSTUNREACH : Errore di reindirizzamento, host irraggiungibile).
close
int close(int sockfd);
Chiude il socket descriptor. In realtà, la connessione non viene immediatamente chiusa,
ma continua a trasmettere i dati eventualmente in transito. Nel frattempo, il processo
non può più utilizzare il descrittore.
Parametri
sockfd : descrittore del socket da chiudere.
Funzioni di Input/output
read e readline
int read (int sockfd, char* recvline, int size);
int readline(int sockfd, char* recvline, int size);
Le due funzioni permettono di leggere da un dato socket.
read legge size caratteri dal socket. Se la funzione legge meno dati
di quanti richiesti da size, vuol dire che non ci sono dati da leggere.
readline legge size caratteri dal socket. La funzione si blocca
in lettura sul socket fino a quando non riceve una battuta di invio (un
carattere di newline '\n'.
Procedendo con ordine, i parametri sono:
sockfd : descrittore del socket su cui scrivere.
recvline : variabile di appoggio delle informazioni recuperate dal socket.
size : quantità di byte da leggere.
write
int write(int sockfd, char* line, int size);
La funzione permette di scrivere dati in un socket. I parametri sono:
sockfd : descrittore del socket su cui scrivere.
line : variabile di appoggio delle informazioni recuperate dal socket.
size : quantità di byte da scrivere.
se la funzione scrive meno byte di quanti richiesti da size, allora non c'è spazio
disponibile nel buffer del socket.
fgets
int fgets(char* buff, int quantitadati, sockfd);
Legge dal socket e scrive i dati in buff.
Parametri:
buff : Questo buffer serve per prelevare o inserire i dati(struttura d'appoggio).
quantitadati : numero di byte da leggere.
sockfd : descrittore di socket
fputs
int fputs(char* buff, fd);
Scrive i dati memorizzati in buff nel socket.
buff : Questo buffer serve per prelevare o inserire i dati(struttura d'appoggio).
sockfd : descrittore di socket
Altre funzioni di Input/output
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);
Parametri:
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:
- MSG_OOB : invia o riceve dati fuori banda.
- MSG_PEEK : preleva un messaggio in arrivo.(recv, recvfrom)
- MSG_DONTROUTE : evita l'instradamento.(send, sendto)
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:
- FD_ZERO(fd_set *fdset); Azzera tutti gli elementi della maschera dei bit.
- FD_SET(int fd, fd_set*fdset); Setta il socketdescriptor(fd) nella maschera dei bit.
- FD_CLR(int fd, fd_set*fdset); Elimina il socketdescriptor(fd) dalla maschera dei bit.
- FD_ISSET( int fd, fd_set*fdset) ; Controlla se il socketdescriptor(fd) è 1 o 0.
Ci sono 3 modi in cui specificare i descrittori di file da esaminare:
- Il primo modo è quello di rendere
bloccante la select mettendo a NULL il puntatore alla struttura Timeval.
- Il secondo modo è di mettere il
valore del timer della struttura timeval uguale a 0,si avrà polling (interrogazioni
continue) sulla maschera dei bit, senza bloccaggio.
- Il terzo modo è quello di mettere
il timer della struttura a un certo valore,in questo modo la select interrogherà
la maschera di bit ad ogni quantità di tempo(quella settata nella struttura).
Se si è resa bloccante la chiamata della select(puntatore della struttura timeval
a NULL), il risveglio dipende dai seguenti eventi:
- uno dei socket descriptor di readfds è pronto per la lettura.
- uno dei socket descriptor di writefd è pronto per la scrittura.
- uno dei socket descriptor di exceptfds è in una eccezione pendente.
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.
La struttura timeval è:
struct timeval{
long tv_sec; /* secondi */
long tv_usec; /* microsecondi */
}
Approfondimenti
Socket di ascolto
Il socket di ascolto, il "listening socket", è quello creato dalla funzione socket e
viene utilizzato per tutta la durata del processo. Generalmente, viene usato per
accettare richieste di connessione. Dopodichè il server crea un altro socket per
connettersi effettivamente con il client. Viene utilizzata una chiamata a sistema
fork() per duplicare il processo server con cui trattare la trasmissione con il client.
Socket connesso
Questo è il tipo di socket utilizzato per la connessione con un certo client e usato
per lo scambio di dati con il client.
Download
I miei esercizi |
Altro materiale |
Stampa quest'articolo
|