Torna alla pagina di Sistemi per l'elaborazione delle informazioni
:: Socket Library ::
Data la noiosità della materia trattata, l'esposizione dei contenuti sarà intervallata ad immagini di donne in pose discinte (uomini solo a richiesta).
Indice
In soldoni, perché due processi residenti su macchine diverse possano comunicare tra loro, è necessario:
I socket sono i due capi di tale canale, che forniscono l'interfaccia tra i programmi applicativi e il livello di trasporto (TCP, UDP, ...).
Cosa siano client e server lo sappiamo tutti (no? Leggi qua), come comunichino (in generale) tramite socket è presto detto:
La funzione necessaria per creare un socket è, manco a dirlo, socket.
sockid = socket (pf, type, protocol)
, dove:
La funzione socket ritorna un intero positivo in caso di successo (che diventerà il descrittore del socket) e -1 in caso di errore.
Esempio: sd = socket (AF_INET, SOCK_STREAM, 0);
Avrete notato che la funzione socket si disinteressa completamente degli indirizzi che identificano i due punti finali della comunicazione, occupandosi solo di specificare il tipo di famiglia dei protocolli da utilizzare. E' solo attraverso le altre funzioni di gestione del socket (bind, listen, ...) che vengono manipolati gli indirizzi, sotto forma di strutture (passate sempre per riferimento, tramite puntatori).
La struttura generica è la seguente:
struct sockaddr { u_short sa_family; // Address Family char sa_data[14]; // Indirizzo (la cui forma dipende dall'AF scelta) };
Va da sé che ogni famiglia di protocolli implementerà in modo diverso questa struttura generica, dal momento che ognuna di esse ha forme di indirizzamento specifiche e distinte. I nomi di tutte queste strutture iniziano per sockaddr_ più un suffisso finale che indica la famiglia di appartenenza.
Vediamo ad esempio la sockaddr_in, ovvero la sockaddr specifica per IP.
struct sockaddr_in { short sin_family; // Valore: AF_INET u_short sin_port; // Numero di porta. Valore: 0 - 65535 struct in_addr sin_addr; // sin_addr è l'indirizzo IP char sin_zero[8]; // inutilizzato };
Importante sottolineare come i bit degli indirizzi e dei numeri di porta possano essere ordinati in modo diverso a seconda dell'hardware degli host. Si parla di ordinamento big-endian quando si parte dai bit più significativi (adottato ad esempio da IBM, Sun, Motorola), little-endian quando parte da quelli meno significativi (es. Intel). Questo comporta la necessità di eseguire delle apposite routine di verifica dell'ordinamento ed eventuale riconversione per garantire la portabilità del codice.
Un'altra cosa da ricordare è che se specifico sin_port = 0, il sistema mi troverà la prima porta disponibile in automatico.
Abbiamo visto che nel momento della loro creazione i socket non sono associati a nessun indirizzo (IP o di porta). Se lato client non è sempre necessario conoscere l'indirizzo locale, lato server è spesso importante specificare la porta in ascolto di connessione. Ecco dunque lo scopo della funzione bind: assegnare un indirizzo locale ad un socket.
status = bind (sockid, &localaddr, addrlen)
, dove:
La funzione bind restituisce 0 in caso di successo e -1 in caso di errore.
Introduciamo le prossime funzioni di gestione del socket descrivendo le varie fasi di connessione che caratterizzano il protocollo TCP. Le due figure che entrano in gioco sono il partecipante passivo (un server), in fiduciosa attesa che qualcuno gli richieda la connessione, e un partecipante attivo (il client) che effettua tale richiesta.
Ecco in pratica cosa succede:
Da notare che tutto ciò accade solo su quei socket che supportano la connessione, come ad esempio SOCK_STREAM. Per il SOCK_DGRAM (tipico delle UDP), ad esempio, non accade nulla di tutto questo.
Se la bind permetteva a un server di assegnare una porta ad un socket, la connect è la funzione che consente ad un client di collegarsi a quella porta specifica, ponendo un socket nello stato connesso.
status = connect (sockid, &destaddr, addrlen)
, dove:
La funzione connect restituisce 0 in caso di successo e -1 in caso di errore.
La connect è inoltre bloccante, ovvero ritorna solo quando la connessione è stabilita o quando si è verificato un errore. Se ciò non avviene, il codice non va avanti.
Una volta che il socket è stato creato e assegnato a una porta locale, al server per completare la procedura di apertura passiva non rimane altro che eseguire la funzione listen, che mette il socket in modalità passiva e specifica una lunghezza della coda per le richieste di connessione.
status = listen (sockid, queuelen)
, dove:
La funzione listen restituisce 0 in caso di successo e -1 in caso di errore.
La listen è non-bloccante, ovvero ritorna subito, e può essere usata solo per socket che supportano le connessioni.
Completata l'apertura passiva, grazie alla funzione accept il server blocca tutto e si mette in attesa di richieste di connessione.
s = accept (sockid, &addr, &addrlen)
, dove:
La funzione accept restituisce un intero positivo in caso di successo (il descrittore del nuovo socket) in caso di successo e -1 in caso di errore.
Vediamo passo passo cosa succede:
Mentre vengono eseguiti i punti 3 e 4, il socket sockid originale non viene toccato e resta nello stato di listen.
La funzione accept, lo abbiamo visto, è decisamente bloccante.
Una volta accettata una connessione, il server ha due approcci diversi per gestire le richieste:
Abbiamo parlato più di una volta di chiusura del socket senza spiegare come si fa. E' presto detto:
status = close (sockid)
, dove:
La funzione close restituisce 0 in caso di successo e -1 in caso di errore.
Tre precisazioni:
Se per programmi semplici il fatto di avere funzioni bloccanti (come la connect o la accept) può essere utili, per quelli più complessi aumentano i problemi. Ad esempio, come saranno gestite le connessioni multiple? E cosa succederà nel caso di connessioni simultanee? Si finirà irrimediabilmente per saturare la coda dei client in attesa, continuando a scartare nuove richieste.
Un sistema per risolvere il problema l'abbiamo già visto con la fork() e quindi con la gestione simultanea delle richieste. Un altro è fare in modo che uno stesso processo possa attendere delle connessioni su più di un socket. Questo sistema avviene utilizzando la funzione select.
status = select (nfds, &readfds, &writefds, &exceptfds, &timeout)
, dove:
La funzione select restituisce i descrittori dell'insieme dei descrittori dei socket che sono pronti, o -1 in caso di errore.
Generalmente viene usata quando il numero dei socket da gestire è basso.
Ora che sappiamo come creare e gestire le connessioni, vogliamo anche farci qualcosa. Per inviare dati posso utilizzare la funzione send.
count = send (sockid, &buf, len, flags)
, dove:
La funzione send restituisce il numero dei byte trasmessi, o -1 in caso di errore.
La send è bloccante, ovvero non ritorna finché non vengono immessi dei dati nel buffer.
A che serve mandare dati con la send se poi non abbiamo modo di riceverli? Ecco a cosa serve la recv.
count = recv (sockid, &buf, len, flags)
, dove:
La funzione recv restituisce il numero dei byte ricevuti, o -1 in caso di errore.
La recv è bloccante, ovvero non ritorna finché non vengono ricevuti dei dati.
Da notare infine che sia la send che la recv sono pensate per socket che supportano le connessioni. I SOCK_DGRAM, ad esempio, utilizzeranno altre funzioni come ad esempio la sendto e la recvto in cui verranno specificati anche gli indirizzi di destinazione/partenza.
Torna alla pagina di Sistemi per l'elaborazione delle informazioni