You are on page 1of 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:

1 2 3 4

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 // short Network Byte Order // mette a zero il resto della

my_addr.sin_port = htons(MYPORT);
stuttura

my_addr.sin_addr.s_addr = inet_addr("10.12.110.37"); 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, sockaddr)); ... ... ...

(struct

sockaddr

*)&dest_addr,

sizeof(struct

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

You might also like