Swappa : Uni / Socket Library
Creative Commons License

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

  1. Introduzione
  2. Come avviene la comunicazione
  3. Creare un socket
  4. Codifica degli indirizzi
  5. Funzione bind
  6. Creazione della connessione
  7. Funzione connect
  8. Funzione listen
  9. Funzione accept
  10. Gestione delle richieste
  11. Chiusura del socket
  12. Funzione select
  13. Funzione send
  14. Funzione recv

Introduzione

In soldoni, perché due processi residenti su macchine diverse possano comunicare tra loro, è necessario:

  1. verificare che le macchine siano in rete
  2. creare un canale di comunicazione

I socket sono i due capi di tale canale, che forniscono l'interfaccia tra i programmi applicativi e il livello di trasporto (TCP, UDP, ...).

Torna su

Come avviene la comunicazione

Cosa siano client e server lo sappiamo tutti (no? Leggi qua), come comunichino (in generale) tramite socket è presto detto:

  1. ho un programma X che gira su un server, ed è associato a una certa porta tramite il socket "s1"
  2. il server continua ad ascoltare su "s1", aspettando che qualche client richieda la connessione
  3. quando questa arriva, se è tutto ok il server accetta la connessione e crea un nuovo socket "s2" su una nuova porta
  4. il servizio chiamato dal client viene gestito dal server sul socket "s2" (che verrà poi chiuso una volta soddisfatta la richiesta)
  5. mentre viene eseguito il punto 4, il server è tornato ad ascoltare su "s1" eventuali chiamate da altri client.

Torna su

Creare un socket

La funzione necessaria per creare un socket è, manco a dirlo, socket.

sockid = socket (pf, type, protocol)

, dove:

Nota: da notare che non tutte le combinazioni "famiglia di protocolli"-"tipo di socket" sono valide (ad esempio la PF_UNIX non ha SOCK_RAW).

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);

Torna su

Codifica degli indirizzi

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.

Torna su

Funzione bind

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.

Torna su

Creazione della connessione

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:

  1. (lato passivo) Il server esegue l'apertura passiva del socket, attraverso le funzioni socket, bind e listen;
  2. (lato attivo) Il client richiede l'inizio della connessione (apertura attiva) usando la funzione connect;
  3. (lato passivo) Il server accetta la connessione tramite la funzione accept e la sposta su un nuovo socket;
  4. (lato passivo e attivo) Invio e ricezione dei dati.

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.

Torna su

Funzione connect

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.

Torna su

Funzione listen

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.

Torna su

Funzione accept

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:

  1. la accept estrae da sockid la prima richiesta di connessione che attende in coda. Se non sono presenti, si blocca finché non ne arriva una;
  2. la chiamata della funzione viene completata riempendo l'argomento addr con l'indirizzo del client richiedente e l'argomento addrlen calcolato di conseguenza;
  3. viene creato un nuovo socket con le stesse caratteristiche di sockid, la cui destinazione è connessa al client chiamante. Il descrittore del nuovo socket è quello restituito dalla accept;
  4. viene ritornato il descrittore del nuovo socket al client richiedente.

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.

Torna su

Gestione delle richieste

Una volta accettata una connessione, il server ha due approcci diversi per gestire le richieste:

Torna su

Chiusura del socket

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:

  1. se più di un processo ha lo stesso socket aperto, questo non verrà chiuso finché non sarà invocata una close su ognuno di essi;
  2. se un processo termina per una qualsiasi ragione, tutti i socket che utilizzava saranno chiusi automaticamente dal sistema;
  3. una volta chiuso il socket, la porta che utilizzava diventa di nuovo disponibile.

Torna su

Funzione select

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.

Torna su

Funzione send

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.

Torna su

Funzione recv

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 su


Torna alla pagina di Sistemi per l'elaborazione delle informazioni

(Printable View of http://www.swappa.it/wiki/Uni/SocketLibrary)