Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
(Prima parte)
INTRODUZIONE
CIao...
P.S.: nel caso abbiate trovato la guida di Beej gi tradotta in italiano... vi prego di non
dirmelo...
-----------------------------------------------------------------------------P5yc[0]--------------------------
"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().
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 ).
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.
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".
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
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
Questa struttura contiene informazioni sui "socket addresses" per molti tipi di sockets:
struct sockaddr {
unsigned short sa_family;
char
};
Per comunicare con la "struct sockaddr", i programmatori hanno creato una struttura
parallela, pi chiara:
struct sockaddr_in {
short int sin_family;
\\ Internet address
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.
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.
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".
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.
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
Vediamo, a questo punto, un esempio su come "riempire i campi della struttura struct
sockaddr_in":
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.
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));
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:
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...
Questa Sezione dedicata alle chiamate di sistema per accedere alla rete.
#include <sys/types.h>
#include <sys/sockets.h>
Ora cerchiamo di capire nel dettaglio gli argomenti della chiamata socket():
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 ).
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...
#include <sys/types.h>
#include <sys/socket.h>
Vediamo un esempio:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
main() {
int sockfd;
struct sockaddr_in my_addr;
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
my_addr.sin_addr.s_addr = inet_addr("10.12.110.37");
stuttura
Da notare:
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:
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:
char yes=-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().
#include <sys/types.h>
#include <sys/socket.h>
Esempio:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
main() {
int sockfd;
struct sockaddr_in dest_addr; // conterr l'indirizzo di destinazione
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.
D1) listen()
--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 ).
socket()
bind()
listen()
//qui andr accept che vedremo tra un secondo...
D2) accept()
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() ).
#include <sys/sockets.h>
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>
main() {
int sockfd, new_fd;
struct sockaddr_in my_addr;
struct sockaddr_in their_addr;
int sin_size;
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
listen(sockfd, BACKLOG);
...
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()
int ( int sockfd, const void *msg, int len, int flags);
Esempio di codice:
...
...
...
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);
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()
Chiamata sento():
int sendto() ( int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr
*to, int tolen);
--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.
int recvfrom( int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int
*fromlen );
--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.
G) close() e shutdown()
close(sockfd);
Gli argomenti:
In realt shutdown(), non chiude un socket, ma cambia il suo utilizzo... per chiudere
definitivamente un socket, dovete usare close().
Questa funzione molto semplice. Ci dice chi connesso all' altro capo della
connessione di tipo STREAM.
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)
Una volta ricevuto l' indirizzo della macchina remota, si possono usare "inet_ntoa" o
"gethostbyaddr" per avere altre informazioni sulla macchina remota.
#include <unistd.h>
Argomenti:
--hostname un puntatore di array di caratteri che conterr l' hostname al "ritorno" dalla
funzione.
--size la lunghezza in bytes dell' array "hostname"
#include <netdb.h>
struct hostent *gethostbyname ( const char *name );
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]
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().
/*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>
if( argc != 2 ) {
fprintf(stderr, "Usage: ./getip <hostname>\n");
exit(1);
}
if((host=gethostbyname(argv[1])) == NULL) {
herror("gethostbyname");
exit(1);
}
Il programma utilizza le funzioni appena viste per stampare a video l' host remoto e il suo
indirizzo IP.