Sei sulla pagina 1di 30

Guida all' uso dei sockets nella programmazione in C

( pseudo-traduzione personalizzata di "Beej's Guide to Network Programming" )

(Prima parte)

INTRODUZIONE

Finalmente ho trovato una guida chiara e semplice sui sockets in C.


L' ho cercata molto: ho sguazzato per la rete alla ricerca di testi pi o meno specifici e alla
fine ho scelto questa. Il testo in questione la "Beej' s Guide to Network Programming"
( che un riassunto dei librri pi dettagliati di Ritchard Stevens sui sockets sotto UNIX ).
Non sicuramente una guida completa ai sockets, ma un buon inizio per tutti.
Perch ho tradotto questo testo? Sicuramente per me stesso visto che di facile
consultazione ( nel caso mi dimenticassi qualche cosa (e succede spessissimo ))... e poi
anche per condividerla con voi, dato che penso possa essere uno spunto per tutti.
La traduzione non letterale: in alcuni punti ho cancellato frasi inutili per cercare di
arrivare al succo del discorso e altri punti li ho completamente stravolti aggiungendo
spiegazioni personali che ho ritenuto essere pi chiare... in ogni caso la guida non
ancora completa, perch ho intenzione di scrivere un tutorial che pu essere considerato
l'appendice di questo... Il progetto sarebbe quello di collezionare dei programmi trovati in
rete o scritti da me e commentarli dettagliatamente... Comunque vedremo pi avanti che
cosa verr fuori.
Ritornando a questo tutorial, devo avvisarvi che non troverete l' argomento "ClientServer"... Giuro che l'avrete tra un po'... in fondo questa solo la prima parte.
La guida si rivolge a tutti quelii che hanno anche una piccola base di C. L' unica cosa
importante che abbiate bene in mente come funziona una "struttura" e come
"manipolare" i suoi elementi attraverso i puntatori. Avendo queste basi, potrete seguire
senza problemi ci che descritto in questo testo. La programmazione viene fatta sotto
Linux...
Per adesso vi auguro una buona lettura e se volete fare qualche critica o chiedere
qualche cosa potete scrivere a psyco@hackeralliance.net

CIao...

P.S.: nel caso abbiate trovato la guida di Beej gi tradotta in italiano... vi prego di non
dirmelo...

-----------------------------------------------------------------------------P5yc[0]--------------------------

1-- COS' E' UN SOCKET?

Sicuramente avrete sentito parlare di sockets e vi interesserebbe conoscere qualcosa in


pi.
I sockets sono un "modo" per comunicare con altri programmi, usando dei files
"descrittori" standard di UNIX ( ricordiamo che in questo testo faremo riferimento solo a
sistemi Unix-Like ).
Alcuni sostengono che qualsiasi cosa, in UNIX, pu essere considerato un file. Quello che
si vuol dire il fatto che quando un programma sotto UNIX fa un qualsialsi lavoro di I/O, lo
fa leggendo o scrivendo su un file detto "descrittore" ( file descriptor ). Questo file pu
essere una connessione via internet o un terminale o un file su disco o una pipe o
qualsiasi altra cosa: insomma qualsiasi cosa considerato un file! Quindi, quando
vogliamo "comunicare" con altri programmi, anche attraverso la rete, lo facciamo
attraverso un "file descriptor".

"Dove si trova questo file descriptor?" forse l' ultima domanda da porsi in questo
momento, ma la risposta : "Bisogna fare una chiamata alla routine di sistema... questa
chiamata la socket().

Questo restituisce il socket descriptor e noi comunicheremo, attraverso di esso, usando le


"socket calls" send() e recv() ( digitare i comandi "man send" e "man recv" per ulteriori
informazioni ).

Potreste dire:"Hey, ma, se si tratta di un file descrittore, perch, per mille balene, non
posso usare read() e write() per comunicare attraverso i sockets? la risposta secca
sarebbe: "Puoi farlo"... quella dettagliata :"Puoi farlo, ma send() e recv() offrono un
maggiore controllo sulle vostre trasmissioni di dati.

Ci sono molti tipi di sockets, ma noi ci occuperemo dei DARPA INTERNET ADDRESS
( Internet sockets ).

2-- DUE TIPI DI INTERNET SOCKETS

Studieremo solo due dei molti tipi di sockets esistenti, anche se ci sono i RAW SOCKETS
che sono anch' essi molto importanti e andrebbero studiati.
I due tipi di sockets di cui ci occuperemo sono: gli STREAM SOCKETS e i DATAGRAM
SOCKETS che, nel seguito, chiameremo, rispettivamente, SOCK_STREAM e
SOCK_DGRAM.

I "datagram sockets" sono spesso chiamati "connectionless sockets" ( letteralmente:


sockets senza connessione ) ( anche se, comunque, possibile connetterli... vedremo in
seguito ).

Attraverso gli "stream sockets", i dati arrivano in ordine e senza errori. Ad esempio, se
dobbiamo trasmettere un "1,2", con gli stream sockets saremo sicuri del fatto che
arriveranno a destinazione nel giusto ordine e liberi da errore... cio arriveranno nell'
ordine "1,2".

Da quali applicazioni sono utilizzati gli "Stream Sockets"?


Avete mai sentito parlare di Telnet? Telnet li usa: tutti i caratteri che vengono digitati,
devono poter arrivare nello stesso ordine.

Un altro esempio pu essere il Browser: questo utilizza, infatti, il protocollo HTTP che fa
uso degli stream sockets.

Come fanno gli stream sockets a raggiungere questo alto livello di qualit? Usano un
protocollo chiamato "Trasmission Control Protocol" conosciuto anche come TCP ( per i
dettagli sul protocollo TCP, fare riferimento alla RFC-793 ). TCP fa si che i vostri dati
arrivino sequenzialmente e senza errori. Inoltre, sicuramente, avrete sentito parlare di
TCP/IP. "IP" sta per "Internet Protocol" ( vedi RFC-791 ). IP comunica per primo con l'
Internet routing e non , in generale, responsabile dell' integrit dei dati.
Bene. E per quanto riguardano i "Datagram Sockets"? Ci sono alcune cose da dire: se
viene mandato un datagram, possono verificarsi due casi:
1) il dato arriva
2) il dato non arriva
Se il dato arriva, i dati sono senza errori, ma non detto che arrivino in ordine.

I Datagram sockets usano l' "IP" per il routing, ma non usano "TCP": usano un altro
protocollo chiamato UDP ( "User Datagram Protocol" (RFC-768)).

Perch sono detti "connectionless"? Perch non serve mantenere una connessione
aperta come si fa con gli Stream sockets. Basta costruire un pacchetto con un IP header (
che abbia le informazioni riguardanti la destinazione ), e lo si "spedisce". Sono
solitamente utilizzati i trasferimenti di informazioni "packet by packet". Un' applicazione
pu essere il tftp.

Abbiamo detto abbastanza. Aggiungiamo solo che ogni pacchetto inviato con protocolllo
UDP deve ricevere un pacchetto "di avvenuta ricezione" chiamato "ACK packet". Questo
importante quando andiamo ad utilizzare i SOCK_DGRAM, perch rischiamo di mandare
un pacchetto UDP senza essere sicuri se, effettivamente, il pacchetto sia arrivato.

3-- NETWORKING

Vediamo come funzionano, brevemente, le reti e, in particolare, vediamo qualche


esempio sui SOCK_DGRAM.

( ETHERNET ( IP ( UDP ( TFTP ( DATA ) ) ) ) )


SCHEMA INCAPSULAMENTO DEI DATI

l' incapsulamento dei dati di fondamentale importanza. Funziona in questo modo ( fare
riferimento allo schema sopra riportato ):
Viene creato un pacchetto ( DATA ), viene messo nell' header del primo protocollo ( nel
nostro schema di esempio il primo protocollo TFTP ). Successivamente, tutto questo
viene incapsulato nel protocollo UDP ( ad esempio ) e il tutto, ancora, viene messo in IP.
E, infine, in ETHERNET ( o un altro protocollo hardware ).

Quando un altro computer riceve il pacchetto descritto sopra, l' hardware lo "spoglia" dell'
involucro pi esterno ( l' ethernet header ); Il kernel preleva gli header dell' IP e UDP;
TFTP preleva l' header di TFTP e, infine, il dato.

Modello di rete a livelli: questo modello descrive la funzionalit di un sistema di rete, che
ha molti vantaggi rispetto ad altri. Per esempio, con questo modello, si possono scrivere
programmi in cui non andremo a preoccuparci di come il dato verr trasmesso, in quanto
ci saranno altri programmi ( ai livelli pi bassi ) che se ne occuperanno. L' hardware e la
topologia del network trasparente al programmatore.

Ecco i livelli:

--Application
--Presentation
--Session
--Transport
--Network
--Data link
--Physical

Il livello "Physical" quello hardware ( seriale, Ethernet ).


Il livello "Application" il livello in cui l' utente interagisce con il network.

Questo descritto un modello generale.

Vediamo un modello un po' pi specifico in Unix:


I livelli, in questo caso sono:

Application ( telnet, ftp, ... )

Host - to - Host transparent layer ( TCP, UDP )

Internet Layer ( IP and routing )

Network Access Layer ( Ethernet, ACK, ecc. )

4-- STRUTTURE E MANIPOLAZIONE DEI DATI

Con questo capitolo, iniziamo la parte pratica: la programmazione.


Nelle pagine successive verranno considerati vari tipi di dati usati dall' interfaccia sockets.
Il tipo pi semplice int.

Da ricordare: ci sono due tipi di ordinamento di bytes: il primo prevede la presenza di un


byte pi significativo ( chiamato anche "ottetto" ) e uno meno significativo. Questo tipo di
ordinamento dei bytes chiamato "Network Byte Order" ( detto anche Big-Endian Byte
Order ). Ci sono macchine che utilizzano, in alternativa, l' Host Byte Order. Nel seguito,
quando diremo che "un qualcosa" deve essere messo in Netwok Byte Order, vorr dire
che dovrete chiamare una funzione ( come htons(), che vedremo dopo ) per passare da
"Host byte Order" in "Network Byte Order"... se non diremo nulla, il valore sar lasciato in
Host Byte Order.

La prima struttura -- struct sockaddr

Questa struttura contiene informazioni sui "socket addresses" per molti tipi di sockets:

struct sockaddr {
unsigned short sa_family;
char

\\ indirizzo della famiglia AF_xxx

sa_data[14]; \\ 14 bytes di protocoll address

};

-- sa_family pu avere valori differenti, ma in questo documento utilizzeremo solo


AF_INET.
-- sa_data contiene un indirizzo di destinazione e un numero di porta per il socket

Per comunicare con la "struct sockaddr", i programmatori hanno creato una struttura
parallela, pi chiara:

struct sockaddr_in {
short int sin_family;

\\ "in" sta per internet


\\ address family

unsigned short int sin_port; \\ port number


struct in_addr sin_addr;

\\ Internet address

unsigned char sin_zero[8]; \\ Stessa grandezza della struttura


};

Questa struttura permette di avere un accesso pi facilitato agli elementi della struttura
stessa. Importante il fatto che sin_zero ( che indica la grandezza della struttura ) deve
avere i suoi elementi ( quelli dell' array ) posti a zero quando utilizziamo la funzione
memset.

Altro fatto importante che un puntatore ad una "struct sockaddr_in" pu essere


modificato in "struct sockaddr" ( attraverso un casting ) e vice versa.

Da notare che:

-- sin_family corrisponde a sa_family della struct sockaddr e pu, quindi, essere posto a
"AF_INET".
-- sin_port e sin_addr devono essere in NETWORK BYTE ORDER.

5-- CONVERSIONE da HOST BYTE ORDER in NETWORK BYTE ORDER e vice-versa

Ci sono due tipi che possono essere convertiti: short ( 2 bytes ) e long ( 4 bytes ) ( anche
quando sono utilizzati con "unsigned"). Se vogliamo, ad esempio, convertire uno short da
"Host Byte Order" a "Network Byte Order". Si utilizza la funzione htons() ( h-to-n-s = hostto-network-short ).
Tutte le conversioni effettuabili sono le seguenti:

htons()

htonl()

ntohs()

ntohl()

In Unix, si deve tener ben presente che: prima di mandare i bytes di dati sul network, essi
devono essere convertiti in Network Byte Order.

Da ricordare: sin_family deve essere in Host Byte Order e pu rimanere in Host Byte
Order anche se non "mandata in rete".

6-- "IP ADDRESSES": COME COMUNICARE CON LORO

Fortunatamente, ci sono molte funzioni che permettono di manipolare gli indirizzi IP.
Immaginiamo di avere una struttura "struct sockaddr_in ina" e abbiamo l' indirizzo
10.12.110.57 che vogliamo posizionare nella struttura. La funzione da usare inet_addr
(), che converte l' indirizzo IP, formato da numeri e punti, in una unsigned long. L'
assegnazione pu essere scritta cos:

ina.sin_addr.s_addr = inet_addr("10.12.110.57");

E da dove viene fuori s_addr? Dal fatto che la struttura in_addr definita in questo modo:

struct in_addr {
unsigned long s_addr;
}

NOTA: inet_addr() restituisce gi l'indirizzo in Network Byte Order, quindi non c' bisogno
di utilizzare htonl() per convertirlo.

Il codice scritto [ ina.sin_addr.s_addr = inet_addr("10.12.110.57") ], non molto robusto


per il fatto che non viene effettuato nessun tipo di controllo degli errori. Quindi utile,
quando andremo ad utilizzare inet_addr(), assicurarsi anche di fare un cos detto "error
checking". A questo proposito importante ricordarsi che, in caso di errore, inet_addr()
restituisce -1.

C' un' interfaccia pi "pulita" di inet_addr() ed inet_aton() ( a-to-n = ascii to network ).


Ecco un esempio e gli headers che utilizza:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton( const char *cp, struct in_addr *inp );

Vediamo, a questo punto, un esempio su come "riempire i campi della struttura struct
sockaddr_in":

struct sockaddr_in my_addr;


my_addr.sin_family = AF_INET;

\\definisce myaddr come struttura di tipo sockaddr_in


\\Host Byte Order

my_addr.sin_port = htons(MYPORT); \\short Network Byte Order


inet_aton("10.12.110.57", &(my_addr.sin_addr)); \\l' abbiamo appena visto
memset (&(my_addr.sin_zero), '\0\, 8);

\\pone a zero il resto della struttura

inet_aton(), al contrario di ogni altra funzione socket, restituisce "non-zero" in caso sia
vera, zero se falsa. E l' indirizzo ripassato a inp.

Comunque, non tutte le piattaforme implementano inet_aton(). Quindi, sebbene sia


preferibile il suo utilizzo, in questa guida sar utilizzato inet_addr().

Adesso siamo riusciti a convertire stringhe di IP nella loro rappresentazione binaria. Nel
caso volessimo fare il contrario, cio se avessimo struct in_addr e volessimo trasformarlo
in una rappresentazione di "numeri e punti", dovremmo usare la funzione inet_ntoa() ( nto-a = network to ascii ), in questo modo:

printf("%s", inet_ntoa(ina.sin_addr));

questo stampa l'indirizzo IP.

NOTA: "inet_ntoa" prende una "struct in_addr" come argomento, non una long!! Da notare
anche che la funzione restituisce un puntatore ad un char. Questo punta ad un array di
caratteri attraverso inet_ntoa() cos che, ad ogni chiamata di inet_ntoa(), questo scriver
l'ultimo IP richiesto.

Ad esempio:

char *a1, *a2;


...
...
a1 = inet_ntoa( ina1.sin_addr ); \\facciamo finta che questo sia l'IP 65.123.7.23
a2 = inet_ntoa( ina2.sin_addr ); \\facciamo finta che questo sia l' IP 192.168.4.24
printf("address1: %s\n", a1);
printf("address2: %s\n", a2);

l'output sar:

address1: 192.168.4.24
address2: 192.168.4.24

Cio verr sovrascritto l' indirizzo precedente. Quindi, se vogliamo evitare che si "perda"
l'indirizzo "address1", potremmo usare la funzione strcpy() per scriverlo, magari, in un altro
array...

7-- CHIAMATE DI SISTEMA

Questa Sezione dedicata alle chiamate di sistema per accedere alla rete.

A) Richiamare il file descriptor

Il primo passo per permetterci di "comunicare" in rete, quello di chiamare la funzione


socket().
Vediamo subito di che cosa si tratta e che "headers" utilizza:

#include <sys/types.h>
#include <sys/sockets.h>

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

Ora cerchiamo di capire nel dettaglio gli argomenti della chiamata socket():

--domain: dovrebbe essere settato a "AF_INET", proprio come la struttura "struct


sockaddr_in"
--type: dice al kernel che tipo di socket voglio usare: SOCK_STREAM o SOCK_DGRAM (
sono quelli che utilizzeremo in questa mini guida ai sockets )
--protocol: si pone a "0" ( "zero" ) per far si che "socket()" scelga il corretto protocollo
basato sull' argomento "type"

NOTA: ci sono molti pi "types" di quelli che abbiamo elencato. Per avere una lista
completa lanciate il comando "man socket" dal vostro terminale preferito :). Inoltre, per
utilizzare i protocolli ci sono altri modi ( fate "man getprotobyname" per saperne di pi ).

Error checking: La chiamata socket() restituisce il "socket descriptor" che utilizzeremo


nelle prossime chiamate a sistema o restituisce -1 se ho un errore.

B) bind() -- Su che porta mi trovo?

