[successivo] [precedente] [inizio] [fine] [indice generale]
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).
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.
|
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>.
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:
EPROTONOSUPPORT: socket o protocollo non supportato nel dominio;
EACCES: mancanza di privilegi per creare il socket;
EINVAL: protocollo sconosciuto o dominio non disponibile;
ENOBUFS o ENOMEM: memoria non sufficiente per la creazione del socket.
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:
domain: famiglia dei protocolli da usare, con i seguenti valori possibili (solo i più utili per gli scopi di queste dispense):
PF_UNIX o PF_LOCAL: comunicazioni locali;
PF_INET: comunicazioni con protocollo IPv4;
PF_INET6: comunicazioni con protocollo IPv6.
type: stile di comunicazione identificato dai seguenti valori:
SOCK_STREAM: comunicazione bidirezionale, sequenziale, affidabile, in conessione con un altro socket; dati inviati e ricevuti come flusso di byte;
SOCK_SEQPACKET: come SOCK_STREAM ma con dati suddivisi in pacchetti di dimensione massima prefissata;
SOCK_DGRAM: pacchetti (datagram) di lunghezza massima prefissata, indirizzati singolarmente, senza connessione e in modo non affidabile;
SOCK_RDM: comunicazione affidabile ma senza garanzia sull'ordine di arrivo dei pacchetti.
protocol: deve valere sempre zero.
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).
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.
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.
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:
|
In queste dispense vediamo solo le strutture relative agli indirizzi IPv4, IPv6 e locali.
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.
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.
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:
un file nel filesystem il cui nome, completo di percorso, è specificato come una stringa terminata da uno zero;
una stringa univoca che inizia con zero e con i restanti byte, senza terminazione, che rappresentano il nome.
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.
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:
big endian o big end first: in questo caso le posizioni di memoria sono occupate a partire dal byte più a sinistra del dato, quindi dal più significativo;
little endian o little end first: in questo caso le posizioni di memoria sono occupate a partire dal byte più a destra del dato, quindi dal meno significativo.
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)
|
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.
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:
ENOSPC: le dimensioni della stringa sono maggiori di len;
ENOAFSUPPORT: il tipo di indirizzo af non è valido.
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.
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:
indirizzo IP;
indirizzo BROADCAST;
NETMASK;
indirizzo MAC.
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