[successivo] [precedente] [inizio] [fine] [indice generale]


Capitolo 2.   I socket di rete

L'interfaccia software basata si socket prevede l'utilizzo di un handle (maniglia) per gestire un flusso di dati, in modo del tutto analogo a quello che avviene con i file; tale handle è un intero che prende il nome di socket.

Quindi un socket di rete è un canale di comunicazione tra due processi in rete, identificato da un intero, sul quale ognuno dei processi può leggere o scrivere usando funzioni simili a quelle usate per leggere o scrivere sui file (a basso livello).

2.1   Funzioni da usare per la programmazione in rete

Per il linguaggio C esistono delle funzioni (molte delle quali riguardano i socket) e strutture dati da utilizzare per realizzare processi che comunicano in rete; esse sono definite in alcuni header memorizzati solitamente in /usr/local.

Nella tabella 2.1 sono elencate alcune di queste funzioni con il relativo header.

Tabella 2.1

uso funzione header
creazione socket int socket() <sys/socket.h>
creazione socket int bind() <sys/socket.h>
connessione socket int connect() <sys/socket.h>
ascolto socket int listen() <sys/socket.h>
attesa connessione socket int accept() <sys/socket.h>
ascolto socket int select() <sys/select.h>
lettura socket ssize_t recvfrom() <sys/socket.h>
lettura socket ssize_t recv() <sys/socket.h>
lettura socket ssize_t read() <unistd.h>
scrittura socket ssize_t sendto() <sys/socket.h>
scrittura socket ssize_t send() <sys/socket.h>
scrittura socket ssize_t write() <unistd.h>
chiusura socket int close() <unistd.h>
chiusura socket int shutdown() <sys/socket.h>
lettura ind. locale socket int gestsockname() <sys/socket.h>
lettura ind. remoto socket int getpeername() <sys/socket.h>
impostazione opzioni socket int setsockopt() <sys/socket.h>
lettura opzioni socket int getsockopt() <sys/socket.h>
conversione indirizzi short int htons() <netinet/in.h>
conversione indirizzi short int ntohs() <netinet/in.h>
conversione indirizzi long int htonl() <netinet/in.h>
conversione indirizzi long int ntohl() <netinet/in.h>
conversione indirizzi int inet_aton() <arpa/inet.h>
conversione indirizzi char *inet_ntoa() <arpa/inet.h>
conversione indirizzi int inet_pton() <arpa/inet.h>
conversione indirizzi char *inet_ntop() <arpa/inet.h>
nome host int gethostname() <unistd.h>
risoluzione nomi struct hostent *gethostbyname() <netdb.h>
risoluzione nomi struct hostent *gethostbyname2() <netdb.h>
risoluzione nomi void sethostent () <netdb.h>
risoluzione nomi void endhostent () <netdb.h>
risoluzione nomi struct hostent *gethostbyaddr() <netdb.h>
nomi di servizi struct servent *getservbyname() <netdb.h>
nomi di servizi struct servent *getservbyport() <netdb.h>

Il tipo ssize_t è un tipo primitivo definito in <sys/types.h> (come size_t usato nel pragrafo 2.4.2).

Le strutture hostent e servent sono definite in <netdb.h> e vengono descritte, insieme alle funzioni che le usano, nel capitolo 5.

Quando si programma in C in Unix o GNU/Linux si deve porre attenzione ai problemi di portabilità tra piattaforme diverse; storicamente infatti alcuni tipi di dati dello standard ANSI C sono stati associati a variabili dei sistemi ospiti dandone per scontata la dimensione. Ad esempio il puntatore all'interno di un file è un intero a 32 bit, cioè un int, il numero di dispositivo è un intero a 16 bit cioè uno short; cambiando la piattaforma hardware però questi tipi possono non corrispondere dando origine a notevoli problemi di portabilità. Per questo motivo le funzioni di libreria non fanno riferimento ai tipi elementari del linguaggio C, ma ai tipi primitivi del sistema, definiti in <sys/types.h>. In questo modo i tipi di dati utilizzati da tali funzioni rimangono indipendenti dai tipi elementari supportati dal particolare compilatore C utilizzato.

Anche le funzioni da usare per i socket fanno largo uso dei tipi di dati primitivi e quindi per il loro utilizzo è sempre necessario includere l'header <sys/types> oltre a quello in cui la funzione è definita.

Per la funzione select() è inoltre necessaria l'inclusione di <sys/time.h>.

2.2   Creazione di un socket

La funzione per la creazione di un socket è socket() il cui prototipo è il seguente:

int socket(int domain, int type, int protocol)

restituisce l'identificatore del socket oppure -1 se c'è errore; in tal caso viene valorizzata la variabile globale errno, definita nell'header <errno.h>, con opportuni valori i più significativi dei quali sono:

I valori elencati in maiuscolo sono alcuni nomi simbolici associati ai valori numerici che identificano i vari errori. Tutti i nomi simbolici di errori iniziano per «E», sono nomi riservati e sono definiti in <errno.h>. Il loro utilizzo è consigliato ogniqualvolta si usa una delle funzioni delle librerie del C e questa ritorna «errore» (-1); in tal caso infatti può essere utile conoscere il tipo di errore occorso testando la variabile errno. I valori che essa può assumere in seguito all'invocazione di una funzione che va in errore sono elencati nel manuale in linea della funzione utilizzata.

Per visualizzare il messaggio di errore associato a un certo valore della variabile errno si può utilizzare la funzione perror() che ha il seguente prototipo:

void perror(const char *msg)

//stampa sullo standard error il messaggio di errore relativo;
//al valore di errno preceduto da msg, da un «:» e 
//     da uno spazio.

Il valore di errno che viene preso in considerazione è quello relativo all'ultimo errore avvenuto.

I parametri della funzione socket() sono:

I nomi indicati in maiuscolo sono nomi simbolici definiti in <sys/socket.h>.

I valori più interessanti dello stile di comunicazione sono SOCK_STREAM e SOCK_DGRAM perché sono quelli utilizzabili (sia con PF_INET che con PF_INET6) rispettivamente con i protocolli TCP e UDP (e sono utilizzabili anche con PF_UNIX).

2.3   Socket bloccanti e non bloccanti

Un socket viene normalmente creato come «bloccante»; ciò significa che, a seguito di una chiamata alla funzione di attesa connessione, blocca il processo chiamante, fino all'arrivo di una richiesta di connessione.

Un socket «non bloccante», invece, non provoca il blocco del processo chiamante in una attesa indefinita; se al momento dell'attesa di connessione non è presente alcuna richiesta, il processo continua la sua esecuzione e la funzione di attesa connessione fornisce un appropriato codice di errore.

Per rendere un socket non bloccante si deve usare la funzione fcntl(), di controllo dei file (un socket può essere in effetti assimilato ad un file gestito a basso livello) definita in <fcntl.h>, nel modo seguente:

fcntl(sd,F_SETFL,O_NONBLOCK);

//sd è il socket precedentemente aperto;
//F_SETFL significa che si vuole impostare il file status flag
//         al valore dall'ultimo parametro;
//O_NONBLOCK costante corrispondente al valore che significa non bloccante.

Ritorna -1 se c'è qualche errore (comunemente socket non aperto) oppure un valore che dipende dal tipo di operazione richiesto.

Anche il numero e la natura dei parametri varia secondo il tipo di utilizzo che può essere molteplice; per i dettagli si rimanda alla consultazione del manuale in linea della funzione.

2.4   Indirizzi dei socket

La creazione di un socket serve solo ad allocare nel kernel le strutture necessarie (in particolare nella file table) e a indicare il tipo di protocollo da usare; non viene specificato niente circa gli indirizzi dei processi coinvolti nella comunicazione.

Tali indirizzi si indicano con altre funzioni, utili alla effettiva realizzazione del collegamento, mediante l'uso di apposite strutture dati.

2.4.1   Struttura indirizzi generica

