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:

  1. 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.
  2. Un client può registrare un indirizzo specifico per se stesso, in modo che il server risponda alle richieste su quel indirizzo.
  3. 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:

  1. EADDRINUSE : l’indirizzo non è utilizzabile.
  2. -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:

  1. Intero che indica un errore.
  2. Intero che indica il nuovo socket descriptor.
  3. 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:

  1. ETIMEDOUT : è scaduto il timeout del SYN (pacchetto utilizzato del avviare una connessione).
  2. ECONNREFUSED : il server ha rifiutato la connessione (vedi chiamata a funzione listen).
  3. 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:

  1. MSG_OOB : invia o riceve dati fuori banda.
  2. MSG_PEEK : preleva un messaggio in arrivo.(recv, recvfrom)
  3. 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:

  1. Il primo modo è quello di rendere bloccante la select mettendo a NULL il puntatore alla struttura Timeval.
  2. 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.
  3. 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:

  1. uno dei socket descriptor di readfds è pronto per la lettura.
  2. uno dei socket descriptor di writefd è pronto per la scrittura.
  3. 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

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *