Basi di network programming sotto Unix/Linux (draft version) · Per capire di cosa si tratta, `e...

31
Basi di network programming sotto Unix/Linux (draft version) Claudio Piciarelli 20 dicembre 2004

Transcript of Basi di network programming sotto Unix/Linux (draft version) · Per capire di cosa si tratta, `e...

Basi di network programming sottoUnix/Linux

(draft version)

Claudio Piciarelli

20 dicembre 2004

ii

Indice

1 Introduzione 11.1 Notazioni e terminologia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

2 Un po’ di premesse 32.1 Il network byte order . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32.2 Strutture per gli indirizzi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42.3 Gestire gli indirizzi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

2.3.1 gethostbyname() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52.3.2 gethostbyaddr() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62.3.3 inet ntoa() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62.3.4 inet aton() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72.3.5 getsockname() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72.3.6 getpeername() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

3 Connessioni TCP/IP 93.1 Funzionamento dei protocolli IP e TCP . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93.2 I socket . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93.3 connect() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103.4 Lettura e scrittura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113.5 Chiusura della connessione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123.6 Un client basato su TCP: l’idea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133.7 Un client basato su TCP: il codice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143.8 Funzioni per i server: bind(), listen() e accept() . . . . . . . . . . . . . . . . . . . . . . . . 153.9 Un server basato su TCP: l’idea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163.10 Un server basato su TCP: il codice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183.11 Come usare inetd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203.12 Un server che utilizza inetd: il codice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

4 UDP/IP 234.1 Funzionamento del protocollo UDP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234.2 Un semplice client UDP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234.3 Un semplice server UDP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

5 Approfondimenti 27

iii

iv INDICE

Capitolo 1

Introduzione

Questo breve testo cerca di dare al lettore le conoscenze di base per poter iniziare a scrivere programmiorientati al networking. L’argomento e molto vasto e questo testo non ha alcuna pretesa di essereesauriente; in particolare ci occuperemo solo di come poter trasmettere e ricevere dati utilizzando iprotocolli TCP e UDP, senza trattare altri protocolli di diversa natura (ad es. IPX) o di livello piu alto(SMTP, POP3, NNTP ecc.).

Daremo per scontate delle nozioni minime di networking e la conoscenza del linguaggio C. Inoltre,citando Aleph1, un prerequisito fondamentale e il possesso di un cervello, di una mente o di un qualsiasialtro device pensante equivalente.

Se trovate qualche errore in questo documento, non esitate a segnalarlo all’indirizzo piccia(at)dimi.uniud.it

1.1 Notazioni e terminologia

Prima di iniziare e bene mettersi d’accordo sulle convenzioni di scrittura e sul significato di alcuni terminiusati in questo testo.

Notazioni:

* Ad ogni funzione di importanza rilevante e associata una scheda riassuntiva che indica gli headerda includere, la dichiarazione formale, la lista dei parametri (ad ognuno dei quali e associato unsintetico commento) e il valore di ritorno.

* le porzioni di codice sono indentate rispetto al testo normale.

* i comandi da eseguire da shell sono preceduti dal prompt “$”

Nelle parti di testo che non rappresentano porzioni di codice o schede di funzioni . . .

* tutti i nomi di funzione sono seguiti dalle parentesi tonde “()”.Es: socket().

* i tipi di dati sono compresi tra apici singoli.Es: ’struct sockaddr’.

* le costanti sono comprese tra apici inversi.Es: ‘INADDR LOOPBACK‘.

* i nomi di variabili sono compresi tra apici doppi.Es: “s”.

Terminologia:

datagramma : unita di trasmissione dei protocolli IP e UDP

pacchetto : unita di trasmissione tra il livello IP e il sottostante livello data link. Ad esempio undatagramma IP puo essere suddiviso in piu frammenti, ognuno dei quali e un pacchetto.

segmento : unita di trasmissione del protocollo TCP

1

2 CAPITOLO 1. INTRODUZIONE

Capitolo 2

Un po’ di premesse

2.1 Il network byte order

Uno dei problemi che si deve affrontare lavorando con architetture diverse e quello della cosiddetta “en-dianity”. Il problema riguarda l’ordine in cui i byte vengono memorizzati in RAM nelle varie architetturehardware. Per capire di cosa si tratta, e utile iniziare subito con un esempio pratico. Prendiamo un nume-ro intero di tipo ’long’, che nella maggior parte della macchine attuali e lungo 32 bit, o equivalentemente4 byte. Ad esempio:

2.520.600.212

Sı, duemiliardicinquecentoventimilioniseicentomiladuecentododici. Se convertiamo questo numero in esa-decimale, otteniamo:

963D4E94

o, in binario:

10010110001111010100111010010100

Possiamo quindi rappresentare questo numero con 4 gruppi di 8 bit, ovvero 4 byte

10010110 00111101 01001110 10010100 (binario)

96 3D 4E 94 (esadecimale)

MSB LSB

Il byte piu a sinistra si chiama Most Significant Byte (MSB), mentre quello piu a destra e detto LeastSignifican Byte (LSB). Il perche di questi nomi e facile da intuire: il byte piu a sinistra e quello che “pesadi piu”, mentre quello piu a destra e quello che “pesa di meno”. E’ come dire che, nel numero 1.234.567,l’1 e la cifra piu importante, perche rappresenta i milioni, mentre il 7 e quella meno importante, percherappresenta le unita.

La memoria RAM di un computer e vista come una sequenza di byte, ognuno con il suo indirizzo. E’lecito quindi chiedersi in che ordine i 4 byte visti prima vengano memorizzati quando vogliamo scriverein memoria un intero di tipo ’long’. La risposta e: dipende dall’architettura. I processori Intel x86 adesempio memorizzano per primo il byte meno significativo (LSB) e per ultimo quello piu significativo,quindi in memoria il nostro numero verrebbe rappresentato come:

94 4E 3D 96

i processori motorola 68x00 invece memorizzano prima il byte piu signficativo, ovvero:

96 3D 4E 94

3

4 CAPITOLO 2. UN PO’ DI PREMESSE

Nel primo caso si parla di approccio “little endian”, il secondo e invece detto big endian.

Perche tutta questa lunga digressione sulla rappresentazione in memoria dei numeri? Semplice: nel-la programmazione di applicazioni orientate alle connessioni di rete ci si trova inevitabilmente a doverscambiare con altri host alcuni dati di tipo ’long’ o ’short’. Ad esempio un indirizzo IP e composto da32 bit (per ora - le cose cambieranno con IPv6) e quindi e semplicemente un long. Allo stesso modo unaporta e un numero compreso tra 0 e 65535, quindi rappresentabile con 16 bit, ovvero uno ’short’. Se ogniprogramma memorizzasse gli indirizzi nei pacchetti IP a seconda dell’endianita dell’architettura su cuigira sarebbe un disastro: macchine con architetture diverse non riuscirebbero a comunicare tra loro; unPC e un Macintosh ad esempio non potrebbero condividere una connessione TCP per il semplice fatto cheusano metodi diversi per rappresentare le porte e gli indirizzi. Poiche la rete Internet (e, assieme ad essa, iprotocolli IP e TCP) e stata concepita per permettere la comunicazione tra elaboratori indipendentementedalla loro architettura, si e reso necessario l’utilizzo di una rappresentazione comune, una sorta di “lin-gua franca” a cui tutti i software di rete devono conformarsi: questa rappresentazione dei numeri e detta“Network Byte Order”. (per i piu curiosi, il network byte order dovrebbe essere equivalente al big endian).

Risulta quindi necessario avere delle funzioni che permettano di convertire i numeri dal formato“locale” (Host Byte Order) a quello di rete. Sotto linux queste funzioni sono:

header:

#include <netinet/in.h>

dichiarazioni:

unsigned long int htonl (unsigned long int n);

unsigned short int htons (unsigned short int n);

unsigned long int ntohl (unsigned long int n);

unsigned short int ntohs (unsigned short int n);

parametri:

n: numero da convertire

valore di ritorno:

il numero convertito

Il nome delle funzioni dice tutto: htonl() significa “host to network long” e converte appunto un longdal formato locale a quello di rete; la funzione ntohl() fa ovviamente l’operazione inversa. Le altre duefunzioni, ntohs() e htons(), sono equivalenti a ntohl() e htonl(), ma lavorano con gli short integer.

2.2 Strutture per gli indirizzi

Come vedremo in seguito, molte funzioni devono ricevere come parametri degli indirizzi e dei numeri diporta; per rappresentare questi dati si utilizza la struttura ’struct sockaddr’. La cosa curiosa, e che spessoconfonde i programmatori alle prime armi, e che questa struttura non viene mai utilizzata direttamentedal programmatore, sebbene sia richiesta come parametro da molte funzioni! Il motivo di tutto cio emolto semplice, e nasce da una necessita di generalizzazione. Funzioni come bind(), connect() o altreche vedremo in seguito, sono progettate per funzionare con diversi tipi di protocolli: non esiste solo ilTCP/IP, ci sono ad esempio IPX (usato nelle reti Novell), Appletalk, X25 ecc. Ognuno di questi pro-tocolli tuttavia necessita di una struttura “ad hoc” adatta a contenerne gli indirizzi, e queste strutturesono diverse l’una dall’altra. La struttura ’struct sockaddr’ in realta non e altro che un “wrapper”, cheserve a nascondere le differenze tra le strutture specifiche dei vari protocolli. Se per esempio vogliamousare il protocollo IPX definiremo i nostri indirizzi con la struttura apposita (’struct sockaddr ipx’) e poifaremo un type cast a ’struct sockaddr’ ogni volta che sara necessario utilizzarla.

Un esempio come al solito chiarira le idee. La struttura usata per gli indirizzi IP e la ’structsockaddr in’, definita in <netinet/in.h>:

struct sockaddr_in {

2.3. GESTIRE GLI INDIRIZZI 5

short sin_family;

short sin_port;

struct in_addr sin_addr;

}

dove a sua volta la ’struct in addr’ e definita come:

struct in_addr {

long s_addr;

}

In sin family va sempre messa la costante ‘AF INET‘, in sin port la porta e in sin addr.s addr l’indirizzoIP (questi ultimi due in network byte order!). Tutte le volte che una funzione richiedera una ’structsockaddr’ come parametro, sara sufficiente passare come argomento la nostra ’struct sockaddr in’ usandoun type cast:

struct sockaddr_in s;

s.sin_family = AF_INET;

s.sin_port = htons(25);

s.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

connect(..., (struct sockaddr *) &s, ...);

In questo caso la funzione connect() richiede (oltre ad altri parametri che sono stati omessi) un puntatorea ’struct sockaddr’, tuttavia e stato passato come argomento un puntatore ad una ’struct sockaddr in’,“ingannando” la connect() con il type cast ’(struct sockaddr *)’. In questo modo si ottiene la generaliz-zazione cercata: la funzione connect() riceve comunuque sempre e solo delle ’struct sockaddr’, le qualipero in realta sono delle strutture piu specifiche, a seconda del protocollo utilizzato.

2.3 Gestire gli indirizzi

Abbiamo visto precedentemente come gli indirizzi IP siano rappresentati dalla struttura ’struct in addr’,che contiene un solo elemento, il long ’s addr’. Ma come trovare gli indirizzi che ci servono? La libc civiene incontro con alcune funzioni molto utili; qui di seguito verranno analizzate le piu importanti.

2.3.1 gethostbyname()

header:

#include <netdb.h>

dichiarazione:

struct hostent *gethostbyname(const char *name);

parametri:

*name: stringa contenente l’hostname o un indirizzo IP numerico

valore di ritorno:

una struttura contenente i dati dell’host

Questa funzione permette di trovare l’indirizzo IP di una macchina, data una stringa che contiene unhostname (es: “pippo.pluto.com”) o un indirizzo numerico (es: “127.0.0.1”). Il sistema operativo si occu-pa di risolvere il nome nella maniera piu opportuna, ad esempio cercandolo in /etc/hosts o interrogandoun DNS. Il risultato viene salvato in una ’struct hostent’, il cui formato e il seguente:

struct hostent {

char *h_name; /* nome ufficiale dell’host */

char **h_aliases; /* lista di alias */

int h_addrtype; /* tipo di indirizzo (vale sempre ‘AF_INET‘) */

6 CAPITOLO 2. UN PO’ DI PREMESSE

int h_length; /* lunghezza dell’indirizzo in bytes */

char **h_addr_list; /* lista di indirizzi */

}

#define h_addr h_addr_list[0] /* il primo indirizzo */

Il campo piu importante e “h addr” (che in realta e definito come primo elemento della lista h addr list).Anche se non sembra, “h addr” punta ad una ’struct in addr’, e quindi al suo interno contiene l’unicoelemento “s addr”, che e una long rappresentante l’indirizzo IP in formato Network Byte Order.

Notate tuttavia che “h addr” e definito come un ’char *’ e non come una ’struct in addr *’ (sempre permotivi di generalizzazione). Si rende quindi necessaria la presenza di h length, che contiene la lunghezzain byte della struttura puntata da h addr. Se ad esempio vogliamo memorizzare l’indirizzo appena trovatocon gethostbyname() in una ’struct sockaddr in’, sara necessario scrivere:

struct sockaddr_in s;

struct hostent *h;

h = gethostbyname("localhost");

memcpy(&s.sin_addr, h->h_addr, h->h_length);

in questo esempio la funzione memcpy() prende la ’struct in addr’ puntata da “h addr” e la copia in“s.sin addr”.

2.3.2 gethostbyaddr()

header:

#include <netdb.h>

dichiarazione:

struct hostent *gethostbyaddr(const char *addr, int len, int type);

parametri:

*addr: puntatore ad una ’struct in\_addr’ (type cast a char *)

len : lunghezza della struttura puntata da addr

type : tipo di indirizzo (vale sempre ‘AF\_INET‘)

valore di ritorno:

una struttura contenente i dati dell’host

Questa funzione, come dice il nome, fa il lavoro inverso di gethostbyname(). Prende in input una ’structin addr’ (di cui e stato fatto il type casting a char *), la lunghezza della struttura e il tipo di indirizzo(che e sempre ‘AF INET‘). Anche gethostbyaddr() ritorna una ’struct hostent’, in cui e memorizzato ilnome dell’host cercato nel campo “h name”. Un esempio del suo utilizzo:

struct in_addr a;

struct hostent *h;

a.s_addr = htonl(INADDR_LOOPBACK);

h = gethostbyaddr((char *) &a, sizeof(a), AF_INET);

printf("il nome e: %s\n", h->h_name);

Poiche ‘INADDR LOOPBACK‘ e una costante che indica l’indirizzo 127.0.0.1, il risultato dell’esecuzionedi questo codice dovrebbe essere:

il nome e: localhost

2.3.3 inet ntoa()

header:

#include <sys/socket.h>

#include <netinet/in.h>

2.3. GESTIRE GLI INDIRIZZI 7

#include <arpa/inet.h>

dichiarazione:

char *inet_ntoa(struct in_addr in);

parametri:

in: ’struct in_addr’ contenente un indirizzo

valore di ritorno:

una stringa rappresentante l’indirizzo IP

Il funzionamento di questa funzione e banale: data una ’struct in addr’ restituisce una stringa conl’indirizzo IP espresso in formato numbers-and-dots. Ad esempio il codice:

a.s_addr = htonl(INADDR_LOOPBACK);

printf("l’indirizzo e: %s\n", inet_ntoa(a));

da come risultato

l’indirizzo e: 127.0.0.1

Il nome della funzione significa network to ASCII.

2.3.4 inet aton()

header:

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

dichiarazione:

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

parametri:

*cp : stringa rappresentante un indirizzo IP

*inp: ’struct in_addr’ in cui salvare il risultato

valore di ritorno:

un numero diverso da zero se l’indirizzo e valido, 0 altrimenti

Ovviamente la simmetrica di inet ntoa(). Presa una stringa con un indirizzo IP in formato numbers-and-dots, lo trasforma in una ’struct in addr’. Il risultato e salvato nella struttura passata come argomento.

2.3.5 getsockname()

header:

#include <sys/socket.h>

dichiarazione:

int getsockname(int s, struct sockaddr *name, socklen_t *namelen);

parametri:

s : un socket

*name : struttura in cui salvare il risultato

*namelen: lunghezza di name, in byte

valore di ritorno:

0 in caso di successo, -1 in caso di errore

Questa funzione salva in “name” i dati relativi all’indirizzo a cui il socket “s” e stato associato tramitela funzione bind().

8 CAPITOLO 2. UN PO’ DI PREMESSE

2.3.6 getpeername()

header:

#include <sys/socket.h>

dichiarazione:

int getpeername(int s, struct sockaddr *name, socklen_t *namelen);

parametri:

s : un socket

*name : struttura in cui salvare il risultato

*namelen: lunghezza di name, in byte

valore di ritorno:

0 in caso di successo, -1 in caso di errore

Analoga a getsockname(), ma ritorna l’indirizzo dell’host remoto a cui il socket e connesso.

Capitolo 3

Connessioni TCP/IP

3.1 Funzionamento dei protocolli IP e TCP

IP e un protocollo per la trasmissione dei dati inaffidabile e connectionless, ovvero non orientato allaconnessione. Questo significa che non possiamo sapere nulla della sorte di un datagramma IP dopo averloinviato: i router cercheranno di farlo arrivare a destinazione (e questo e infatti l’unico scopo di IP),ma non ci viene garantito nulla riguardo al percorso seguito dai datagrammi, al loro ordine di arrivo, oaddirittura all’arrivo stesso: il datagramma potrebbe venire perso lungo il tragitto senza che il mittentevenga notificato di quanto e accaduto.

TCP al contrario, pur viaggiando incapsulato nei datagrammi IP, e un protocollo di trasmissione deidati affidabile e orientato alla connessione. “Orientato alla connessione” significa che questo protocolloimplementa una forma di comunicazione “point-to-point”, in cui due host che vogliano scambiarsi deidati sono alle due estremita di un canale di comunicazione che li collega. Il paragone piu classico inquesto caso e quello del telefono: quando Tizio telefona a Caio, si crea tra di loro una sorta di “tunnel”virtuale che li mette in contatto: non importa il modo in cui effettivamente la telefonata e gestita dal-l’operatore telefonico, dal punto di vista dell’utente e come se ci fosse un cavo che collega direttamente idue apparecchi. “Affidabile” significa che TCP ci garantisce la corretta ricezione dei dati trasmessi. Inaltre parole TCP pone rimedio a tutti i problemi del protocollo sottostante (IP), garantendo l’arrivo deidati e la loro corretta ricostruzione nel caso arrivino in un ordine diverso da quello di spedizione.

Il risultato finale e un protocollo che offre un alto livello di astrazione. La suddivisione dei dati spe-diti in segmenti e infatti compito del protocollo stesso e non dell’utente (a differenza di quanto vedremoaccadere con UDP); in questo modo l’utente vede solo un flusso (stream) di byte che viaggiano da unhost all’altro, cosa che rende la trasmissione dei dati molto piu flessibile e semplice da gestire.

Ricordiamo infine che TCP introduce il concetto di porta. Se l’indirizzo IP identifica univocamente unhost, le porte TCP sono i vari “ingressi” (o uscite) verso (da) quell’host: ogni connessione TCP provieneda una porta ed e diretta verso una porta. In questo modo tutte le connessioni sono univocamenteidentificate da quattro valori: indirizzo IP sorgente, indirizzo IP di destinazione, porta sorgente, porta didestinazione.

3.2 I socket

Il concetto piu importante nella programmazione di rete e forse quello del socket. Per capire cosa sia unsocket, e utile ricorrere all’analogia telefonica vista precedentemente. Se paragoniamo una connessioneTCP ad una connessione telefonica, allora il socket e il telefono stesso. Dal punto di vista del programmaapplicativo i socket sono i punti di accesso alla rete, o meglio il punto di contatto tra il programma e ilkernel sottostante che gestisce le connessioni TCP; ogni connessione TCP ha due socket alle estremita,proprio come ogni telefonata avviene tra due telefoni. Il nostro programma si occupera solo di gestire unsocket, senza preoccuparsi troppo del funzionamento del protocollo TCP stesso, cosı come un utente te-

9

10 CAPITOLO 3. CONNESSIONI TCP/IP

lefonico interagisce solo con il telefono, senza chiedersi come la telefonata venga effettivamente instradata.

Piu concretamente, dal punto di vista del programmatore il socket e molto simile ad un file descriptor,proprio come quelli ottenuti tramite la funzione open(). Su un socket e possibile effettuare operazioni dilettura e scrittura, proprio come su un file discriptor, e in generale la somiglianza tra le due entita e taleda permette di applicare ai socket molte funzioni che solitamente sono utilizzate con i file descriptor: adesempio vedremo che, tramite fdopen(), sara possibile associare uno stream (ovvero un ’FILE *’) ad unsocket.

Per creare un socket, si usa la funzione omonima:

header:

#include <sys/types.h>

#include <sys/socket.h>

dichiarazione:

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

parametri:

domain : il dominio di comunicazione del socket

type : semantica di comunicazione

protocol: il protocollo da utilizzare

valore di ritorno:

un descrittore che identifica il socket, o -1 in caso di errore

“Domain” e una costante che indica quale famiglia di protocolli verra utilizzata con quel socket e nel casodi una connessione TCP assume il valore ‘PF INET‘; nella man page di socket() potrete trovare tuttigli altri possibili valori utilizzabili. “Type” indica invece il tipo di comunicazione usata e nel caso delTCP deve assumere il valore costante ‘SOCK STREAM‘. “Protocol” infine indica il tipo di protocolloda utilizzare. Tipicamente domain e type identificano univocamente un solo protocollo possibile, quindinon e necessario specificare esplicitamente questo parametro: sara sufficiente usare il valore 0 per lasciareal sistema operativo il compito di decidere da solo il protocollo piu adatto da usare. Il valore ritornato eun socket; si tratta di un semplice ’int’, esattamente come accade per i file descriptor. Riassumendo, percreare un socket da utilizzare in una connessione TCP e sufficiente scrivere:

int s;

s = socket(PF_INET, SOCK_STREAM, 0);

3.3 connect()

Se vogliamo connetterci ad un host remoto, una volta creato un socket dobbiamo inizializzare unaconnessione TCP ed associarla al socket stesso. Per farlo si utilizza la funzione connect():

header:

#include <sys/types.h>

#include <sys/socket.h>

dichiarazione:

int connect(int sockfd, const struct sockaddr *serv_addr,

socklen_t addrlen);

parametri:

sockfd : socket da associare alla connessione

*serv_addr: indirizzo del server

addrlen : lunghezza in byte della struttura puntata da serv_addr

valore di ritorno:

3.4. LETTURA E SCRITTURA 11

0 in caso di successo, -1 in caso di errore

connect() apre una connessione verso un host remoto. Il primo parametro e il socket creato con socket(),il secondo e un puntatore alla ’struct sockaddr in’ che contiene l’indirizzo e la porta a cui dobbiamocollegarci (come al solito e richiesto un type cast a ’struct sockaddr *’) mentre l’ultimo parametro devecontenere la lunghezza in byte della struttura stessa. connect() ritorna 0 in caso di successo e -1 in casodi errore.

3.4 Lettura e scrittura

Una volta instaurata una connessione TCP con socket() e connect(), come possiamo comunicare conl’host remoto? Esistono due funzioni apposite per la lettura e scrittura su socket:

header:

#include <sys/types.h>

#include <sys/socket.h>

dichiarazioni:

int recv(int s, void *buf, size\_t len, int flags);

int send(int s, const void *msg, size\_t len, int flags);

parametri:

s : descrittore da cui leggere o su cui scrivere

*buf/*msg: buffer in cui salvare i dati letti o contenente

i dati da spedire

len : dimensione del buffer

flags : flag aggiuntivi

valori di ritorno:

il numero di byte ricevuti o spediti, -1 in caso di errore

Tuttavia nella maggior parte dei casi non e necessario ricorrere a queste funzioni. Ricordiamo infatti cheun socket viene trattato come un file descriptor, quindi possiamo utilizzare le funzioni standard di letturae scrittura su file:

header:

#include <unistd.h>

dichiarazioni:

ssize_t read(int fd, void *buf, size_t count);

ssize_t write(int fd, const void *buf, size_t count);

parametri:

fd : descrittore da cui leggere o su cui scrivere

*buf : buffer in cui salvare i dati letti o contenente i dati da

scrivere

count: numero di byte da leggere/scrivere

valore di ritorno:

il numero di byte letti (0 = End Of File) o scritti. -1 in caso di errore.

Sia read() che write() richiedono come parametri il file descriptor da cui leggere/scrivere (nel nostro casola socket), il puntatore ad un buffer in cui scrivere i dati letti o da cui leggere i dati da scrivere e ladimensione massima dei dati da leggere/scrivere. Ricordiamo che non e possibile sapere a priori in chemodo TCP ha segmentato i dati da spedire, quindi possono essere necessarie piu read() per ricevere tuttii dati trasmessi dall’host remoto.

Tuttavia anche read() e write() sono delle funzioni di basso livello, scomode da usare e inadeguate nelcaso si debbano usare le features di formattazione dell’input e dell’output tipiche di fscanf() e fprintf().

12 CAPITOLO 3. CONNESSIONI TCP/IP

Sappiamo infatti che TCP ci offre un livello di astrazione tale da poter considerare la connessione come unsemplice flusso (stream) di byte, quindi e lecito pensare di poter usare anche con i socket le funzioni perla gestione di stream. E’ infatti possibile associare uno stream ad un socket tramite la funzione fdopen():

header:

#include <stdio.h>

dichiarazione:

FILE *fdopen (int fildes, const char *mode);

parametri:

fildes: descrittore

*mode : stringa indicante la modalita di apertura dello stream

valore di ritorno:

un puntatore a FILE in caso di successo, NULL in caso di errore.

Fdopen ritorna uno stream (ovvero un FILE *) partendo da un file descriptor e una stringa che indicail modo di gestione dello stream (“r” per la sola lettura, “w” per la sola scrittura ecc. ecc. Vedere larelativa man page per i dettagli). Una volta aperto lo stream, e consigliabile disabilitare il buffer ad essoassociato, per evitare di introdurre dei ritardi di trasmissione. Per farlo, e sufficiente usare la funzionesetbuf():

header:

#include <stdio.h>

dichiarazione:

void setbuf(FILE *stream, char *buf);

parametri:

*stream: lo stream di cui si vuole cambiare il buffer

*buf : il buffer (NULL = no buffer)

valore di ritorno:

nessuno

Con setbuf() si puo disabilitare completamente il buffering ponendo buf = NULL. In conclusione, datoun socket, e possibile associare ad esso uno stream in questo modo

/* s e un socket */

FILE *f;

f = fdopen(s, "r");

setbuf(f, NULL);

A questo punto e possibile gestire il socket usando lo stream ’f’ tramite le classiche funzioni fprintf(),fscanf() e simili. Nel caso sia necessario compiere contemporanemanete operazioni di lettura e scrittura,conviene usare due stream diversi per evitare fastidiosi problemi:

FILE *f1, *f2;

f1 = fdopen(s, "r");

f2 = fdopen(s, "w");

setbuf(f1, NULL);

setbuf(f2, NULL);

3.5 Chiusura della connessione

Una connessione TCP puo essere chiusa sia dal client che dal server, semplicemente usando la classicaclose() (o fclose() nel caso sia stato associato uno stream al socket). Quando uno dei due host chiude laconnessione, l’altro riceve un EOF (end of file). In alternativa si puo utilizzare una funzione apposita,ovvero shutdown():

3.6. UN CLIENT BASATO SU TCP: L’IDEA 13

header:

#include <sys/socket.h>

dichiarazione:

int shutdown(int s, int how);

parametri:

s : socket da chiudere

how: 0 = chiusura in ricezione, 1 = chiusura in spedizione,

2 = chiusura bidirezionale

valore di ritorno:

0 in caso di successo, -1 in caso di errore

La caratteristica interessante di shutdown() e quella di permettere la chiusura del socket solo in lettura(“how” = 0) o solo in scrittura (how = 1). Nel caso si assegni ad “how” il valore 2, la chiusura avvienesia in lettura che scrittura, ed il suo effetto e equivalente a quello di close().

3.6 Un client basato su TCP: l’idea

Arrivati a questo punto, abbiamo tutte le conoscenze necessarie per scrivere un client che apra unaconnessione TCP verso un server. Come esempio pratico, scriveremo un programma che si colleghi allaporta daytime (13/tcp) per leggere la data e l’ora corrente; l’effetto sara lo stesso dell’esecuzione delcomando

$ telnet nomehost 13

Ovviamente la macchina su cui ci collegheremo dovra avere il servizio daytime attivo. Sotto linux esufficiente avere il demone inetd attivo e aggiungere, se non c’e gia, la seguente linea in /etc/inetd.conf

daytime stream tcp nowait root internal

Se usate xinetd il file da modificare e /etc/xinetd.conf, e la sintassi e:

service daytime

{

type = INTERNAL

id = daytime-stream

socket\_type = stream

protocol = tcp

user = root

wait = no

}

Vediamo quali sono i passi principali che il nostro client deve effettuare:

1. creare un socket

2. memorizzare in una ’struct sockaddr in’ l’indirizzo dell’host remoto

3. inizializzare una connessione TCP verso l’host specificato al punto 2 e assegnare questa connessioneal socket creato al punto 1

4. leggere la data e l’ora che il server ci inviera automaticamente

5. chiudere la connessione.

14 CAPITOLO 3. CONNESSIONI TCP/IP

3.7 Un client basato su TCP: il codice

/*---------8<----------------------------------------------------------*/

#include <netdb.h> /* gethostbyname() */

#include <sys/types.h> /* socket() connect() */

#include <sys/socket.h> /* socket() connect() */

#include <netinet/in.h> /* struct sockaddr_in */

#include <string.h> /* memset() memcpy() */

#include <stdio.h>

#include <errno.h>

#define BUF_SIZE 100

extern int h_errno; /* variabile esterna per la gestione degli errori */

int main(int argc, char **argv)

{

struct hostent *host; /* la macchina a cui vogliamo collegarci */

struct sockaddr_in addr; /* il suo indirizzo nella struttura inaddr_in */

int s; /* il nostro socket... */

FILE *f; /* ...e lo stream ad esso associato */

char buf[BUF_SIZE]; /* buffer di lettura */

if(argc != 2){

printf("Uso: %s hostname\n", argv[0]);

exit(1);

}

/* cerchiamo l’indirizzo dell’host passato come parametro */

host = gethostbyname(argv[1]);

if(host == NULL){

herror("gethostbyname");

exit(1);

}

/* memorizziamo in addr l’indirizzo dell’host e la porta (13)

* a cui vogliamo connetterci */

memset(&addr, 0, sizeof(addr));

addr.sin_family = AF_INET;

addr.sin_port = htons(13);

memcpy(&addr.sin_addr, host->h_addr, host->h_length);

/* creiamo un socket adatto alle connessioni TCP */

s = socket(PF_INET, SOCK_STREAM, 0);

if(s == -1){

perror("socket");

exit(1);

}

/* effettuiamo la connessione */

if(connect(s, (struct sockaddr *) &addr, sizeof(addr)) == -1){

perror("connect");

exit(1);

}

3.8. FUNZIONI PER I SERVER: BIND(), LISTEN() E ACCEPT() 15

/* associamo uno stream al socket */

f = fdopen(s, "r");

setbuf(f, NULL);

/* ciclo di lettura: leggi e scrivi finche non arriva un EOF */

fgets(buf, BUF_SIZE, f);

do{

printf(buf);

fgets(buf, BUF_SIZE, f);

}while(! feof(f));

/* chiudiamo lo stream (e di conseguenza il socket) */

fclose(f);

exit(0);

}

/*---------8<----------------------------------------------------------*/

3.8 Funzioni per i server: bind(), listen() e accept()

In una connessione TCP il client ha solitamente un ruolo attivo, poiche si occupa di richiedere l’aperturadi una connessione usando la funzione connect(). Al contrario il server ha un ruolo passivo, e si limita arimanere in ascolto su una determinata porta, in attesa di connessioni. Per questo la programmazione diun server richiede l’utilizzo di alcune funzioni particolari. La prima di queste e bind():

header:

#include <sys/types.h>

#include <sys/socket.h>

dichiarazione:

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

parametri:

sockfd : socket

*my_addr: indirizzo locale di binding

addrlen : lunghezza della struttura puntata da my_addr

valore di ritorno:

0 in caso di successo, -1 in caso di errore

Bind in inglese significa “legare”, ed in effetti e proprio questo lo scopo di tale funzione: quello di “legare”un socket ad una porta, in modo da poter gestire tramite quel socket tutte le connessioni che arrivanoa quella porta. “sockfd” e il socket da legare, “my addr” punta ad una ’struct sockaddr in’ contenente idati dell’host locale su cui il server deve girare e “addrlen” e la lunghezza in byte della struttura puntatada “my addr”.

Cosa deve contenere la struttura puntata da “my addr”? Come al solito e necessario assegnare asin family la costante ‘AF INET‘ e a sin port la porta su cui vogliamo metterci in ascolto. Ma qualeindirizzo mettere in “sin addr”? Probabilmente vorremo che il nostro server possa girare su qualsiasimacchina, quindi sara sufficiente utilizzare la costante ‘INADDR ANY‘ che rappresenta, appunto, unindirizzo qualsiasi, ed e equivalente a 0.0.0.0. In altre parole per mettere un server in ascolto sulla porta6969 avremo bisogno di una ’struct sockaddr in’ cosı inizializzata:

struct sockaddr_in addr;

addr.sin_family = AF_INET;

16 CAPITOLO 3. CONNESSIONI TCP/IP

addr.sin_port = htons(6969);

addr.sin_addr.s_addr = htonl(INADDR_ANY);

Associare il socket ad una porta non e tuttavia sufficiente, bisogna anche mettersi in ascolto su quelsocket per accorgersi dell’arrivo di nuove connessioni. La funzione da utilizzare e listen():

header:

#include <sys/socket.h>

dichiarazione:

int listen(int s, int backlog);

parametri:

s : socket

backlog: dimensione della coda delle connessioni

valore di ritorno:

0 in caso di successo, -1 in caso di errore

“s” e il socket legato alla porta su cui vogliamo attendere connessioni, mentre “backlog” indica quanteconnessioni possono rimanere contemporaneamente in attesa di completamento. Attenzione, questo nu-mero non indica quante connessioni il server puo gestire contemporaneamente! Una connessione TCP,prima di raggiungere lo stato finale di ’ESTABLISHED’, deve passare attraverso una fase di inizializza-zione (il famoso three-way handshake); backlog indica quante connessioni ancora non completate il serverpuo gestire contemporaneamente. Un valore tipico e 5.

L’ultima funzione e quella per accettare le connessioni in arrivo, ed ovviamente si chiama accept():

headers:

#include <sys/types.h>

#include <sys/socket.h>

dichiarazione:

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

parametri:

s : socket su cui rimanere in ascolto

addr : puntatore alla ’struct sockaddr_in’ in cui verra

memorizzato l’indirizzo e la porta DA CUI PROVIENE la

connessione

addrlen: puntatore ad un intero in cui verra memorizzata la

lunghezza dell’indirizzo ‘‘addr’’. Deve essere inizializzato

con la dimensione della struttura puntata da addr

valore di ritorno:

un socket

accept() gestisce le connessioni in arrivo sul socket “s”, su cui ci eravamo precedentemente messi in ascoltocon listen(). L’indirizzo dell’host remoto da cui proviene la connessione viene salvato in addr. accept()ritorna un nuovo socket da utilizzare per gestire la connessione appena accettata; in questo modo il primosocket (“s”) puo continuare a rimanere in attesa di altre connessioni. In altre parole il socket “s” servesolo ad accettare le connessioni, che poi saranno gestite tramite gli altri socket creati di volta in volta daaccept(). Ricordatevi che accept e una funzione bloccante, ovvero il vostro programma si blocchera suquesta funzione finche non arrivera una connessione da gestire.

3.9 Un server basato su TCP: l’idea

Quali sono dunque le operazioni che deve compiere un server?

1. creare un socket

3.9. UN SERVER BASATO SU TCP: L’IDEA 17

2. memorizzare in una ’struct sockaddr in’ l’indirizzo locale

3. legare il socket creato al punto 1. all’indirizzo locale specificato al punto 2. con bind()

4. mettersi in attesa di connessioni con listen()

5. accettare le connessioni in arrivo con accept()

6. gestire la connessione

7. chiudere la connessione

8. ritornare al punto 5. per accettare nuove connessioni

Tuttavia in questo modo si puo gestire solo una connessione alla volta: dopo la accept() (punto 5) ilserver e impegnato a gestire la connessione appena ricevuta, e puo aspettarne altre (ovvero tornare alpunto 5) solo dopo aver chiuso la connessione corrente. Questo puo andare bene in alcuni casi, in cuiil server si limita a mandare una breve sequenza di dati al client chiudendo subito dopo la connessione(vedi ad es. il server daytime per il quale precedentemente abbiamo scritto un client), tuttavia in altricasi e inaccettabile. In particolar modo, un server non puo accettare una sola connessione alla volta sedeve attendere dei dati spediti dal client: in questi casi infatti sarebbe banale per un utente collegarsi,non spedire nulla e bloccare cosı il nostro server impedendo la connessione a tutti gli altri utenti.

E’ giunta quindi l’ora di sfruttare le potenzialita multitasking del nostro sistema operativo. L’idea esemplice: ogni volta che arriva una nuova richiesta di connessione, il server crea una copia di se stesso pergestirla. In questo modo, mentre il processo padre si limita ad accettare le connessioni, queste sono poigestite dai processi figli che vengono appositamente creati di volta in volta. La funzione per creare questinuovi processi e fork(), la cui trattazione approfondita va al di la degli scopi di questo documento, percui rimando il lettore alla relativa man page. L’importante, per noi programmatori di server, e sapereche guardando il valore di ritorno di fork() siamo in grado di capire se ci troviamo nel processo padre(in questo caso fork ritorna il PID del processo, necessariamente diverso da 0) o nel processo figlio (ilvalore ritornato e 0); in questo modo saremo in grado di gestire i due diversi casi. Fork() inoltre tornautile se vogliamo scrivere un vero e proprio daemon, ovvero un server che vada in background da solo almomento dell’esecuzione. La cosa e molto semplice da realizzare, e consiste nel fare un fork all’inizio delprogramma. Successivamente si fara terminare subito il processo padre, mentre il processo figlio dovralimitarsi a chiamare la funzione setsid().

Un’ultima cosa: senza entrare nel dettaglio della gestione dei segali, diciamo solo che si renderanecessaria la linea

signal(SIGCHLD, SIG\_IGN);

per evitare la creazione di processi zombie ad ogni nuova connessione. Questo serve a disabilitare ilcomportamento “di default”, in cui un processo padre dovrebbe attendere la terminazione dei processifigli con la funzione wait().

I nuovi passi per scrivere un server sono quindi:

1. se necessario, andare in background (daemon mode)

2. creare un socket

3. memorizzare in una ’struct sockaddr in’ l’indirizzo di binding

4. legare la socket creata al punto 1. all’indirizzo di binding specificato al punto 2. con bind()

5. mettersi in ascolto con listen()

6. accettare le connessioni in arrivo con accept()

7. effettuare una fork()

8. il processo padre ritorna subito al punto 6.

9. il processo figlio invece gestisce e alla fine chiude la connessione. La terminazione della connessionecoincide con la terminazione del processo figlio che la gestiva.

18 CAPITOLO 3. CONNESSIONI TCP/IP

Notate come il processo padre rimane perennemente in attesa di connessioni, in un loop infinito. Il nostroserver non prevede un metodo esplicito di terminazione, e sara necessario killarlo da linea di comando.

Detto questo, siamo pronti per scrivere un piccolo server che rimane in attesa di connessioni sullaporta 6969. Il server chiedera all’utente di immettere il proprio nome e scrivera in risposta il nomeimmesso e l’indirizzo IP da cui proviene la connessione.

3.10 Un server basato su TCP: il codice

/*---------8<----------------------------------------------------------*/

#include <netdb.h> /* gethostbyaddr() */

#include <sys/types.h> /* socket() bind() accept() */

#include <sys/socket.h> /* socket() bind() listen() accept() */

#include <netinet/in.h> /* struct sockaddr_in */

#include <string.h>

#include <errno.h>

#include <signal.h>

#include <stdio.h>

extern int h_errno; /* variabile esterna per la gestione degli errori */

void gestisci_connessione(int, struct sockaddr_in *);

int main(void)

{

int s; /* il socket di ascolto */

int s2; /* il socket per la gestione delle connessioni*/

int raddrlen; /* la lunghezza della struttura raddr */

struct sockaddr_in laddr; /* il nostro indirizzo locale */

struct sockaddr_in raddr; /* indirizzo remoto di chi si collega */

/* evitiamo gli zombie */

signal(SIGCHLD, SIG_IGN);

/* andiamo in daemon mode */

switch(fork()){

case -1: /* errore */

perror("fork");

exit(1);

case 0: /* processo figlio */

setsid();

break;

default: /* processo padre */

exit(0);

}

/* creiamo una socket */

s = socket(PF_INET, SOCK_STREAM, 0);

if(s == -1){

perror("socket");

exit(1);

}

/* mettiamo in laddr il nostro indirizzo di binding */

memset(&laddr, 0, sizeof(laddr));

laddr.sin_family = AF_INET;

3.10. UN SERVER BASATO SU TCP: IL CODICE 19

laddr.sin_port = htons(6969);

laddr.sin_addr.s_addr = htonl(INADDR_ANY);

/* colleghiamo la socket all’indirizzo di binding */

if(bind(s, (struct sockaddr *) &laddr, sizeof(laddr)) == -1){

perror("bind");

exit(1);

}

/* mettiamoci in ascolto */

if(listen(s, 5) == -1){

perror("listen");

exit(1);

}

/* creiamo un ciclo infinito in attesa di connessioni */

while(1){

/* accettiamo le connessioni! */

raddrlen = sizeof(raddr);

s2 = accept(s, (struct sockaddr *) &raddr, &raddrlen);

if(s2 == -1){

perror("accept");

exit(1);

}

switch(fork()){

case -1: /* errore */

perror("fork");

exit(1);

case 0: /* processo figlio */

gestisci\_connessione(s2, &raddr);

exit(0);

default: /* processo padre */

close(s2); /* il padre non ne ha bisogno! */

break;

}

} /* fine while */

}

/*--------------------------------*/

void gestisci_connessione(int s2, struct sockaddr_in *raddr)

{

FILE *f1, *f2;

char buf[100];

/* associamo due stream al socket */

f1 = fdopen(s2, "r");

f2 = fdopen(s2, "w");

setbuf(f1, NULL);

setbuf(f2, NULL);

/* dialogo col client */

fprintf(f2, "Ciao! Scrivi il tuo nome:\n");

fgets(buf, 100, f1);

buf[strlen(buf) - 2] = ’\0’; /* eat newline */

20 CAPITOLO 3. CONNESSIONI TCP/IP

fprintf(f2, "Ciao %s, ti colleghi da %s\n",

buf, inet_ntoa(raddr->sin_addr));

/* chiudiamo gli stream e quindi il socket */

fclose(f1);

fclose(f2);

}

/*---------8<-----------------------------------------------------------*/

3.11 Come usare inetd

Inetd e un demone molto particolare che permette di semplificare notevolmente la scrittura di un server.Questo programma si mette in ascolto su determinate porte e, quando riceve una connessione ad unadi esse, esegue il server ad essa corrispondente, “passandogli la palla” e lasciandogli la gestione dellatrasmissione dei dati vera e propria. In altre parole inetd e un meta-server che raduna in se tutto ilcodice necessario per gestire le connessioni TCP, liberando cosı i singoli server dal gravoso compito dioccuparsene loro. I server lanciati da inetd infatti possono astrarre completamente dal livello del TCP/IP,poiche inetd collega i flussi di input e di output rispettivamente ai loro standard input e standard output.Questo significa che un server puo gestire la connessione come se leggesse l’input da tastiera e scrivessel’output a schermo, il tutto in maniera completamente trasparente.

L’utilizzo di inetd da alcuni importanti vantaggi, quali:

* maggiore semplicita di programmazione del server

* il server viene eseguito solo quando e necessario

* si possono usare filtri per accettare/rifiutare le connessioni, come il tcp-wrapper (tcpd).

Tuttavia comporta anche alcuni svantaggi:

* il server non e piu indipendente, per utilizzarlo e necessario appoggiarsi a inetd

* l’esecuzione del server ad ogni richiesta di connessione puo rivelarsi penalizzante in termini diprestazioni (e per questo che, ad esempio, i server web non vengono solitamente lanciati tramiteinetd)

Qual e la soluzione migliore per il vostro server? Se pensate che la richiesta di avere inetd installato nonsia troppo restrittiva e se ritenete che il server non dovra gestire molte connessioni, allora potete usareinetd e risparmiarvi un po’ di lavoro; se invece volete un programma autonomo o pensate che dovra gestireun numero elevato di connessioni, scrivete il vostro server partendo da zero. Spesso la soluzione miglioree quella di implementare entrambe le funzionalita, lasciando poi all’utente la possibilita di decidere seutilizzare il programma in modalita stand-alone o tramite inetd.

Vediamo ora come e possibile riscrivere il server di prima utilizzando inetd. Innanzitutto aggiungiamola seguente riga in /etc/inetd.conf

6969 stream tcp nowait nobody /tmp/mioserver mioserver

o, nel caso di xinetd:

service mioserver

{

port = 6969

socket_type = stream

protocol = tcp

wait = no

user = nobody

server = /tmp/mioserver

}

3.12. UN SERVER CHE UTILIZZA INETD: IL CODICE 21

e facciamo rileggere ad inetd il suo file di configurazione con il comando

$ killall -HUP inetd

o, nel caso di xinetd

$ killall -USR2 xinetd

Ora inetd e pronto ad eseguire il programma /tmp/mioserver ogni volta che verra richiesta una connes-sione sulla porta 6969. Il codice del server risulta molto semplificato, visto che sono state tolte tutte leparti di gestione della connessione TCP e ci si limita ad usare gli stream standard di input e output percomunicare con il client.

3.12 Un server che utilizza inetd: il codice

/*---------8<------------------------------------------------------------*/

#include <string.h>

#include <stdio.h>

#include <errno.h>

#include <netinet/in.h> /* struct sockaddr_in */

int main(void)

{

char buf[100];

struct sockaddr_in addr;

int addrlen;

/* disabilitiamo i buffer in input e output */

setbuf(stdin, NULL);

setbuf(stdout, NULL);

/* prendiamo l’indirizzo del client remoto. Poiche non usiamo piu

* la funzione accept() che salva in una struct sockaddr_in l’indirizzo

* dell’host remoto, dobbiamo ottenere questa informazione con una

* chiamata a getpeername(). NB: 0 e il file descriptor dello

* standard input. */

addrlen = sizeof(addr);

if(getpeername(0, &addr, &addrlen) == -1){

perror("getpeername");

exit(1);

}

/* dialogo col client */

printf("Ciao! Scrivi il tuo nome:\n");

/* potremmo usare gets, ma fgets permette di specificare

* un utile vincolo sulla dimensione del buffer */

fgets(buf, 100, stdin);

buf[strlen(buf) - 2] = ’\0’; /* eat newline */

printf("Ciao %s, ti colleghi da %s\n",

buf, inet_ntoa(addr.sin_addr));

exit(0);

}

/*---------8<------------------------------------------------------------*/

22 CAPITOLO 3. CONNESSIONI TCP/IP

Capitolo 4

UDP/IP

4.1 Funzionamento del protocollo UDP

A differenza di TCP, UDP e un protocollo connectionless, ovvero non orientato alle connessioni. Questosignifica che un pacchetto UDP, quando viene trasmesso, e “lasciato a se stesso”: nulla ci garantira il suoeffettivo arrivo a destinazione. In altre parole, con UDP vengono meno tutte quelle caratteristiche garan-tite da un protocollo orientato alla connessione come TCP. In questo UDP e molto simile al protocolloIP in cui viaggia incapsulato, rispetto al quale aggiunge solo il concetto di porta, come gia faceva TCP.

Per queste sue caratteristiche, UDP e utilizzato nei casi in cui il traffico sia limitato e non richiedaforme sofisticate di controllo; ad esempio le risposte dei DNS viaggiano spesso su UDP. In questo casoinfatti la perdita di un pacchetto non e critica in quanto il client puo sempre chiedere la ritrasmissionedei dati nel caso non siano arrivati entro un certo tempo limite.

E’ importante notare che ogni trasmissione dei dati con UDP comporta l’invio di un singolo datagram-ma. E’ infatti compito del programmatore spedire un datagramma alla volta, poiche UDP non fa nessuntipo di segmentazione automatica dei dati come invece accade con TCP. Se si limita la dimensione deidati spediti a 512 bytes, e garantito che il pacchetto UDP possa essere contenuto in un singolo pacchettoIP, mentre con dimensioni maggiori e possibile incorrere nella frammentazione dei datagrammi operatadal protocollo IP stesso. In ogni caso non e possibile superare la dimensione massima del datagramma IP,che in linea teorica e di 65535 bytes, anche se alcuni sistemi operativi non implementano correttamentelo stack IP e impongono restrizioni maggiori.

Quando possibile, e conveniente usare UDP, in quanto questo protocollo, vista la sua estrema sempli-cita, genera meno traffico di TCP. Ovviamente non sempre e possibile convivere con le pesanti restrizioniimposte da questo protocollo.

4.2 Un semplice client UDP

Il codice di un client UDP non presenta molte differenze rispetto ad uno basato su TCP. Le funzioni dausare sono le stesse, e bisogna solo avere l’accortezza di usare la costante ‘SOCK DGRAM‘ al posto di‘SOCK STREAM‘ nella chiamata a socket(). Una cosa che puo apparire a prima vista illogica e l’usodella connect(). Che senso ha usare una funzione che crea connessioni in un protocollo connectionless?Nel caso di UDP, connect() si limita a specificare l’indirizzo a cui i dati vanno trasmessi; tale indirizzosara anche l’unico da cui sara accettato del traffico in ingresso, di modo che un client possa ricevere dellerisposte solamente dal server che ha contattato.

Vediamo il codice di un client che legge la data e l’ora corrente tramite il servizio daytime (porta13/udp). Si tratta dello stesso servizio utilizzato precedentemente per il client TCP, e che e disponibileper entrambi i protocolli.

/*---------8<------------------------------------------------------------*/

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <netdb.h>

23

24 CAPITOLO 4. UDP/IP

#include <stdio.h>

#define BUF_SIZE 100

extern int h_errno;

int main(int argc, char **argv)

{

int s;

struct sockaddr_in addr;

struct hostent *host;

char buf[BUF_SIZE];

if(argc != 2){

printf("Uso: %s hostname\n", argv[0]);

exit(1);

}

/* cerchiamo l’indirizzo dell’host passato come parametro */

host = gethostbyname(argv[1]);

if(host == NULL){

herror("gethostbyname");

exit(1);

}

/* memorizziamo in addr l’indirizzo dell’host e la porta (13)

* a cui vogliamo connetterci */

memset(&addr, 0, sizeof(addr));

addr.sin_family = AF_INET;

addr.sin_port = htons(13);

memcpy(&addr.sin_addr, host->h_addr, host->h_length);

/* creiamo un socket adatto alle connessioni UDP */

s=socket(PF_INET, SOCK_DGRAM, 0);

if(s == -1){

perror("socket");

exit(1);

}

/* associamo al socket l’indirizzo remoto */

if(connect(s, (struct sockaddr *) &addr, sizeof(addr)) == -1){

perror("connect");

exit(1);

}

/* spediamo qualcosa in modo che il server sappia che

* deve risponderci! E’ lecito spedire un pacchetto vuoto */

send(s, NULL, 0, 0);

/* lettura di un unico pacchetto UDP */

recv(s, buf, BUF_SIZE, 0);

/* scriviamo a video il risultato */

printf("%s", buf);

/* chiudiamo il socket e usciamo */

close(s);

4.3. UN SEMPLICE SERVER UDP 25

exit(0);

}

/*---------8<------------------------------------------------------------*/

Notate come la versione UDP del protocollo daytime richieda che venga inizialmente spedito un pacchettodal client verso il server, altrimenti il server non avrebbe modo di sapere che c’e qualcuno in attesa deidati, e in ogni caso non saprebbe a quale indirizzo spedirli!

La seconda cosa da sottolineare e l’utilizzo di send() e recv(). UDP infatti non fornisce l’astrazione del“flusso di byte” da trasmettere tipica di TCP, quindi non avrebbe senso usare in questo caso gli stream.Inoltre, come gia detto precedentemente, l’invio di dati con UDP comporta sempre la trasmissione di ununico pacchetto, che si puo facilmente gestire anche con queste due funzioni di basso livello.

4.3 Un semplice server UDP

Passiamo ora alla programmazione di un server basato su UDP; per semplicita ci limiteremo a scrivereun server che rispedisce indietro al mittente tutto quello che riceve. La creazione del socket ed il suobinding all’indirizzo locale su cui rimanere in ascolto e analoga al caso del server TCP, eccetto per il fattoche il socket deve essere di tipo ‘SOCK DGRAM‘. Successivamente tuttavia non sono piu necessarie lefunzioni listen() e accept() per il semplice fatto che non ci sono connessioni da accettare, poiche UDP eun protocollo connectionless. Per lo stesso motivo, non e necessario usare una fork() per gestire le variecomunicazioni: sara sufficiente un server iterativo che di volta in volta riceve un pacchetto e risponde almittente.

Ma, una volta ricevuti dei dati, com’e possibile sapere qual e l’indirizzo del mittente a cui vogliamorispondere? In altre parole, come possiamo sapere chi e che ci spedisce dei dati? In questo caso bisognautilizzare delle funzioni di lettura/scrittura adatte allo scopo:

header:

#include <sys/types.h>

#include <sys/socket.h>

dichiarazioni:

int recvfrom(int s, void *buf, size_t len, int flags,

struct sockaddr *from, socklen_t *fromlen);

int sendto(int s, const void *msg, size_t len, int flags,

const struct sockaddr *to, socklen_t tolen);

parametri:

s : socket

*buf/*msg: buffer dei dati da spedire/ricevere

len : numero di byte da spedire/ricevere

flags : flag aggiuntivi

*from/*to: indirizzo destinatario/mittente

*fromlen/tolen: dimensione della struttura puntata da *from/*to

valori di ritorno:

il numero di byte letti/scritti, -1 in caso di errore.

Queste due funzioni sono analoghe a send() e recv(), tranne per gli ultimi due parametri: si tratta diuna ’struct sockaddr’ e della relativa lunghezza, in cui viene memorizzato l’indirizzo da cui i dati sonostati ricevuti (nel caso di recvfrom()) o a cui devono essere spediti (nel caso di sendto()). Sebbenequeste funzioni siano teoricamente utilizzabili con qualsiasi tipo di socket, per ovvi motivi si rivelano utilisoprattutto nell’utilizzo di protocolli connectionless.

Detto questo, abbiamo tutti i mezzi per scrivere il server:

/*---------8<------------------------------------------------------------*/

#include <sys/types.h>

#include <sys/socket.h>

26 CAPITOLO 4. UDP/IP

#include <netinet/in.h>

#include <stdio.h>

#define BUF_SIZE 100

int main(void)

{

int s;

struct sockaddr_in laddr, raddr;

int raddrlen;

char buf[BUF_SIZE];

int r;

/* mettiamo in laddr il nostro indirizzo di binding */

memset(&laddr, 0, sizeof(laddr));

laddr.sin_family = AF_INET;

laddr.sin_port = htons(6969);

laddr.sin_addr.s_addr = htonl(INADDR_ANY);

/* creiamo una socket */

s=socket(PF_INET, SOCK_DGRAM, 0);

if(s == -1){

fprintf(stderr, "socket() error\n");

exit(1);

}

/* colleghiamo la socket all’indirizzo di binding */

if(bind(s, (struct sockaddr *) &laddr, sizeof(laddr)) == -1){

perror("bind");

exit(1);

}

/* ciclo lettura / risposta */

while(1)

{

/* recvfrom necessaria per sapere a chi spedire dopo!! */

/* NB: recvfrom e bloccante */

r = recvfrom(s, buf, BUF_SIZE, 0,

(struct sockaddr *) &raddr, &raddrlen);

/* in raddr abbiamo l’indirizzo del mittente. Usiamolo */

/* per rispedirgli i dati che ci ha trasmesso */

sendto(s, buf, r, 0, (struct sockaddr *) &raddr, raddrlen);

}

close(s);

exit(0);

}

/*---------8<------------------------------------------------------------*/

Capitolo 5

Approfondimenti

Per qualsiasi dubbio riguardante le funzioni qui menzionate, si rimanda il lettore alle relative man page,sempre molto dettagliate e precise. Una serie di articoli introduttivi alle tematiche qui affrontate sonoreperibili su DEV n. 34, ottobre 1996; da essi e stata tratta la maggior parte della documentazionenecessaria per la stesura di questo testo. La bibbia del network programming e invece “Unix NetworkProgramming” voll. I & II, del compianto W. R. Stevens. Per chi puo permettersela. Per chi volesseapprofondire invece il funzionamento dei protocolli IP, UDP e TCP, il testo sacro e “TCP/IP illustrated”voll. I, II & III, sempre di Stevens.

27