Siccome le funzioni di gestione dei socket sono concepite per funzionare con tutti i tipi di indirizzi presenti nelle varie famiglie di protocolli, è stata definita in <sys/socket.h> una struttura generica per gli indirizzi dei socket:

struct  sockaddr {
        sa_family_t     sa_family;      // famiglia di indirizzi
        char            sa_data[14];    // indirizzo 
        };

Le funzioni che usano gli indirizzi hanno nel prototipo un puntatore a questa struttura; quando si richiamano occorre fare il casting del puntatore effettivo per il protocollo specifico utilizzato.

I nomi delle strutture dati associate ai vari protocolli hanno la forma sockaddr_ seguito da un suffisso che dipende dal protocollo.

Tutte queste strutture, e anche quella generica, usano dei tipi dati standard (POSIX) che sono descritti in tabella 2.2 dove è indicato anche l'header di definizione:

Tabella 2.2

tipo dato descrizione header
int8_t intero a 8 bit con segno <sys/types.h>
uint8_t intero a 8 bit senza segno <sys/types.h>
int16_t intero a 16 bit con segno <sys/types.h>
uint16_t intero a 16 bit senza segno <sys/types.h>
int32_t intero a 32 bit con segno <sys/types.h>
uint32_t intero a 32 bit senza segno <sys/types.h>
sa_family_t famiglia degli indirizzi <sys/socket.h>
socklen_t lunghezza ind. (uint32_t) <sys/types.h>
in_addr_t indirizzo IPv4 (uint32_t) <netinet/in.h>
in_port_t porta TCP o UDP (uint16_t) <netinet/in.h>

In queste dispense vediamo solo le strutture relative agli indirizzi IPv4, IPv6 e locali.

2.4.2   Struttura indirizzi IPv4

La struttura indirizzi IPv4 è definita in <netinet/in.h> nel modo seguente:

struct  sockaddr_in {
        sa_family_t     sin_family;     // famiglia, deve essere = AF_INET
        in_port_t       sin_port;       // porta
     struct in_addr     sin_addr;       // indirizzo IP 
        unsigned char   sin_zero[8]     // per avere stessa dim. di sockaddr
        };

con la struttura in_addr così definita:

struct  in_addr {
        u_int32_t       s_addr;         // indirizzo IPv4 di 32 bit
        };

Il campo sin_zero è inserito affinché sockaddr_in abbia la stessa dimensione di sockaddr e deve essere valorizzato con zeri con la funzione memset(), definita in <string.h> e il cui prototipo è il seguente:

void    *memset(void *s, int c, size_t n);

La funzione riempie i primi n byte dell'area di memoria puntata da s con il byte c.

2.4.3   Struttura indirizzi IPv6

La struttura indirizzi IPv6 è definita in <netinet/in.h> nel modo seguente:

struct  sockaddr_in6 { 
        sa_family_t     sin6_family;   // famiglia, deve essere = AF_INET6  
        in_port_t       sin6_port;     // porta 
        uint32_t        sin6_flowinfo; // informazioni sul flusso IPv6   
    struct in6_addr     sin6_addr;     // indirizzo IP  
        uint32_t        sin6_scope_id; // id di scope   
        }; 

con la struttura in6_addr così definita:

struct  in6_addr { 
        uint8_t       s6_addr[16];   // indirizzo IPv6 di 128 bit
        }; 

Il campo sin6_flowinfo è relativo a certi campi specifici dell'intestazione dei pacchetti IPv6 e il suo uso è sperimentale.

Il campo sin6_scope_id è stato introdotto in Linux con il kernel 2.4, e serve per gestire il multicasting.

La struttura sockaddr_in6 è più grande di sockaddr quindi non serve il riempimento con zeri necessario invece in sockaddr_in.

2.4.4   Struttura indirizzi locali

La struttura indirizzi locali è definita in <sys/un.h> nel modo seguente:

#define UNIX_PATH_MAX    108 
struct  sockaddr_un { 
        sa_family_t  sun_family;              // famiglia, deve essere = AF_UNIX  
        char         sun_path[UNIX_PATH_MAX]; // nome percorso 
        }; 

Il campo sun_path contiene l'indirizzo che può essere:

2.5   Conversione dei valori numerici per la rete

Gli indirizzi IP e i numeri di porta devono essere specificati nel formato chiamato network order, che corrisponde al big endian e che si contrappone al little endian.

2.5.1   Ordinamento big endian e little endian

Nel caso di posizioni di memoria costituite da più byte (ad esempio di 16 bit) l'ordine con cui i diversi byte di una stessa posizione sono memorizzati dipende dall'architettura del computer.

I due ordinamenti più diffusi sono:

Da quanto detto emerge che nel caso di big endian il byte più significativo (MSB Most Significant Byte) ha l'indirizzo di memoria più piccolo, mentre nel caso di little endian è il byte meno significativo (LSB Least Significant Byte) ad avere l'indirizzo più piccolo.

Ad esempio se si deve memorizzare il dato 'AB' a partire dall'indirizzo 100, avremo, nel caso di big endian:

indirizzo 100: A
indirizzo 101: B

invece, nel caso di little endian:

indirizzo 100: B
indirizzo 101: A

I termini big endian e little endian derivano dai Lillipuziani dei "Viaggi di Gulliver", il cui problema principale era se le uova debbano essere aperte dal lato grande (big endian) o da quello piccolo (little endian); il significato di questa analogia è ovvio: nessuno dei due metodi è migliore dell'altro.

Esiste comunque un problema di compatibilità noto come NUXI problem dovuto al fatto che i processori Intel usano il metodo little endian e quelli Motorola il metodo big endian; si dice anche che hanno endianess deverse.

Il termine NUXI deriva dall'aspetto che avrebbe la parola UNIX se memorizzata in due posizioni consecutive di due byte in little endian.

Il seguente programma può essere utilizzato per verificare la endianess della propria macchina:(1)

#include <stdio.h>
#include <sys/types.h>
int main(void) {
    union {
       long lungo;
       char ch[sizeof(long)];
    } unione;
    unione.lungo = 1;
    if (unione.ch[sizeof(long)-1] == 1) 
       printf("big endian\n");
    else
       printf("little endian\n");
    return (0);
}

2.5.2   Funzioni di conversione di indirizzi e porte

Da quanto detto emerge la necessità di convertire i valori numerici corrispondenti agli indirizzi e alle porte in network order o formato rete cioè big endian se stiamo lavorando su una piattaforma che usa come formato macchina il little endian.

Le funzioni da utilizzare sono le seguenti (definite anche sulle piattaforme che usano big endian ma, ovviamente, vuote):

unsigned long int htonl(unsigned long int hostlong)

//converte l'intero a 32 bit hostlong dal formato macchina al formato rete;

unsigned short int htons(unsigned short int hostshort)

//converte l'intero a 16 bit hostshort dal formato macchina al formato rete;

unsigned long int ntohl(unsigned long int netlong)

//converte l'intero a 32 bit netlong dal formato rete al formato macchina;

unsigned short int ntohs(unsigned short int netshort)

//converte l'intero a 16 bit netshort dal formato rete al formato macchina. 

Tutte le funzioni ritornano il valore convertito, e non sono previsti errori.

2.5.3   Conversione del formato degli indirizzi

Esistono anche delle funzioni per convertire gli indirizzi dal formato rete binario al dotted decimal, o decimale puntato, caratteristico degli indirizzi IPv4; tali funzioni sono definite in <arpa/inet.h> e i seguenti sono i loro prototipi:

in_addr_t inet_addr(const char *strptr)

//converte la stringa strptr decimale puntata nel numero IP in formato rete;

int inet_aton(const char *src, struct in_addr *dest)

//converte la stringa src decimale puntata in un indirizzo IP;

char *inet_ntoa(struct in_addr ind)

//converte l'indirizzo IP ind in una stringa decimale puntata. 

L'uso della prima funzione è deprecato a favore della seconda.