Una volta che abbiamo un socket, potremmo associarlo ad una porta della macchina
locale.
Il numero della porta usato dal kernel per associare un pacchetto entrante ad un certo
processo del socket. Se vogliamo utilizzare solo un connect() ( verr spiegato pi avanti ),
allora non ci sar bisogno di aprire una porta...

Scriviamo brevemente come si utilizza bind() e i suoi "headers":

#include <sys/types.h>
#include <sys/socket.h>

int bind (int sockfd, struct sockaddr *my_addr, int addrlen);

Come prima, analizziamo gli argomenti di bind:

--sockfd: il socket descriptor restituito da "socket()".


--my_addr: un puntatore alla struttura "struct sockaddr", che contiene informazioni
come il nome del nostro indirizzo, la porta e indirizzo IP.
--addrlen: pu essere settato a sizeof(struct sockaddr).

Vediamo un esempio:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT 3490

main() {
int sockfd;
struct sockaddr_in my_addr;

sockfd = socket( AF_INET, SOCK_STREAM, 0); //aggiungere qualche "error checking"

my_addr.sin_family = AF_INET;

//Host Byte Order

my_addr.sin_port = htons(MYPORT);

// short Network Byte Order

my_addr.sin_addr.s_addr = inet_addr("10.12.110.37");

// mette a zero il resto della

stuttura

memset(&(my_addr.sin_zero), '\0', 8);

//non dimenticarsi degli "error checking"

bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));


...
...
...

Da notare:

--my_addr.sin_port in Network Byte Order


--my_addr.sin_addr.s_addr in Network Byte Order

Importante: gli headers, potrebbero cambiare da sistema a sistema. Per essere sicuri del
loro corretto utilizzo, visitate le pagine di manuale del vostro sistema *nix e controllate,
per ogni funzione che andate ad utilizzare, quali headers ci vogliono... Ad esempio se
volete sapere che "header files" utilizza la chamata "socket()", fate "man socket".

Un' ultima analisi: alcune volte, pu essere necessario far si che l'assegnazione del
proprio indirizzo IP e/o della porta, venga fatta automaticamente... per fare ci, nel listato
precedente, avremmo dovuto operare in questo modo:

my_addr.sin_port = 0; // sceglie in modo random una porta non utilizzata...


my_addr.sin_addr.s_addr = INADDR_ANY; // usa il nostro indirizzo IP

Quindi, ponendo my_addr.sin port a 0, facciamo in modo che bind() scelga la porta per
noi. E ancora, ponendo my_addr.sin_addr.s_addr a INADDR_ANY, faremo in modo che
bind() metta automaticamente l' indirizzo della macchina in cui sta girando il processo...

Perch INADDR_ANY non stato messo in Network Byte Order? Perch INADDR_ANY
gi di per se uno 0 ( zero )... e zero, anche in forma di bytes, rimane sempre zero. C' chi
dice che INADDR_ANY potrebbe essere un valore diverso da 0 e, dunque, questo codice
potrebbe non funzionare. Allora, a scanso di ogni equivoco, possiamo scrivere:

my_addr.sin_port = htons(0);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

Error checking: bind() restituisce -1in caso di errore e errno viene aggiornato al valore di
errore.

Altra nota importante: tutte le porte sotto la 1024 sono RISERVATE. Potete, invece,
utilizzare tutte le porte dalla 1024 alla 65535 ( a meno che non siano usate da qualche
altro programma ).

Alle volte potreste imbattervi in un errore del tipo "Address already in use"... questo
significa che, anche se la comunicazione attraverso il socket conclusa, il socket la sta
ancora utilizzando. Il problema si risolve da solo aspettando un po', oppure potreste
aggiungere al vostro programma il seguente codice:

int yes=-1; // Gli utenti del Solaris usano

char yes=-1;

if(setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int))


== -1){
perror("setsockopt");
exit(1);
}

Ultima nota: come abbiamo gi detto, se il nostro scopo solo quello di connetterci ad
una macchina remota e non ci interessa che porta andremo ad utilizzare sulla nostra
macchina, allora non sar necessario usare bind().

C) connect() -- Hey, tu!!!

Ohh finalmente ci connetteremo a qualche cosa...


Facciamo finta di essere un applicazione telnet. Il vostro utente vi comanda di "prelevare"
un socket. Allora chiamate la funzione socket(). Dopo, l' utente vi dice di connettervi a
"10.12.110.57" sulla porta "23". Come dovreste fare?

Lo spieghiamo subito con la descrizione di connect e dei sui headers:

#include <sys/types.h>
#include <sys/socket.h>

int connect ( int sockfd, struct sockaddr *serv_addr, int addrlen );

Ecco, in dettaglio, gli argomenti usati da connect():

--sockfd il socket "descrittore", restituito dalla chiamata socket()


--serv_addr una struct sockaddr contenente la destinazione,la porta e l' indirizzo della
macchina remota
--addrlen pu essere posto a sizeof(struct sockaddr)

Esempio:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define DEST_IP "10.12.110.57"


#dedfine DEST_PORT 23

main() {
int sockfd;
struct sockaddr_in dest_addr; // conterr l'indirizzo di destinazione

sockfd = socket(AF_INET, SOCK_STREAM, 0); // fate qualche error checking

dest_addr.sin_family = AF_INET; // Host Byte Order


dest_addr.sin_port = htons(DEST_PORT); // short, Network Byte Order
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
memset(&(dest_addr.sin_zero), '\0', 8); //

mette a zero il resto della struttura

// fate qualche error checking per connect

connect(sockfd,

(struct

sockaddr

*)&dest_addr,

sizeof(struct

sockaddr));
...
...
...

Error checking: connect() restituisce -1 se ho errore e verr settata anche errno al valore
di errore.

D) listen() -- Grazie per aver chiamato 3490

Facciamo finta di voler rimanere in attesa di connessioni da parte di una macchina


remota. Il processo si compone di due passi costituiti dalle funzioni: listen() e accept().

D1) listen()

La listen() abbastanza semplice da utilizzare...

int listen (int sockfd, int backlog);

Gli argomenti in dettaglio sono:

--sockfd: sempre lo stesso "socket file descriptor" ricevuto dalla chiamata a socket().
--backlog: il numero massimo di connessioni permesse sulla coda entrante. Cosa
significa? Le connessioni entranti vengono messe in coda fino a che non vengono
accettate con accept(). Backlog, quindi, rappresenta il limite massimo di queste
connessioni messe in attesa.

Error checking: listen() restituisce -1 in caso di errore e errno viene settato al valore di
errore...

E' ovvio che, nel caso andassimo ad utilizzare listen(), dovremmo prima specificare quale
porta dovremo mettere in "listening" e questo lo faremo con bind() ( visto prima ).

In generale avremo questa sequenza:

socket()
bind()
listen()
//qui andr accept che vedremo tra un secondo...

D2) accept()

accept() un po' strana.

Vediamo subito che cosa succede:


qualcuno, da remoto, prova a "connettersi" ( con connect() ) alla vostra macchina, su una

porta ( specificata da bind() ) su cui siete in ascolto ( usando listen() )... Le connessioni
verranno messe in coda aspettando di essere accettate ( con accept() ). Allora
chiameremo la funzione accept() e le "diremo" di prendere la prima connessione. Questo
restituir un nuovo socket file descriptor da usare per una singola connessione. A questo
punto, come avrete notato, avremo 2 "socket file descriptor": "l'originale" ( cio il primo )
ancora in fase di ascolto sulla nostra porta e "il nuovo" creato finamente pronto a
mandare ( con send()... tra un po' lo vedremo ) e ricevere ( con recv() ).

Vediamo la chiamata in dettaglio:

#include <sys/sockets.h>

int accept( int sockfd, void *addr, int *addrlen);

E ora gli argomenti:

--sockfd il file descrittore restituito da listen()


--addr sar un puntatore ad una "struct sockaddr_in" locale. Qui dove verranno allocate
le informazioni relative alla connessione ( host, porta della macchina remota )
--addrlen l'intero locale variabile che dovrebbe essere settato a sizeof(struct
sockaddr_in) prima che il suo indirizzo sia passato ad accept().

Error checking: accept, in caso di errore, restituisce -1 e viene settato, come al solito,
anche errno ad errore.

Esempio di codice:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT 3490


#define BACKLOG 10

main() {
int sockfd, new_fd;
struct sockaddr_in my_addr;
struct sockaddr_in their_addr;
int sin_size;

sockfd = socket(AF_INET, SOCK_STREAM, 0);

my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
my_addr.sin_addr.s_addr = INADDR_ANY;

// non dimenticate di fare i controlli sugli errori

bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

listen(sockfd, BACKLOG);

sin_size = sizeof(struct sockaddr_in);


new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
...
...

...

NOTA: new_Fd usato per mandare ( send() ) e ricevere ( recv() ) chiamate. Se si vuole
dare accesso ad un' altra connessione, si pu chiudere quella precedente con close().

E) send() e recv()

Queste due funzioni vengono utilizzati per comunicare attraverso STREAM_SOCKETS o


DGRAM_SOCKETS connessi. Se volessimo usare DATAGRAM SOCKETS non connessi,
allora dovremmo usare sendto() e revcfrom() ( vedere paragrafo successivo ).

Ecco la chiamata send():

int ( int sockfd, const void *msg, int len, int flags);

Ed ecco gli argomenti:

--sockfd: sempre il solito


--msg un puntatore ai dati che vogliamo mandare
--len la lunghezza dei dati, in bytes
--flags viene posto a 0
( per ulteriori chiarimenti vedere "man send" )

Esempio di codice:

char *msg = "Eccomi!!";


int len, bytes_sent;

...
...
...
len = strlen(msg);
bytes_sent = send(sockfd, msg,len, 0);

send() restituisce il numero di bytes in uscita ( potrebbero essere minori del numero di
bytes che avete deciso di mandare). Se i bytes effettivamente inviati non sono uguali a
"len", dovete essere voi a mandare il resto della stringa. La buona notizia sta nel fatto che,
se il pacchetto di dati abbastanza piccolo (meno di 1k ), probabilmente si riuscir a
mandare tutto il pacchetto in una sola volta.

Error checking: send() restituisce -1 se c' errore e errno posto allo stesso valore dell'
errore.

La chiamata recv():

int recv( int sockfd, void *buf, int len, unsigned int flags);

Come potete vedere recv() molto simile a send()....


Vediamo, comunque, gli argomenti nel dettaglio:

--sockfd il socket descriptor dal quale leggere.


--buf il buffer da cui leggere le informazioni ricevute
--len la lunghezza massima del buffer
--flags posto a zero
( vedere "man recv")

Ricapitolando, recv() restituisce il numero di bytes attualmente letto nel buffer oppure il

valore -1 se c' errore. Ma pu anche restituire 0: questo significa che la macchina remota
ha chiuso la connessione.

F) sendto() e recvfrom()

L' informazione di cui abbiamo bisogno l'indirizzo di destinazione.

Chiamata sento():

int sendto() ( int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr
*to, int tolen);

Come possiamo vedere, ho gli stessi argomenti di send(), pi altri 2:

--to: puntatore a "struct sockaddr" ( avrete struct sockaddr_in su cui dovrete fare un
casting all' ultimo momento ) che contiene l' indirizzo IP e la porta di destinazione.
--tolen pu essere posto a sizeof(struct sockaddr)

Error checking: come send(), sendto() restituisce il numero di bytes mandati ( che pu
essere minore dei bytes che abbiamo deciso di mandare ) oppure -1 se ho errore.

Altrettanto simili sono recv() e recvfrom().

Per recvfrom() ho:

int recvfrom( int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int
*fromlen );

Anche qui ho due argomenti in pi rispetto a recv():

--from un puntatore ad una struct sockaddr locale e sar "riempito" con l' indirizzo IP e
la porta della macchina d' origine.
--fromlen un puntatore ad un "int" locale che dovrebbe essere inizializzato a sizeof
(struct sockaddr ). Conterr la lunghezza dell' indirizzo attualmente messo in from.

Error checking: recvfrom() restituisce il numero di bytes ricevuti oppure -1 se errore e


errno viene settato al valore dell' errore.

Da ricordare: Se connettiamo un DATAGRAM SOCKET, possiamo usare send() e recv()


per le transazioni.

G) close() e shutdown()

Per chiudere una connessione si usa close(), in questo modo:

close(sockfd);

Per avere un controllo maggiore sulla chiusura di un socket, si pu usare shutdown():

int shutdown( int sockfd, int how );

Gli argomenti:

--sockfd il socket che si vuole interrompere


--how pu essere di tre tipi:

-> 0 : la ricezione disabilitata


-> 1 : la trasmissione disabilitata
-> 2 : sia la ricezione che la trasmissione sono disabilitate

Error checking: shutdown() restituisce 0 se ha successo, -1 se c' errore... ( errno viene


settato al valore di errore... come sempre!!!)

In realt shutdown(), non chiude un socket, ma cambia il suo utilizzo... per chiudere
definitivamente un socket, dovete usare close().

H) getpeername() -- Chi sei?

Questa funzione molto semplice. Ci dice chi connesso all' altro capo della
connessione di tipo STREAM.

int getpeername( int sockfd, struct sockaddr *addr, int *addrlen );

Gli argomenti:

--sockfd il solito
--addr un puntatore alla struttura "struct sockaddr" ( o struct sockaddr_in ) e contiene le
informazioni sulla macchina remota
--addrlen un puntatore ad interi che dovrebbe essere inizializzato a sizeof(struct
sockaddr)

Error checking: la funzione restituisce -1 se abbiamo errore.

Una volta ricevuto l' indirizzo della macchina remota, si possono usare "inet_ntoa" o
"gethostbyaddr" per avere altre informazioni sulla macchina remota.

I) gethostname() -- Chi sono?

Molto pi semplice della funzione precedente...

#include <unistd.h>

int gethostname(char *hostname, size_t size);

Argomenti:

--hostname un puntatore di array di caratteri che conterr l' hostname al "ritorno" dalla
funzione.
--size la lunghezza in bytes dell' array "hostname"

L) DNS -- Dimmi "whitehouse.org" e rispondo "198.137.240.92"

DNS sta per "Domain Name Service"

Per avere, ad esempio, l'IP associato ad un host, si usa gethostbyname():

#include <netdb.h>
struct hostent *gethostbyname ( const char *name );

Come si pu vedere, restituisce un puntatore a "struct hostent", la quale ha il seguente


layout:

struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_lenght;
char **h_addr_list;
};
#define h_addr h_addr_list[0]

Descrizione degli argomenti:

--h_name il nome ufficiale dell' host


--h_aliases l' array di nomi alternativi dell' host
--h_addrtype il tipo di indirizzo restituito... di solito AF_INET
--h_lenght la lunghezza dell' indirizzo di bytes
--h_addr_list l'array di indirizzi di rete per l' host. Gli indirizzi degli host sono in Network
Byte Order.

Error checking: la funzione restituisce un puntatore alla struttura "struct hostent" ( i cui
campi sono stati riempiti ) o NULL se ho un errore... Questa volta non errno ad essere
settato, ma h_errno... quindi quando faremo il controllo degli errori, non dovremo usare
perror(), ma herror().

Vediamo ora un esempio completo di programma:

/*GetIP*/

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {

struct hostent *host;

if( argc != 2 ) {
fprintf(stderr, "Usage: ./getip <hostname>\n");
exit(1);
}

if((host=gethostbyname(argv[1])) == NULL) {
herror("gethostbyname");
exit(1);
}

printf("Host: %s\n", host->h_name);

printf("IP: %s\n", inet_ntoa(*((struct in_addr *)host->h_addr)));

Il programma utilizza le funzioni appena viste per stampare a video l' host remoto e il suo
indirizzo IP.

FINE PRIMA PARTE

Potrebbero piacerti anche