Programmazione socket
2-1
Programmazione socket
Obiettivo: imparare a costruire applicazioni
client/server che comunicano tramite socket
Socket API
introdotte in UNIX BSD4.1,
1981
create, utilizzate e
rilasciate esplicitamente
dalle applicazioni
paradigma client/server
due tipi di servizi di
trasporto via API socket:
unreliable datagram
reliable, byte streamoriented
socket
un’interfaccia situata
nell’host, creata
dall’applicazione e
controllata dal SO
attraverso la quale un
processo applicativo può
sia inviare che ricevere
messaggi a/da un altro
processo applicativo
situato in un altro host
2-2
I due tipi principali di socket
SOCK_STREAM
SOCK_DGRAM
TCP
affidabile
ordine dati garantito
orientato alla connessione
bidirezionale
App
3 2
1
UDP
inaffidabile
nessuna garanzia su ordine dati
nessuna nozione di “connessione”
– l’applicazione indica la
destinazione di ogni pacchetto
può inviare o ricevere
App
3
socket
2
D1
2
1
1
Dest.
3 2
1
socket
D2
3
D3
2-3
Creazione di socket in C: socket
int s = socket (domain, type, protocol);
s: socket descriptor, un intero (come un file-handle)
domain: intero, dominio di comunicazione
• es., AF_INET (IPv4 protocol) – usato di solito
type: tipo di comunicazione
• SOCK_STREAM: affidabile, 2-vie, connection-based
• SOCK_DGRAM: inaffidabile, connectionless
• altri valori: servono permessi root, usati raramente o
obsoleti
protocol: specifica il protocollo, di solito settato a 0
(vedere file /etc/protocols per una lista di opzioni)
NOTA: Una chiamata socket non specifica da dove
verranno i dati o dove andranno, crea solamente
un’interfaccia!
2-4
La funzione bind
associa e riserva un port alla socket
int status = bind (sockid, &addrport, size);
status: error status, = -1 se il bind fallisce
sockid: intero, socket descriptor
addrport: struct sockaddr, l’indirizzo (IP) e il port
della macchina
• es. indirizzo: INADDR_ANY sceglie l’indirizzo locale
• es. port: 0 lascia al SO il compito di stabilire il port
size: la dimensione (in byte) della struttura
addrport
usato dal server (opzionalmente dal client)
2-5
Quando usare il bind
SOCK_DGRAM:
in trasmissione il bind non è necessario. Il SO
trova un port ogni volta che la socket manda un
pacchetto
in ricezione il bind è necessario
SOCK_STREAM:
la destinazione è determinata durante il setup
di connessione
non occorre conoscere il port attraverso cui
vengono inviati i dati (durante il setup di
connessione l’estremità ricevente è informata
sul port del mittente)
2-6
Connection Setup
(SOCK_STREAM)
Ricordare: nessun connection setup per il
SOCK_DGRAM
I partecipanti alla connessione sono di due tipi:
passivo: aspetta che un partecipante attivo richieda la
connessione
attivo: inizia la richiesta di connessione verso il lato
passivo
Una volta che la connessione è stabilita, i
partecipanti attivi e passivi sono “simili”
entrambi possono mandare e ricevere dati
ognuno può terminare la connessione
2-7
Connection setup (cont.)
Participante passivo (es.
server)
step 1: listen (per arrivo di
richieste)
step 3: accept (una
richiesta)
step 4: trasferimento dati
La connessione viene
accettata su una nuova
socket
La vecchia socket continua
ad aspettare la connessione
di nuovi partecipanti
Three way handshaking
Participante attivo (es.
client)
step 2: richiede &
stabilisce connection
step 4: trasferimento dati
Passive Participant
a-sock-1
l-sock
a-sock-2
socket
socket
Active 1
Active 2
2-8
Connection setup: listen & accept
Usate dal partecipante passivo (server)
int status = listen (sock, queuelen);
status: 0 se si mette in ascolto, -1 se dà errore
sock: intero, socket descriptor
queuelen: intero, numero di partecipanti attivi che possono
“aspettare” per una connessione
listen è non-blocking: ritorna immediatamente
int s = accept (sock, &name, namelen);
s: intero, la nuova socket (usata per il trasferimento dati)
sock: intero, la socket originale, usata come prototipo per s
name: struct sockaddr, indirizzo del partecipante attivo
namelen: sizeof(name): valore/risultato
• deve essere settato in maniera appropriata prima della chiamata
• aggiustato dal SO quando la funzione ritorna
accept è blocking: aspetta una connessione prima di ritornare
2-9
connect call
Usata dal partecipante attivo (client)
int status = connect (sock, &name, namelen);
status: 0 se connessione OK, -1 altrimenti
sock: intero, socket da essere utilizzata nella
connessione
name: struct sockaddr: indirizzo del partecipante
passivo
namelen: intero, sizeof(name)
connect è blocking
2-10
Sending / Receiving Data
Con connessione (SOCK_STREAM):
int count = send (sock, &buf, len, flags);
•
•
•
•
int count = recv (sock, &buf, len, flags);
•
•
•
•
count: Numero byte trasmessi (-1 se errore)
buf: char[ ], buffer da trasmettere
len: intero, lunghezza buffer (in byte) da trasmettere
flags: intero, opzioni speciali, di solito settate a 0
count: Num. byte ricevuti (-1 se errore)
buf: void[ ], immagazzina i byte ricevuti
len: intero, lunghezza buffer (in byte)
flags: intero, opzioni speciali, di solito settate a 0
Le chiamate sono blocking [ritornano solo dopo
che i dati sono inviati (al socket buf) / ricevuti]
2-11
Sending / Receiving Data
(cont.)
Senza connessione (SOCK_DGRAM):
int
count = sendto (sock, &buf, len, flags, &addr, addrlen);
• count, sock, buf, len, flags: stesse di send
• addr: struct sockaddr, indirizzo della destinazione
• addrlen: sizeof(addr)
int
count = recvfrom (sock, &buf, len, flags, &addr, addrlen);
• count, sock, buf, len, flags: stesse di recv
• name: struct sockaddr, indirizzo della sorgente
• namelen: sizeof(name): valore/risultato
Le
chiamate sono blocking [ritornano solo dopo che i dati
sono inviati (al socket buf) / ricevuti]
2-12
close
Quando si finisce di utilizzare una socket, la
socket dovrebbe essere chiusa:
status = close (s);
status: 0 se OK, -1 se errore
s: il socket descriptor (della socket da chiudere)
Chiusura di una socket
Chiude una connessione (per SOCK_STREAM)
Libera il port utilizzato dalla socket
2-13
Client/server socket interaction: TCP
Server
Client
crea socket
socket()
crea socket
socket()
associa port
bind ()
aspetta richieste
listen ()
accetta richieste
accept ()
TCP
connection setup
richiedi connessione
connect ()
manda dati
send ()
ricevi dati
recv ()
manda dati
send ()
chiudi socket
close ()
ricevi dati
recv ()
chiudi socket
close ()
2-14
Client/server socket interaction: UDP
Server
Client
crea socket
socket()
crea socket
socket()
associa port
bind ()
manda dati
sendto ()
ricevi dati
recvfrom ()
manda dati
sendto ()
chiudi socket
close ()
ricevi dati
recvfrom ()
chiudi socket
close ()
2-15
La struct sockaddr
Generica:
struct sockaddr {
u_short sa_family;
char sa_data[14];
};
sa_family
• specifica quale
famiglia di indirizzi
deve essere usata
• determina come i 14
byte rimanenti
saranno utilizzati
Specifica Internet:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_family = AF_INET
sin_port: port # (0-65535)
sin_addr: IP address
sin_zero: non utilizzato
//Structure per ragioni storiche
struct in_addr {
u_long s_addr; //32-bit long
};
2-16
Byte-ordering di indirizzo e port
Indirizzo e port sono memorizzati come interi
u_short sin_port; (16 bit)
in_addr sin_addr; (32 bit)
Problema:
Macchine/SO usano differenti modalità per memorizzare i dati
• little-endian: lower bytes first
• big-endian: higher bytes first
Queste macchine devono poter comunicare l’una con l’altra
attraverso la rete
128.119.40.12
128
Big-Endian
machine
119
40
12
Little-Endian
machine
128
119
12.40.119.128
40
12
2-17
Soluzione: Network Byte-Ordering
Definizioni:
Host Byte-Ordering: il byte ordering usato
dall’host (big o little)
Network Byte-Ordering: il byte ordering usato
dalla rete – sempre big-endian
Ogni word inviata attraverso la rete dovrebbe essere
convertita in Network Byte-Order prima della
trasmissione (e viceversa in Host Byte-Order una
volta riceuta)
D: Le socket devono effettuare la conversione
automaticamente?
D: Dato che per le macchine big-endian non servono
routine di conversione e per le macchine little-endian sì,
come si può evitare di scrivere due versioni di codice?
2-18
Funzioni di byte-ordering
u_long htonl(u_long x);
u_long ntohl(u_long x);
u_short htons(u_short x);
u_short ntohs(u_short x);
Sulle macchine big-endian, queste routine non fanno
nulla
Sulle macchine little-endian, invertono il byte order
Big-Endian
Little-Endian12 40
128.119.40.12
119 128
machine
128
119
40
12
128.119.40.12
machine
119
40
12
128
119
40
ntohl
128
12
Lo stesso codice funziona indipendentemente dal tipo
di “endian” della macchina
2-19
Altre funzioni utili
atoi (char* s): converte la stringa s in un intero
bcopy (void* s, void* d, int n): copia n byte di s in d
bzero (char* c, int n): pone n byte a 0 a partire dal
valore puntato da c
gethostname (char *name, int len): ritorna il nome
dell’host sui cui il processo risiede
gethostbyname (char *name): converte l’hostname
in una struttura (hostent) contenente l’indirizzo IP
(utilizzando il servizio di DNS)
2-20
Server : Inizializzazione
#include <sys/types.h>
[…altri include…]
#define MAX_CODA 5
/* massimo backlog */
main(int argc, char* argv[]) /* prende in input la porta */
{
int sock;
/* socket in attesa */
int sockmsg; /* socket servente */
struct sockaddr_in server;
if ( argc != 2 ) {
printf("uso: %s <numero-della-porta>\n", argv[0]);
exit(EXIT_FAILURE);
}
sock = socket(AF_INET,SOCK_STREAM,0); /* socket prototipo */
if( sock <0 ) {
printf("server: errore %s nella creazione del socket\n",
strerror(errno));
exit(EXIT_FAILURE);
}
2-21
Server: Creazione della coda
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(atoi(argv[1]));
struttura per
il bind
if( bind(sock, (struct sockaddr *)&server, sizeof(server)) )
{
printf("server: bind fallita\n");
exit(EXIT_FAILURE);
}
printf("server: rispondo sulla porta %d\n",
ntohs(server.sin_port));
if( listen(sock, MAX_CODA) <0 ) {
printf("server: errore %s nella listen\n",
strerror(errno));
exit(EXIT_FAILURE);
}
dimensiono
la coda di
backlog
2-22
Server: Gestione delle connessioni
int totale=0;
char input[256];
sockmsg = accept(sock, 0, 0);
if( sockmsg <0 ) {
printf("errore %s nella accept\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("server: accetto una nuova connessione\n”);
qui ci va il codice che presta il servizio (segue)
}
close(sock);
printf("server: ho chiuso il socket\n");
/* fine della funzione main */
2-23
Server: Gestione del client
{ /* questo e’ il codice di servizio del server */
int len;
printf("server %d: iniziato \n", getpid() );
while( len = recv(sockmsg, input, sizeof(input), 0) ) {
int numero;
char tot[256];
input[len]='\0'; /* termina la stringa*/
numero = atoi(input); /* converti in intero */
printf("server: arrivato il numero: %d\n", numero);
totale=totale+numero; /* calcolo totale*/
sprintf(tot, "%d", totale); /* prepara la stringa */
send(sockmsg, tot, sizeof(tot), 0); /* invia la stringa */
}
close(sockmsg); /* prima di uscire chiudi il socket */
printf("server: socket chiuso\n”);
exit(EXIT_SUCCESS); /* connessione terminata */
}
2-24
Client: Inizializzazione
#include <stdio.h>
[…altri include…]
main(int argc, char* argv[])
{
int sock;
/* descrittore del socket */
struct sockaddr_in server;
struct hostent *hp;
char input[256];
if(argc!=3) {
printf("uso: %s <host> <numero-della-porta>\n", argv[0]);
exit(1);
}
sock = socket(AF_INET, SOCK_STREAM, 0);
if( sock < 0 ) {
printf("client: errore %s nella creazione del socket\n",
strerror(errno));
exit(1);
}
2-25
Client: Connessione col server
hp = gethostbyname(argv[1]);
if( hp == NULL ){
printf("client: l'host %s non e' raggiungibile.\n",
argv[1]);
exit(1);
}
server.sin_family = AF_INET;
bcopy(hp->h_addr, &server.sin_addr, hp->h_length);
server.sin_port = htons(atoi(argv[2]));
if( connect(sock, (struct sockaddr *)&server, sizeof(server))
< 0 )
{
printf("client: errore %s durante la connect\n",
strerror(errno));
exit(1);
}
printf("client: connesso a %s, porta %d\n", argv[1],
ntohs(server.sin_port));
2-26
Client: Gestione messaggi
printf("client: num. o ‘quit’? ");
scanf("%s",&input);
while( strcmp(input,"quit") != 0 )
{
char result[256];
if( send (sock, (char *)&input, strlen(input), 0) <0) {
printf("errore %s durante la write\n", strerror(errno));
exit(1);
}
if( recv(sock,(char *)&result, sizeof(result), 0) < 0 ) {
printf("errore %s durante la read\n", strerror(errno));
exit(1);
}
printf("client: ricevo dal server %s\n", result);
printf("client: num. o \"quit\"? ");
scanf("%s",&input);
}
close(sock);
printf("client: ho chiuso il socket\n");
}
/* fine della funzione main */
2-27
Gestione del blocco delle funzioni
Molte delle funzioni esaminate si bloccano finchè
accade un determinato evento
accept: fino all’arrivo di una connessione
connect: fino a quando la connessione non è stabilita
recv, recvfrom: fino a quando un pacchetto (di dati) non è
ricevuto
send, sendto: fino a quando i dati non vengono messi nel
buffer della socket
Per semplici programmi il blocco è conveniente
Cosa accade ai programmi più complessi?
connessioni multiple
invio e ricezione contemporaneo
necessità di eseguire in contemporanea codice non legato
alla rete
2-28
Gestione blocco delle funzioni (cont.)
Opzioni:
creazione di codice multi-process o multi-threaded
“eliminazione” del blocking (es., usando la funzione di
controllo del file descriptor fcntl)
uso della funzione select
Cosa fa la select?
si può bloccare permanentemente, per un intervallo
limitato o non bloccarsi
input: un set di file-descriptor
output: info sullo stato dei file-descriptor
cioè, può identificare le socket che sono “pronte all’uso”:
le funzioni che coinvolgono quelle socket ritornano
immediatamente
2-29
select function call
int status = select (nfds, &readfds, &writefds,
&exceptfds, &timeout);
# di oggetti pronti, -1 se errore
nfds: 1 + il numero del più grande file descriptor da
controllare
readfds: lista dei descrittori “pronti alla lettura”
writefds: lista dei descrittori “pronti alla scrittura”
exceptfds: lista dei descrittori che registrano
un’eccezione
status:
timeout: intervallo dopo il quale la select ritorna,
anche se non c’è niente di pronto – può essere tra 0
e
(settare il parametro timeout a NULL per )
2-30
Da utilizzare con la select
select utilizza una struct fd_set per le liste dei
descrittori
è un vettore di bit
se il bit i è settato in [readfds, writefds, exceptfds], select
controllerà che il file descriptor (cioè la socket) i è
pronta per [reading, writing, exception]
Prima di chiamare select:
FD_ZERO (&fdvar): azzera la struttura
FD_SET (i, &fdvar): aggiunge il file descriptor i alla lista
FD_CLR (i, &fdvar): rimuove il file descriptor i dalla lista
Dopo aver chiamato select:
int FD_ISSET (i, &fdvar): booleano ritorna TRUE iff i è
“pronto”
2-31
Rilascio dei port
Qualche volta un’uscita “rude” da un programma
(es. ctrl-c) non rilascia il port correttamente
In ogni caso il port dovrebbe essere rilasciato
dopo alcuni minuti
Per ridurre la probabilità di questo inconveniente,
includere il codice seguente:
#include <signal.h>
void cleanExit(){exit(0);}
nel codice della socket:
signal(SIGTERM, cleanExit);
signal(SIGINT, cleanExit);
2-32