La funzione inet_aton() converte la stringa src nell'indirizzo binario dest in formato rete e restituisce 0 in caso di successo e 1 in caso di errore; può anche essere usata con il parametro dest pari a NULL nel qual caso controlla se l'indirizzo è valido (valore di ritorno 0) oppure no (valore di ritorno 1).

La funzione inet_ntoa() converte l'indirizzo addrptr, espresso in formato rete, nella stringa in formato decimale puntato che è il valore di ritorno della funzione stessa.

Le funzioni appena illustrate operano solo con indirizzi IPv4 mentre ce ne sono altre che possono gestire anche indirizzi IPv6:

int inet_pton(int af, const char *src, void *addr_ptr)

//converte l'indirizzo stringa src nel valore numerico in formato
//    rete restituito all'indirizzo puntato da addr_ptr;

char *inet_ntop(int af, const void *addr_ptr, char *dest, size_t len)

//converte l'indirizzo puntato da addr_ptr dal formato rete in una stringa
//    puntata da dest che deve essere non nullo e di lunghezza coerente con il
//    tipo di indirizzo (si possono usare le costanti INET_ADDRSTRLEN per IPv4
//    e INET6_ADDRSTRLEN per IPv6); con tale lunghezza deve anche essere
//    valorizzato il parametro len.

Le lettere «n» (numeric) e «p» (presentation) nei nomi delle funzioni servono a ricordare il tipo di conversione fatta.

In entrambe c'è il parametro af che indica il tipo di indirizzo, e quindi può essere AF_INET o AF_INET6.

La prima funzione ritorna un valore positivo in caso di successo, nullo se l'indirizzo da convertire non è valido e negativo se il tipo di indirizzo af non è valido (in tal caso errno vale ENOAFSUPPORT).

La seconda funzione ritorna un puntatore alla stringa convertita in caso di successo oppure NULL in caso di errore con errno che allora vale:

Per entrambe le funzioni gli indirizzi vengono convertiti usando le rispettive strutture (in_addr per IPv4 e in6_addr per IPv6)) da preallocare e passare con il puntatore addr_ptr.

2.6   Uso della funzione ioctl() con i socket

La funzione ioctl() viene utilizzata per gestire i dispositivi quando non sono sufficienti le comuni funzioni di gestione dei file (si ricorda che in GNU/Linux e Unix i dispositivi sono visti come file dal sistema).

L'uso della ioctl() diviene necessario quando si devono gestire caratteristiche specifiche dell'hardware; la sua sintassi dipende dal tipo di dispositivo e varia anche in funzione del sistema ospite introducendo problemi di portabilità per i programmi fra versioni diverse di Unix.

In questa sede ci limitiamo a vederne l'utilizzo in GNU/Linux per accedere a informazioni associate ad un socket di rete: in particolare per reperire la lista delle interfacce di rete della macchina con rispettivi indirizzi IP, MAC, NETMASK e BROADCAST.

La funzione ioctl(), definita in <ioctl.h>, in questo contesto ha il seguente prototipo:

int ioctl(int sd, int ric, struct xxx *p);

//sd è il socket creato in precedenza;
//ric è il tipo di operazione richiesta;
//ifc p è il puntatore a una struttura apposita 
//        che dipende dal tipo di richiesta. 

Ritorna 0 in caso di successo oppure -1 se c'è qualche errore.

Per interrogare il socket circa le interfacce presenti sulla macchina si usa la funzione nel modo seguente:

int ioctl(int sd, SIOCGIFCONF, struct ifconf *ifc);

La struttura ifconf è definita in <net/if.h> nel modo seguente:

struct ifconf {
    int    ifc_len;              // grandezza del buffer per i dati
    union {
        __caddr_t ifcu_buf;     // è un puntatore a carattere
        struct ifreq *ifcu_req;
    } ifc_ifcu;
};
#define  ifc_buf    ifc_ifcu.ifcu_buf // puntatore al buffer
#define  ifc_req    ifc_ifcu.ifcu_req // struttura alternativa
                                      // per accedere al buffer

Anche la struttura ifcreq è definita in <net/if.h> in questo modo:

struct ifreq {
#define IFHWADDRLEN     6
#define IFNAMSIZ        16
    union {
      char ifrn_name[IFNAMSIZ];  // nome dell'interfaccia di rete
    } ifr_ifrn;
    union {
      struct sockaddr ifru_addr;
      struct sockaddr ifru_dstaddr;
      struct sockaddr ifru_broadaddr;
      struct sockaddr ifru_netmask;
      struct sockaddr ifru_hwaddr;
      short int ifru_flags;
      int ifru_ivalue;
      int ifru_mtu;
      struct ifmap ifru_map;
      char ifru_slave[IFNAMSIZ];
      char ifru_newname[IFNAMSIZ];
      __caddr_t ifru_data;
    } ifr_ifru;
};
// Ci sono quindi due campi: uno per il nome dell'interfaccia,
// l'altro per il parametro di ritorno.
// Per accedere ai campi si possono utilizzare le seguenti macro:
#define ifr_name         ifr_ifrn.ifrn_name      // nome interfaccia
#define ifr_hwaddr       ifr_ifru.ifru_hwaddr    // indirizzo MAC
#define ifr_addr         ifr_ifru.ifru_addr      // indirizzo IP
#define ifr_dstaddr      ifr_ifru.ifru_dstaddr\011 // other end of p-p link
#define ifr_broadaddr    ifr_ifru.ifru_broadaddr // ind. BROADCAST
#define ifr_netmask      ifr_ifru.ifru_netmask   // NETMASK
#define ifr_flags        ifr_ifru.ifru_flags     // flags
#define ifr_metric       ifr_ifru.ifru_ivalue    // metric
#define ifr_mtu          ifr_ifru.ifru_mtu       // mtu
#define ifr_map          ifr_ifru.ifru_map       // device map
#define ifr_slave        ifr_ifru.ifru_slave     // slave device
#define ifr_data         ifr_ifru.ifru_data      // for use by interface
#define ifr_ifindex      ifr_ifru.ifru_ivalue    // interface index
#define ifr_bandwidth    ifr_ifru.ifru_ivalue    // link bandwidth
#define ifr_qlen         ifr_ifru.ifru_ivalue    // queue length
#define ifr_newname      ifr_ifru.ifru_newname   // new name

Per usare la funzione si definisce un area dati abbastanza ampia (ad esempio 1024 byte), si inserisce la sua lunghezza in ifc_len e il puntatore ad essa in ifc_buf; una volta invocata la funzione, in assenza di errori, abbiamo in ifc_len la dimensione dei dati e nell'area puntata da ifc_buf i nomi delle varie interfacce.

Per acquisire le caratteristiche di una qualche interfaccia di rete si deve poi utilizzare la funzione ioctl() con la richiesta opportuna e passando come parametro la struttura ifreq ottenuta dall'invocazione con richiesta SIOCGIFCONF.

Ad esempio:

int ioctl(int sd, SIOCGIFADDR, struct ifreq *ifr);
int ioctl(int sd, SIOCGIFBRDADDR, struct ifreq *ifr);
int ioctl(int sd, SIOCGIFNETMASK, struct ifreq *ifr);
int ioctl(int sd, SIOCGIFHWADDR, struct ifreq *ifr);

servono ad ottenere in ifr rispettivamente:

I nomi delle richieste usate iniziano tutti con SIOCGIF che è l'acronimo di Socket Input Output Control Get InterFace; esistono anche analoghe richieste per il settaggio delle proprietà delle interfacce di rete il cui nome inizia con per SIOCSIF ( Socket Input Output Control Set InterFace).

Un esempio completo di utilizzo di queste funzioni si trova nel paragrafo 6.4.


1) una copia di questo file, dovrebbe essere disponibile anche qui: <allegati/progr-socket/esempio-01.c>.


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome i_socket_di_rete.html

[successivo] [precedente] [inizio] [fine] [indice generale]

Valid ISO-HTML!

CSS validator!