Alma Mater Studiorum - Universita' di Bologna
Sede di Cesena
Reti di Calcolatori
L’interfaccia socket
Vedi:
• D. Comer, Internetworking con TCP/IP - Principi, protocolli e
architetture, vol. 1, Addison-Wesley, capp. 21-22, pagg. 429-468.
• W.R. Stevens, Unix Network Programming, Prentice Hall, cap. 6,
pagg. 258-339.
• SunSoft, Network Interfaces Programmer's Guide, cap. 7, pagg.
219-243.
Copyright © 2006-2014 by Claudio Salati.
Lez. 4
1
Il modello di interazione client-server
• Il modello principale di organizzazione delle applicazioni di rete e'
quello client-server.
• Un server e' un programma che offre un servizio: nel nostro caso un
server offre un servizio tramite la rete.
• Un server, secondo il modello, si affaccia alla rete ad un indirizzo ben
noto (e.g. una porta ben nota) e rimane in attesa di richieste da parte
dei client.
 Analogia: negozio.
• Esisterebbero anche altri possibili modelli:
• Analogia: vendita porta a porta.
• Un client si affaccia alla rete ad un indirizzo ben noto.
• Un server si presenta al cliente per offrirgli il proprio servizio.
• In effetti le API di accesso al servizio di Trasporto sono neutre
rispetto al modello di funzionamento del server.
• In pratica, il paradigma utilizzato da tutti i server (e quello assunto
2
in questo corso) e’ quello del negozio.
Il modello di interazione client-server
• Se l’indirizzo su cui il server offre il proprio servizio e' una porta TCP la
prima cosa che un client deve fare per richiedere il servizio e'
connettersi al server.
• L'apertura della connessione e' una operazione sbilanciata:
• il server si dichiara disposto ad accettare richieste di connessione
da parte di clienti (apre la connessione in modo passivo).
Il server non conosce a priori l'identita' dei suoi clienti.
Analogia: il negoziante apre la saracinesca.
• un client chiede in modo attivo l'apertura della connessione con il
server.
Il client deve conoscere a priori l'identita' (l’indirizzo) del server.
Analogia: il cliente entra nel negozio (grazie al fatto che conosce
l’indirizzo del negozio e che la saracinesca e’ aperta!).
• L'apertura della connessione implica un rendez-vous tra client e server.
• La vita di un server si prolunga normalmente oltre il tempo
dell'interazione con il singolo client.
3
API di accesso al servizio di Trasporto
• Come fanno i programmi applicativi (client o server) ad accedere ai
servizi di rete?
• In particolare:
• I protocolli TCP e UDP implementano il servizio di Trasporto di
Internet, rispettivamente COTS e CLTS.
• Attraverso quale Application Programming Interface (API) questi
servizi sono davvero utilizzabili?
• Nessuno standard o RFC di Internet definisce un'API per accedere ai
servizi di Trasporto.
 Anche perche’ la definizione sarebbe comunque “language specific”.
• Fortunatamente esiste uno standard de facto (nei linguaggi C e Java):
l'interfaccia (API) socket.
• L'interfaccia (API) socket e' disponibile non solo su Unix ma anche su
Windows.
• La disponibilita' universale dell'interfaccia socket rende possibile la
portabilita' dei programmi di rete (a parita’ di linguaggio utilizzato).
4
API, servizi, protocolli
.1
• Di norma le API del servizio di Trasporto sono (pensate per essere)
multiprotocollo, cioe' capaci di gestire diverse famiglie (stack) di
protocolli:
• non solo una API (e.g. socket o TLI) consente di accedere sia al
COTS che al CLTS di Internet (TCP e UDP, rispettivamente),
• essa consente di accedere anche al servizio di Trasporto di altri
stack di protocolli
• Xerox,
• OSI,
• Unix (comunicazioni interne ad un singolo sistema di elaborazione,
tra processi che risiedono su uno stesso calcolatore),
• ...
• Viceversa, un servizio di Trasporto puo' essere utilizzabile attraverso
diverse API:
• Il servizio di Trasporto internet (sia COTS che CLTS) puo' ad
esempio essere acceduto sui sitemi Unix/C sia attraverso l'API
5
socket che attraverso l'API TLI.
.1’
API, servizi, protocolli
socket API
TCP
UDP
Unix
AF_inet
OSI
127.0.0.1
loopback
IP
Xerox NS
A1.A2.A3.A4
Ethernet
6
.1”
API, servizi, protocolli
TLI API
TCP
UDP
Unix
AF_inet
OSI
127.0.0.1
loopback
IP
Xerox NS
A1.A2.A3.A4
Ethernet
7
Comunicazioni locali
socket API
TCP
UDP
Unix
AF_inet
OSI
127.0.0.1
loopback
IP
Xerox NS
A1.A2.A3.A4
Ethernet
8
API, servizi, protocolli
.2
• Due applicazioni basate su diversi servizi/protocolli di Trasporto non
possono interoperare tra loro.
• Una applicazione che utilizza il CLTS Internet (UDP) non puo'
interoperare con una applicazione che utilizza il COTS Internet
(TCP).
• Una applicazione che utilizza il COTS Internet (TCP) non puo'
interoperare con una applicazione che utilizza il COTS OSI
(TP4).
• Due applicazioni basate su uno stesso servizio/protocollo di
Trasporto possono interoperare tra loro anche se accedono al
servizio attraverso API diverse.
• Una applicazione che utilizza il COTS Internet (TCP) tramite
l'API socket puo' interoperare senza problemi con una
applicazione che utilizza il COTS Internet (TCP) tramite l'API TLI.
• Una applicazione Java che utilizza il COTS Internet (TCP)
tramite l'API socket-Java puo' interoperare senza problemi con
una applicazione C che utilizza il COTS Internet (TCP) tramite
9
l'API TLI (o socket C).
Unix I/O
• In Unix tutto l'I/O viene tradizionalmente assimilato a operazioni sul
file system.
• Pertanto per operare su un dispositivo di I/O (su un device driver),
come su di un file, bisogna:
• Collegarsi al dispositivo (risorsa reale) tramite una system call
open(), alla quale si indica il nome del dispositivo, e che
restituisce una handle detta file descriptor (un intero non
negativo di piccola dimensione, che rappresenta il riferimento al
dispositivo all’interno al processo che ha eseguito la open()).
• Operare sul dispositivo (citato tramite la handle relativa) come
desiderato tramite le system call read() e write().
• Terminare l'accesso al dispositivo (alla risorsa reale) tramite la
system call close().
• Le system call read(), write() e close() riferiscono il file /
dispositivo tramite il suo file descriptor (la sua handle), restituito
dalla system call open().
• L’accesso contemporaneo da parte di piu’ processi ad una stessa
10
risorsa reale e’ disciplinato dal sistema.
I/O di rete
• Sarebbe conveniente che anche l'I/O di rete potesse assere trattato
come normale I/O locale e quindi assimilato all'accesso a file.
• Ci sono pero' delle particolarita' nell'I/O di rete:
• L'I/O di rete mette in comunicazione due attori, non un attore ed
una risorsa "passiva".
• I due attori hanno una relazione sbilanciata: ci sono due maniere
diverse di aprire il colloquio, "chi chiama" e "chi e' chiamato".
• Il colloquio puo' essere di due tipi: CO o CL. Nel caso di colloquio
CL l’API deve consentire di nominare oltre che la porta locale di
accesso alla rete anche la porta remota coinvolta nell’operazione
(che puo’ cambiare per ogni operazione).
• L'I/O di Unix e' stream oriented. Il colloquio TCP e' anch'esso
stream oriented, ma il colloquio UDP e il colloquio OSI (anche
quello COTS) sono record (message) oriented.
• L'API di trasporto deve supportare diversi protocolli di rete (al
normale I/O Unix non si chiede di supportare la nozione di file
record oriented del VMS).
11
L'API socket
.1
• E' stata definita considerando almeno 3 domini di comunicazione
(o Address Family o Protocol Family):
• Il dominio di comunicazione locale Unix: AF_UNIX / PF_UNIX
• Il dominio Internet: AF_INET / PF_INET
• Il dominio Xerox NS: AF_NS / PF_NS
• Esistono socket di diversi tipi, a seconda dello stile di comunicazione
cui si vuole accedere tramite il socket:
• SOCK_STREAM (si applica a AF_UNIX, AF_INET, AF_NS)
• SOCK_DGRAM (si applica a AF_UNIX, AF_INET, AF_NS)
• SOCK_RAW (si applica a AF_INET, AF_NS)
• SOCK_SEQPACKET (si applica a AF_NS)
• Ogni stile di comunicazione, quando e' supportato da un dominio di
comunicazione, e' implementato tramite uno o piu' protocolli.
 Ad esempio lo stile SOCK_RAW nel dominio Internet e' utilizzabile
12
tramite diversi protocolli, UDP, ICMP, IP…
L'API socket
•
.2
Un socket (di tipo ≠ SOCK_RAW) rappresenta
• un punto d'accesso ai servizi del Transport Layer
• secondo la semantica prevista
• dal tipo del socket e
• dal dominio di comunicazione sul quale il socket e' stato
definito
•
Pertanto
• un socket(AF_INET, SOCK_DGRAM) rappresenta una porta UDP
• un socket(AF_INET, SOCK_STREAM ) rappresenta una
connessione TCP
 Un socket e’ la rappresentazione a livello di programma di
un TSAP!
 N.B. in Linux esiste anche la protocol family AF_PACKET che
consente di accedere al servizio Data Link connectionless Ethernet:
in questo caso una porta rappresenta un DlSAP
13
L'API socket
.3
• Nel processo in cui e' stato creato, un socket e' riferito tramite un
socket descriptor.
• Un socket descriptor e' l'equivalente di un file descriptor, rappresenta
cioe’ una handle che consente di operare sul socket.
• In Unix:
• Un socket descriptor e' fatto come un file descriptor (un intero non
negativo di piccole dimensioni).
• Un socket rappresenta un particolare tipo di file.
• In Windows (interfaccia Winsock) un socket descriptor ha lo stesso
significato che in Unix ma non e’ rappresentato concretamente come
un intero di piccole dimensioni.
 Formalmente e’ un oggetto di tipo SOCKET, definito come “a descriptor
referencing the new socket”, ma lo si puo’ trattare come un int (ma non
di piccole dimensioni), che e’ la maniera di rappresentare un file decriptor
in Unix.
• Dal punto di vista dell'accesso in lettura/scrittura un socket supporta
(anche) le normali operazioni Unix su file di read() e write(). 14
Creazione di un socket
• Un socket e' creato tramite la system call
#include <sys/types.h>
#include <sys/socket.h>
int socket (int family, int type, int protocol);
• Nel dominio (family==) AF_INET valori possibili per protocol sono
• IPPROTO_UDP
• IPPROTO_TCP
• IPPROTO_ICMP
• IPPROTO_RAW (IP)
che sono definiti nell'header file <netinet/in.h>
• Nel dominio AF_UNIX non si cita il protocollo (protocol==0)
• Il protocollo puo' essere omesso (protocol ==0) anche quando il suo
valore e' determinato univocamente da quello dei due primi parametri.
• La system call ritorna il socket descriptor del socket che ha creato, un
intero di piccole dimensioni analogo a (e fatto come) un file descriptor.
15
Inizializzazione di un socket
• La system call socket() costruisce un oggetto socket
• gli assegna la handle (file descriptor) per riferirlo
• registra che l’oggetto riferito dal file descriptor e’ un socket (e non un file o
un dispositivo di I/O o un directory o …)
• registra il tipo di risorsa di rete cui il socket e’ associato (address family e
socket type)
ma non lo associa ad una risorsa reale.
• La system call open(), invece, opera anche l’associazione dell’oggetto file alla
risorsa reale acceduta tramite di esso.
int open (char *name, int flag);
// flag: 0=read-only, 1=write-only, 2=read+write
• name e’ l’identificativo della risorsa reale accessibile tramite l’oggetto file
creato dalla system call open() e riferibile tramite il file descriptor ritornato.
• Cosa succede se diversi processi tentano di accedere
contemporaneamente ad una stessa risorsa reale?
• Quale e’ la risorsa (reale) di rete associata al socket che e’ stato creato?
16
Assegnazione di un nome (risorsa reale di rete) ad un socket
• Appena creato un socket non e' collegato al nome di alcuna risorsa,
e non e' quindi associato ad alcuna risorsa reale (porta o file).
• Per essere utilizzato in operazioni di accesso ai servizi del proprio
dominio di comunicazione un socket deve essere collegato ad una
risorsa specifica (e.g. porta TCP o porta UDP, cioe’ un TSAP), il che
avviene associando al socket il nome (indirizzo di rete) della risorsa.
• Questo nome e' indicato come indirizzo del socket.
• L'associazione esplicita di una risorsa reale ad un socket avviene
tramite la system call
#include <sys/types.h>
#include <sys/socket.h>
int bind (int sockfd,
struct sockaddr *myaddr, int addrlen);
• Il secondo e il terzo parametro della system call specificano il nome
della risorsa a cui il socket deve essere associato.
17
Assegnazione esplicita ed implicita
• Nel dominio di comunicazione AF_INET e' anche possibile chiedere
esplicitamente il binding automatico del socket ad una porta casuale
non utilizzata all’istante corrente.
(una risorsa a caso, purche' disponibile e del tipo corretto, va bene).
• In alcuni casi l'associazione di un socket ad una risorsa puo' anche
avvenire in modo implicito/automatico:
 se il socket non e’ ancora collegato ad alcuna risorsa reale
l'associazione viene effettuata implicitamente dal sistema prima
di eseguire una operazione, richiesta dal programma cliente, di
accesso ai servizi del dominio di comunicazione.
• Le porte che possono essere scelte casualmente dal sistema per
essere associate in modo automatico ad un socket sono dette anche
porte effimere.
• La scelta operata dal sistema operativo, casuale, e’ effettuata
all’interno dell’insieme delle porte effimere di tipo congruente con
quello del socket che sono disponibili in quel momento (non sono 18
gia’ in uso).
Assegnazione di un nome ad un socket
.1
Quando e' che un socket deve essere associato (ovviamente in modo
esplicito) ad una specifica risorsa di rete?
• Un server deve presentarsi sulla rete con un indirizzo ben noto, in
modo che i suoi clienti possano raggiungerlo.
Questo e' vero sia per un server CO che per un server CL.
• Un client CO puo' avere un indirizzo di rete ben definito ma cio' non e‘
necessario, anzi.
• Un client CO puo' accontentarsi di un indirizzo effimero associato
implicitamente e automaticamente al suo socket (e.g. nel momento
in cui questo e' utilizzato per connettersi al socket di un server).
• Il server risponde all’interlocutore che si trova dall’altro lato della
connessione, senza bisogno di conoscerne esplicitamente l’identita’.
19
Assegnazione di un nome ad un socket
.1’
 L’utilizzo di una porta di rete fissa da parte di un client CO (in
particolare) risulta addirittura “pericoloso”.
• Non possono esserci su un nodo due istanze di uno stesso client che
utilizzino una stessa porta.
 Sarebbe un problema anche per un client UDP!
• Non possono esistere 2 connessioni TCP contemporanee tra una
stessa coppia di end-point.
 La riapertura della connessione da parte di un client con indirizzo
fisso fallisce se il server non ha ancora chiuso la connessione
precedente.
20
Assegnazione di un nome ad un socket
.2
Quando e' che un socket deve essere associato (ovviamente in modo
esplicito) ad una specifica risorsa di rete?
• Nel dominio di comunicazione AF_UNIX, a causa di un vincolo
implementativo, un client CL deve avere un indirizzo di rete ben
definito affinche’ un server cui esso si rivolge possa rispondergli a
quell'indirizzo.
• Nel dominio di comunicazione AF_INET il binding di client CL puo'
essere anche:
• Implicito e automatico a seguito della prima richiesta di
trasmissione sul socket.
• Esplicito e automatico ad una porta effimera.
• Il server risponde al mittente delle richieste, di cui deve leggere
l’identita’ nelle richieste stesse.
• Nel caso di un client CL (sia nel dominio AF_INET che in quello
AF_UNIX) l’utilizzo di una porta fissa non provoca alcun problema.
 Salvo quello gia’ indicato!
21
Porte .1
• Mentre il nome della porta di un client e' irrilevante, perche'
comunque il server gli risponde sulla porta/connessione mittente, il
nome della porta di un server e' fondamentale:
 un client non puo' mettersi in contatto con un server se non ne
conosce prima l'indirizzo!
• Un server deve avere quindi un indirizzo "ben noto" (well known)
in modo che i suoi client possano raggiungerlo.
 Esiste una alternativa:
un name service che traduca un nome (ben noto) di un servizio
nell'indirizzo di uno o piu' server che lo offrano.
• I port number da 1 a 1023 sono riservati per le porte well known
dei servizi di rete piu' importanti e piu' diffusi.
 Questi numeri di porta, come tutti i "numeri" importanti di
Internet sono gestiti dalla IANA (www.iana.org).
• I numeri di porta da 1024 a 5000 sono allocati dinamicamente dal
sistema (port number effimeri).
• Per realizzare un servizio privato si possono utilizzare i numeri di
22
porta da 5000 a 65535 (e.g. indirizzi di porte well known private).
Porte .2
• Notare che non esiste una porta 0.
• Come si fa a chiedere esplicitamente il binding automatico ad una
porta effimera?
 Chiedendo il binding alla porta 0.
• Nel binding automatico (ad una porta effimera):
•
Il sistema seleziona dinamicamente (in quel momento) una
porta effimera di tipo congruente con quello del socket e che
non sia gia’ in uso.
•
La porta viene occupata.
•
La porta viene associata al socket.
23
IANA
• IANA e' per esempio responsabile di assegnare:
• i protocol type utilizzati dai clienti IP
• gli Ethernet type utilizzati da IP sulla sottorete Ethernet
• In effetti le regole indicate prima per gli assegnamenti di porta
sono obsolete:
• Oltre che gestire l'assegnamento dei port number well-known
fino al numero 1023, IANA adesso registra anche l'utilizzo di
port number fino al numero 50000 (circa)
(e.g. per l’applicazione VNC il port number 5900).
• I numeri di porta rimasti liberi per il binding di porte effimere e
di porte locali sono quindi solo quelli oltre il 50000
• Esiste pero' una significativa differenza di autorevolezza tra i
numeri well-known e quelli registrati.
• Dal punto di vista dei sistemi operativi l'utilizzo dei numeri di porta
fino al 1023 e' di norma riservato a utenti di sistema.
24
Unix system programming
.1
• La maggior parte delle system call Unix ritornano un intero.
• Se l'intero ritornato e' non negativo
• l'esecuzione della system call ha avuto successo, e
• il particolare valore ritornato puo' avere un significato funzionale
(e.g. la system call socket() ritorna il socket descriptor del
socket che ha creato).
• Se l'intero ritornato e' negativo
• l'esecuzione della system call non ha avuto successo, e
• il particolare valore ritornato e' il codice della particolare situazione
di errore che e' stata riscontrata.
• Molte definizioni sono basate sulle typedef contenute nell'header file
<sys/types.h>.
typedef
typedef
typedef
typedef
unsigned
unsigned
unsigned
unsigned
char
short
int
long
u_char;
u_short;
u_int;
u_long;
// 1 byte
// 2 byte
// 4 byte
25
Unix system programming
.2
• In caso di errore durante l’esecuzione di una system call, il codice di
errore e’ disponibile anche tramite la variabile/espressione errno
(dichiarata nello header file <errno.h>).
• Nello header file <errno.h> e’ dichiarata anche la funzione
void perror(char *s);
perror()stampa su standard output s ed un messaggio di errore
definito dal sistema e corrispondente all’intero correntemente contenuto
in errno.
• Nelle dispense viene utilizzata la funzione (immaginaria?)
void err_dump(char *s);
err_dump() chiama perror() e quindi abortisce l’esecuzione del
processo chiamante (tramite system call exit()).
• In C un header file definisce e rende disponibile l’interfaccia di un
modulo
• Per poter utilizzare un identificatore dichiarato in un certo header file
un modulo cliente deve #includere lo header file.
26
• E’ l’analogo dell’import di un modulo in Java.
Indirizzo di un socket
• L'indirizzo di un socket e' descritto dalla struttura dati
#include <sys/socket.h>
struct sockaddr {
u_short sa_family;
char
sa_data[14]; };
• Questa e' una struttura dati astratta, che si incarna in diverse strutture
dati concrete caratteristiche di ciascun dominio di comunicazione (o
address family, AF_xxx).
• Ad esempio per un socket AF_UNIX
#include <sys/un.h>
struct sockaddr_un {
u_short sun_family; // AF_UNIX
char
sun_path[108]; };
• N.B.:
sizeof(struct sockaddr_un)!=sizeof(struct sockaddr)
27
Indirizzo di un socket AF_INET
• Un socket AF_INET e’ definito come
#include <sys/socket.h>
struct sockaddr_in {
u_short
u_short
struct in_addr
char
dove
sin_family; // AF_INET
sin_port;
sin_addr;
sin_zero[8]; /* pad */ };
struct in_addr {
u_long s_addr; };
// indirizzo IP
• sin_port e s_addr sono espressi in network byte order!
• quando si passa un socket concreto al posto di un sockaddr
• lo si fa tramite type-cast
• si indica la lunghezza effettiva della struttura passata
• per un sockaddr_un questa lunghezza indica di quanti char e'
28
effettivamente composto sun_path, che non e' null-terminated
Indirizzo di un socket: INADDR_ANY
• Su Internet un nodo puo' avere diversi indirizzi IP.
• A quale/i di questi indirizzi ci si deve associare quando si chiama la
system call bind()?
• Notare che questa system call consente di indicare un solo indirizzo IP
(in sock_addr.sin_addr.s_addr)!
E che un socket puo’ essere bind-ato an un solo sock_addr!
• Questo significa che un programma puo' ricevere solo dati indirizzati ad
uno particolare degli indirizzi del nodo?
• In effetti:
• Se nella primitiva bind() si indica un particolare indirizzo IP del
nodo, allora il socket accetta solo comunicazioni indirizzate a quel
particolare indirizzo.
• E' pero' possibile specificare come indirizzo IP la costante simbolica
INADDR_ANY:
 in questo caso il socket viene associato a tutti gli indirizzi IP
del nodo.
29
Indirizzo di un socket: INADDR_ANY
• N.B.: INADDR_ANY (== (u_long)0) e’ gia’ definito come “in
network byte order”
• Ovviamente INADDR_ANY
• Non puo' essere utilizzato per indirizzare una porta remota:
 Per fare cio' bisogna utilizzare uno degli indirizzi IP del nodo
remoto (un suo indirizzo IP specifico).
• Non puo’ essere utilizzato come valore nell’indirizzo mittente
 Comunicazioni che partono tramite il socket utilizzeranno
come indirizzo IP mittente uno degli indirizzi specifici del
nodo, e.g. quello sulla sottorete utilizzata per quella
comunicazione.
•
Domanda: come mai nella definizione di sockaddr_in (quindi
nella specifica della struttura dell’indirizzo di TSAP in internet) non
compare l’indicazione del protocollo di trasporto cui la porta e’
relativa?
30
bind(), INADDR_ANY, e porte effimere
• Quando si utilizza la system call bind() per asssociare un socket ad
una porta effimera, al parametro *myaddr viene normalmente
assegnato dal chiamante il valore 0.0.0.0:0 (cioe’ INADDR_ANY:0).
• Notare che l’indirizzo 0.0.0.0:0 non e’ un indirizzo valido (vero), e che
non e’ utilizzabile come indirizzo destinazione in una operazione di rete:
 INADDR_ANY non e’ un indirizzo di rete valido.
 La porta numero 0 non esiste.
•
Effettuare il bind all’indirizzo 0.0.0.0:0 non significa chiedere
effettivamente l’associazione a questo indirizzo (che non e’ un indirizzo
valido), ma chiedere l’associazione a una qualunque porta effimera
libera e a tutti gli indirizzi IP della macchina.
•
Nella system call bind() il parametro *myaddr e’ di ingresso, non di
ingresso/uscita.
•
Ma allora come posso sapere a quale porta effimera il socket e’ stato
effettivamente associato? Perche’ dovrebbe interessarmi?
 Vedi seguito delle dispense.
31
Connessione attiva al socket remoto
• L’associazione di un oggetto file ad una risorsa reale del file system e’
sufficiente al programma per potere operare (leggere/scrivere) sulla
risorsa reale tramite l’oggetto file.
• L’associazione di un socket ad una risorsa di rete locale (ad una
porta, tramite bind()) non e’ sufficiente per potere comunicare sulla
rete:
 Con chi si vuole comunicare?
 Bisogna identificare il proprio interlocutore!
 N.B.: per un socket TCP l’associazione creata con la bind() e’
con una porta locale connessa alla porta remota 0.0.0.0:0;
il socket e’ cioe’ associato con una porta locale non connessa!
• Per potere comunicare con un server CO un client deve stabilire una
connessione con lui.
• N.B.: l’indirizzo di rete del pari remoto e’ necessario anche per
comunicazioni CL (UDP), anche se in questo caso non e’ necessario
costruire un circuito virtuale per comunicare con lui.
32
Connessione attiva al socket remoto
• Un client richiede in modo attivo la connessione ad un porta remota
(ad un pari remoto) tramite la system call
#include <sys/types.h>
#include <sys/socket.h>
int connect (int sockfd,
struct sockaddr *addr, int addrlen);
in cui il secondo e il terzo parametro indicano l'indirizzo del pari
remoto (del TSAP) con cui ci si vuole connettere.
• Se sockfd e' un socket SOCK_STREAM invocare connect()
significa (chiedere al Layer di Trasporto locale di) stabilire una
connessione di Trasporto tra i due end-point citati:
• La porta locale associata al socket sockfd
• La porta (il TSAP) remota indicata da addr
• Un cliente CO non deve necessariamente bindare il proprio socket
prima di chiamare connect() (binding automatico implicito).
33
Socket e porte
• Quando un socket viene utilizzato per accedere alla rete deve
comunque essere associato ad una porta.
• Pertanto, nel caso che un client (CO o CL) invochi la connect() su di
un socket che non e' stato ancora bindato (ed e' questo in pratica il
comportamento normale di un client CO) il sistema si occupa di
effettuare implicitamente il bind automatico del socket ad una delle porte
effimere correntemente libere prima di dare corso alla connect().
• Lo stesso discorso, in caso di socket CL, vale anche se si cerca di
inviare un datagram tramite un socket non ancora bindato.
 Ovviamente il sistema associa al socket un indirizzo completo,
comprendente anche l'indirizzo IP: viene ad esempio utilizzato
l'indirizzo IP della sottorete su cui e' inviato il datagram.
• Si e’ gia’ detto che nel dominio di comunicazione AF_INET e' anche
possibile chiedere esplicitamente il binding automatico del socket ad
una porta casuale (effimera).
 Per fare cio' nella bind() occorre chiedere l'associazione alla porta
34
numero 0.
Connessione attiva (CL) al socket remoto
• Un cliente CL non deve per forza connettersi alla porta remota.
• Un cliente CL puo' pero' connettersi alla porta remota.
• Se un cliente CL si connette ad una porta remota:
• Il sistema locale considera che il socket (la porta) locale sia
connesso univocamente a quella porta remota.
• Operazioni di scrittura sul socket prive dell’indicazione del
destinatario remoto (e.g. system call write()) si traducono quindi
nell'invio di un messaggio alla porta remota connessa.
 In effetti, non e’ possibile inviare dati a nessun altro
destinatario!
• Attraverso il socket vengono ricevuti solo messaggi provenienti dal
socket remoto connesso.
 Eventuali datagram ricevuti da mittenti diversi vengono
cestinati dal sistema di comunicazione.
• E’ quindi utilizzabile per la ricezione dati dal socket la system call
read() (che non ritorna la porta remota da cui si e’ ricevuto il
datagram: questa porta e’ nota a priori, e’ quella connessa al
35
socket).
Connessione attiva (CL) al socket remoto
• L'operazione di connect() di un socket SOCK_DGRAM e' una
operazione locale del sistema su cui e' eseguita: non si traduce nel
set-up di una connessione di Trasporto con la porta remota.
• L'operazione di connect() di un socket SOCK_DGRAM, essendo
una operazione con significato esclusivamente locale, e’ utilizzabile
anche da un server CL, e puo’ essere unilaterale.
 Quindi nell’interazione tra due end-point uno puo’ essere
connesso all’altro senza che sia vero il viceversa.
 Se si vuole che entrambi gli end-point siano connessi tra loro,
entrambi devono connettersi attivamente (eseguire l’operazione
di connect()).
 Per i socket CL non esiste una operazione di apertura passiva di
connessione.
• Domanda: quanto dura (istante di return meno istante di call)
l’esecuzione di una connect() su un socket CL? E quella di una
connect() su un socket CO?
36
Connessione passiva (accettazione) .1
• Si applica solo a socket CO.
• E' tipica di un server.
• Per prima cosa il server CO richiede alla sua protocol entity TCP di
essere pronta ad accettare e bufferare fino ad un certo numero
(massimo) di connessioni (backlog) sul socket.
#include <sys/types.h>
#include <sys/socket.h>
int listen (int sockfd, int backlog);
• Il parametro backlog indica quante richieste di connessione
possono essere accettate implicitamente e accodate nel sistema
• mentre il programma utente server e’ occupato in altre attivita’,
e.g. sta servendo una richiesta precedente e
• prima che esegua la successiva (o anche la prima!) system call
di accettazione (presa in carico) esplicita di una connessione.
• Le richieste di connessione inviate da client applicativi sono
accettate dalla protocol entity TCP lato server e accodate (fino a 37
backlog di esse) all'interno dell'implementazione dell'API.
+---------+ ---------\
active OPEN [connect()]
| CLOSED |
\
----------+---------+<---------\
\
create TCB
|
^
\
\ snd SYN
[listen()] passive OPEN |
|
CLOSE
\
\
------------ |
| ---------\
\
create TCB |
| delete TCB
\
\
V
|
\
\
+---------+
CLOSE
|
\
| LISTEN |
---------- |
|
+---------+
delete TCB |
|
rcv SYN
|
|
SEND
|
|
----------|
|
------|
V
+---------+
snd SYN,ACK /
\
snd SYN
+---------+
|
|<---------------------------------->|
|
|
SYN
|
rcv SYN
|
SYN
|
|
RCVD |<-----------------------------------------------|
SENT |
|
|
snd ACK
|
|
|
|------------------------------------|
|
+---------+
rcv ACK of SYN \
/ rcv SYN,ACK
+---------+
|
-------------|
|
----------|
x
|
|
snd ACK
|
V
V
| CLOSE
+---------+
| ------| ESTAB |
| snd FIN
+---------+
|
CLOSE
|
|
rcv FIN
V
------|
|
------+---------+
snd FIN /
\
snd ACK
+---------+
| FIN
|<---------------------------------->| CLOSE |
| WAIT-1 |-----------------|
WAIT |
+---------+
rcv FIN \
+---------+
| rcv ACK of FIN
------|
CLOSE |
| -------------snd ACK
|
------- |
V
x
V
snd FIN V
+---------+
+---------+
+---------+
|FINWAIT-2|
| CLOSING |
| LAST-ACK|
+---------+
+---------+
+---------+
|
rcv ACK of FIN |
rcv ACK of FIN |
| rcv FIN
-------------- |
Timeout=2MSL -------------- |
| ------x
V
-----------x
V
\ snd ACK
+---------+delete TCB
+---------+
------------------------>|TIME WAIT|------------------>| CLOSED |
+---------+
+---------+
TCP Connection State Diagram
Connessione passiva (accettazione) .2
• Dopo avere eseguito la funzione listen() il server CO puo’ accettare
(in realta’, prendere in carico) la prossima connessione instaurata
(richiesta da un cliente e gia’ accettata dalla protocol entity TCP).
• Se nessuna connessione (gia’ instaurata e bufferata) e’ pendente, il
server si sospende in attesa che una richiesta di connessione arrivi da
un client remoto (e sia accettata dalla protocol entity TCP locale).
• La system call accept() consente all’applicazione server di prendere
in carico la piu’ vecchia connessione pendente, cioe’ accettata dal
Layer di Trasporto ma non ancora presa in carico dall’applicazione.
#include <sys/types.h>
#include <sys/socket.h>
int accept (int sockfd,
struct sockaddr *peer, int *addrlen);
• Gli ultimi due parametri ritornano l'identita’ del cliente remoto che ha
richiesto (attivamente) la connessione (l'ultimo parametro e’ value-result: in
chiamata indica la dimensione della struttura *peer): se il chiamante non e’
interessato a questa informazione il loro valore di ingresso puo’ essere39
NULL/0.
Connessione passiva (accettazione) .3
TCP
client side
TCP
server side
connect
connect
connect
reject
ok
listen(2)
1 conn. bufferata
2 conn. bufferate
connect
ok
reject
accept
1 conn. bufferata
connect
2 conn. bufferate
ok
40
Connessione passiva (accettazione) .4
• Con la system call listen() l’applicazione server da’ indicazione alla
propria interfaccia socket (alla protocol entity TCP) di accettare
richieste di connessione che provengano da client remoti.
• L’indicazione non e’ condizionata all’identita’ del client che ha inviato
la richiesta.
• Anche quando tramite la system call accept() l’applicazione server
prende in carico una connessione gia’ accettata dalla propria protocol
entity TCP, essa non conosce ancora l’identita’ del client che ha
originato la connessione.
• E’ solo leggendo il valore del parametro di ritorno peer della system
call accept() che il server viene a conoscere l’identita’ del client
remoto.
(e puo’ eventualmente decidere di non volere interagire con lui: nel
qual caso deve chiudere la connessione gia’ creata)
• N.B.: l’API socket non consente al server di accettare solo connessioni
originate da client graditi.
41
La system call accept()
• Il parametro (di ingresso) sockfd della funzione indica il socket
(TSAP) su cui aspettare l’instaurazione di una connessione (se non
ce n’e’ gia’ una instaurata) e prenderla in carico.
• Quando la connessione e' instaurata la funzione accept() crea un
nuovo socket che e' associato alla connessione appena instaurata.
 Il nuovo socket quindi consente il colloquio CO con il client
indicato dal parametro di ritorno peer.
 Il nuovo socket e’ il TSAP che consente di utilizzare i servizi
messi a disposizione dalla connessione a cui e’ associato.
• Il socket decriptor di questo nuovo socket e' il valore di ritorno della
funzione accept().
• Il socket originario (sockfd) non viene invece associato ad alcuna
connessione, e rimane quindi disponibile per essere utilizzato in una
nuova chiamata di accept().
 Formalmente: era e resta connesso al pari remoto 0.0.0.0:0.
42
Creazione e utilizzo di una connessione – Esercizi .1
1. Descrivere altri possibili scenari di combinazione temporale delle
system call connect(), listen(), accept() oltre a quelli indicati
nella pagina “Connessione passiva (accettazione) .3”.
 N.B.: indicare non solo la chiamata ma anche il ritorno di ciascuna
invocazione di system call.
2. Introdurre in questi scenari anche la system call bind().
3. Da quale istante in poi, in questi scenari, un client puo’ effettivamente
cominciare a inviare dati al server?
4. Da quale istante in poi, in questi scenari, un server puo’
effettivamente cominciare ad acquisire e processare i dati inviatigli
dal client?
5. Se questi due istanti non sono necessariamente coincidenti (e non lo
sono) che cosa succede ai dati inviati dal client prima che il server li
possa effettivamente cominciare ad acquisire e processare?
43
Creazione e utilizzo di una connessione – Esercizi .2
1. Mappare sugli scenari disegnati le primitive req, ind, resp, conf del
servizio T-CONNECT scambiate tra utente e fornitore del Servizio di
Trasporto secondo lo scenario indicato in “L01: Scenari di
Connessione - successful TC establishment”.
2. Le system call socket(), bind() e listen() prevedono anche la
possibilita’ di ritornare una condizione di errore. Quali possono essere
delle possibili ragioni non banali di fallimento per queste system call?
3. Indicare i possibili scenari di utilizzo dei parametri di ritorno
peer/addrlen della system call accept(). Considerare anche
scenari in cui siano presenti apparati NAT/NATP.
44
Scenari per domanda 6
user
initiator
TCP
initiator side
.1
TCP
responder side
user
responder
listen.call
listen.return
connect.call
SYN
SYN+ACK
connect.return
ACK
accept.call
accept.return
45
Scenari per domanda 6
user
initiator
TCP
initiator side
.2
TCP
responder side
user
responder
listen.call
listen.return
accept.call
connect.call
SYN
SYN+ACK
connect.return
ACK
accept.return
46
Tipi diversi di socket in un contesto CO
• In un contesto CO un socket rappresenta (sostanzialmente se non
formalmente) il TSAP che consente di accedere e utilizzare:
1. Una connessione di Trasporto (gia’ instaurata)(socket di tipo 1).
2. Una porta su cui aspettare la richiesta di creazione di una connessione di
Trasporto da parte di un pari remoto.
• Apertura passiva della connessione.
• La connessione che e’ creata viene associata ad un nuovo socket (del
primo tipo), mentre la porta (e il relativo socket) rimangono disponibili
per accettare nuove richieste di connessione.
• Un socket di questo tipo non cambia quindi mai di natura
3. Una porta su cui richiedere la creazione attiva di una connessione di
Trasporto con un pari remoto.
• Apertura attiva della connessione.
• Una volta che la connessione e’ stata aperta, il socket si trasforma in
un socket di tipo 1.
• Formalmente un socket di tipo 2 (o 3) e’ associato ad una connessione della
porta locale con la porta remota 0.0.0.0:0.
47
Tipi diversi di socket in un contesto CO
In un contesto CO si possono distinguere 3 tipi diversi di socket:
• Socket server di associazione (connessione):
 associati ad una porta
 operazioni supportate: bind, listen, accept
• Socket server di comunicazione:
 associati ad una connessione gia’ instaurata
 operazioni supportate: read, write
• Socket client di associazione (connessione) e comunicazione:
 associati ad una porta (se la porta e’ sconnessa) o ad una
connessione (se la porta e' connessa)
 operazioni supportate: bind (opzionale), connect (se la porta
non e’ gia’ connessa), read e write (se la porta e' connessa)
N.B.: Potrebbero/dovrebbero essere rappresentati da ADT diversi!
48
Tipi diversi di socket in un contesto CO
Socket server di
associazione (sa)
Socket server di
comunicazione (sc1)
Socket server di
comunicazione (sc2)
Porta server
Porta client 0
Socket client (sc-ac-0)
Connessione 1
Connessione 2
Porta client 1
Porta client 2
Socket client (sc-ac-1)
Socket client (sc-ac-2)
49
Server sequenziali e concorrenti
• Si possono immaginare due modalita' operative che possono essere
utilizzate da un server nel rapportarsi con i suoi client (non sono le sole
possibili!): sequenziale e concorrente.
• Modalita' sequenziale
Quando il server entra in colloquio con un client termina di servirlo e
chiude la connessione con lui prima di andare ad accettare
esplicitamente (farsi carico di) una eventuale altra connessione
richiesta da un altro client.
• Modalita' concorrente
Quando il server entra in colloquio con un client, si duplica:
• Una delle due copie continua a servire il client fino al termine della
sessione, quindi chiude la relativa connessione e si suicida.
• L'altra copia si rimette in attesa di nuove richieste di connessione da
parte di altri client.
• La modalita' normale di costruire i server in Unix e' quella concorrente,
e l'API socket e' espressamente progettata per supportare facilmente
50
questa modalita' nel mondo CO.
Server sequenziali e concorrenti
• L’interazione client-server, di norma, non si limita ad una richiesta
singola ma coinvolge un dialogo complesso, fatto di tante interazioni.
 Quando vado dal salumiere non mi limito a chiedergli un solo
prodotto, faccio una spesa composta di tanti prodotti diversi che
richiedo in sequenza.
• Un cliente, quando viene servito, ha una persona dedicata a farlo.
 Se tanti clienti sono serviti contemporaneamente e’ perche’ ci sono
altrettanti commessi che lavorano nel negozio, e ogni cliente e’
servito da un commesso dedicato.
• Dal punto di vista implementativo e’ molto piu’ facile realizzare un
server concorrente attraverso tanti processi (tanti thread) indipendenti,
ciascuno dedicato al servizio di un solo cliente, che cercare di
realizzare un solo processo (thread) capace di servire
contemporaneamente tanti clienti.
 In analogia al modello del negozio con tanti commessi.
51
Struttura canonica di un server CO sequenziale
// trascuriamo la trattazione degli errori
int sockfd, newSockfd;
sockfd = socket(. . .);
bind(sockfd, . . .);
// alla porta well known
listen(sockfd, 5);
for (;;) {
newSockfd = accept(sockfd, . . .);
doYourJob(newSockfd);
close(newSockfd);
}
Domanda: cosa succede se un altro cliente cerca di connettersi
durante l’esecuzione di doYourJob()?
52
Struttura canonica di un server CO concorrente
// trascuriamo la trattazione degli errori
int sockfd, newSockfd;
sockfd = socket(. . .);
bind(sockfd, . . .); // alla porta well known
listen(sockfd, 5);
for (;;) {
newSockfd = accept(sockfd, . . .);
if (fork() == 0) { // processo figlio/clone
close(sockfd);
doYourJob(newSockfd);
close(newSockfd);
exit(0);
} else {
// processo padre
close(newSockfd);
}
53
}
Struttura canonica di un client CO
// trascuriamo la trattazione degli errori
int sockfd;
sockfd = socket(. . .);
// non e' necessario bind(. . .). Non e’
// nemmeno opportuno, salvo che alla porta 0.
connect(sockfd, . . .);
askForService(sockfd);
close(sockfd);
exit(0);
54
Struttura canonica di un server CL "sequenziale"
// trascuriamo la trattazione degli errori
int sockfd;
sockfd = socket(. . .);
bind(sockfd, . . .); // alla porta well known
for (;;) {
recvfrom(sockfd, buff, . . .);
doYourJob(buff);
// prepara la risposta
sendTo(sockfd, . . .);
}
• In un contesto CL un socket rappresenta una porta su cui trasmettere e
ricevere datagram (messaggi).
• Quale e’ il (modello di) comportamento di un server come questo rispetto ai
client? Rientra tra i modelli che abbiamo indicato?
Quale analogia possiamo fare?
• “sequenziale” = stateless, ogni richiesta e’ gestita in modo indipendente
55
Struttura canonica di un client CL
// trascuriamo la trattazione degli errori
int sockfd;
sockfd = socket(. . .);
bind(sockfd, . . .);
connect(sockfd, . . .);
// non sempre necessaria:
// serve solo nel dominio
// AF_UNIX
// non necessaria
askForService(sockfd);
close(sockfd);
exit(0);
56
Chiusura di un socket
• La chiusura di un socket avviene attraverso la normale system call
di chiusura dei file.
#include <sys/socket.h>
int close (int sockfd);
• Se il socket e’ CO e se il chiamante e’ l'ultimo processo locale ad
avere accesso alla connessione, questa system call
• Prima si assicura che tutti i dati che erano stati trasmessi
tramite il socket siano stati ricevuti dalla controparte.
• Poi, chiude la connessione (in particolare, lato trasmissione).
• Il cliente remoto si accorge della avvenuta chiusura del socket
perche’ un tentativo di lettura dal suo socket gli ritorna una
indicazione di EOF (end of file).
• In ogni caso (socket CL, o socket CO disconnesso, o socket CO
connesso ma con altri processi che hanno accesso alla stessa
connessione), libera le risorse locali del processo associate al
socket.
57
Chiusura di un socket CO: shutdown()
• L’API socket prevede anche una system call che consente di
chiudere la connessione TCP associata ad un socket senza
distruggere il socket stesso.
#include <sys/socket.h>
int shutdown(int s, int how);
• The shutdown() call causes all or part of a full-duplex
connection on the socket associated with s to be shut down.
• If how is SHUT_RD, further receptions will be disallowed.
• If how is SHUT_WR, further transmissions will be disallowed.
• If how is SHUT_RDWR, further receptions and transmissions will
be disallowed.
• N.B.: questa system call consente la gestione esplicita della
chiusura dei 2 lati di una connessione.
 Nel TCP una connessione bi-direzionale si comporta come
una coppia di connessioni uni-direzionali, ed e’ il mittente di
una connessione uni-direzionale l’unico che ha il diritto di
iniziarne la chiusura (morbida).
• Esercizio: in quale scenario puo’ essere utile l’uso della system
call shutdown()?
58
Trasmissione dati tramite un socket
• Sia che il socket sia CO, sia che il socket sia CL, se e' connesso ad
una porta remota (e.g. tramite connect()) esso puo' essere
utilizzato per tramettere dati utilizzando la normale system call
(bloccante) di scrittura del sistema di I/O.
int write (int fd, char *buff, unsigned int nch);
• La funzione ritorna il numero di byte che sono stati effettivamente
scritti (cioe’ "trasmessi", in realta' copiati nel buffer di trasmissione del
socket).
• Questo numero e' normalmente (ma non necessariamente) uguale al
numero di byte nch di cui si e' chiesto la scrittura.
• L'API socket mette pero' a disposizione anche altre system call
utilizzabili per trasmettere dei dati.
• Cio' e' necessario perche' la funzione write() non e' utilizzabile per
socket non connessi ad una porta remota.
(manca il parametro per indicare esplicitamente la destinazione dei
59
dati!)
Trasmissione dati tramite un socket
• Se un socket e’ di tipo SOCK_STREAM non e’ ovviamente possibile
trasmettere dati tramite di esso se non e’ stato precedentemente
connesso ad un socket remoto.
 Per un socket connesso il destinatario dei dati che si trasmettono
attraverso il socket e’ quindi noto (implicito).
• Se un socket e’ di tipo SOCK_DGRAM non e’ pero’ necessario
connetterlo a nessun socket remoto prima di utilizzarlo per trasmettere
dati.
• Si possono ad esempio trasmettere dati alternativamente e
ripetutamente a diversi destinatari.
• Se il socket non e’ connesso e’ necessario indicare esplicitamente
il destinatario di ciascun datagram.
• Ma la system call write() non consente di indicare la
destinazione del datagram che si vuole inviare.
• Non e’ quindi possibile utilizzarla per trasmettere dati tramite un
60
socket SOCK_DGRAM non connesso.
Trasmissione dati tramite un socket CO
• Se il socket e’ SOCK_STREAM (TCP) la trasmissione e’ a stream di byte:
• E’ sufficiente che si riesca a trasmettere anche solo un byte perche’
l’operazione sia terminata con successo.
• Quindi l’operazione e’ bloccante solo se nel buffer di trasmissione
del socket non c’e’ spazio nemmeno per copiare un byte
(il buffer e’ completamente pieno).
• E’ quindi possibile che l’operazioni termini con successo ritornando
un numero positivo minore di nch.
• N.B.: quando si dice
“trasmettere”
si intende
“copiare nel buffer di trasmissione del socket (della connessione)”
• N.B.: se anche gli nch byte fossero copiati tutti nel buffer di
trasmissione cio’ non garantirebbe comunque che essi sarebbero
trasmessi tutti e soli utilizzando un unico segmento informativo
(e.g. algoritmo di Nagle, MTU, …).
61
Trasmissione dati tramite un socket CL
• Se il socket e’ CL la trasmissione e’ a messaggi:
 in tutte le funzioni di trasmissione il messaggio coincide con il
buffer dati indicato nella chiamata.
• Perche’ l'operazione sia terminata con successo occorre che nel buffer
del socket si riesca a copiare l’intero messaggio che si vuole
trasmettere.
• Quindi perche’ l'operazione sia effettivamente bloccante basta che
nel buffer di trasmissione del socket non ci sia spazio sufficiente
per copiare tutto il messaggio.
• Pertanto se l’operazione ha successo essa ritorna necessariamente
un valore uguale a nch.
• Se il messaggio che si vuole trasmettere eccede la dimensione
massima supportata del datagram UDP l’operazione termina con un
errore.
• Notare che una operazione di scrittura su socket CL si blocca non solo
se non c’e’ in assoluto spazio libero nel buffer di trasmissione del
socket, ma anche se lo spazio libero del buffer non e’ sufficiente a 62
contenere l’intero datagram.
Scrittura di (esattamente) nch byte su socket CO
int writeNch (int fd, char *buffer, int nch) {
int nLeft = nch, nWritten;
while (nLeft > 0) {
nWritten = write(fd, buffer, nLeft);
if (nWritten <= 0) return(nWritten); // error
nLeft -= nWritten;
buffer += nWritten;
}
return(nch);
}
63
Scrittura di nch byte su socket CO: alternativa
int writeNch (int fd, char *buffer, int nch) {
int scan, nWritten ;
for(scan = 0; scan <nch; scan += 1) {
nWritten = write(fd, &buffer[scan], 1);
if (nWritten <= 0) return(nWritten); // error
}
return(nch);
}
• Funzionalmente le due versioni di writeNch() sono equivalenti.
• Ovviamente questo non sarebbe vero se la trasmissione fosse su un
socket CL.
• La prima versione pero’ e’ preferibile dal punto di vista dell’efficienza, sia
dal punto di vista del carico computazionale sui nodi che da quello del
carico di rete.
• Esercizio: descrivere quello che succede in rete nei due casi, sia
64
quando l’algoritmo di Nagle e’ abilitato che quando non lo e’.
Altre system call di scrittura su socket
#include <sys/types.h>
#include <sys/socket.h>
int send (int sockfd,
char *buff, int nch,
int flags);
• utilizzabile solo per socket connessi
(send(), come la funzione write(), non cita la porta destinazione).
• in piu' consente di specificare delle flag (in OR tra loro) per controllare
la modalita' di trasmissione:
• MSG_OOB per trasmettere dei dati out-of-band (expedited data).
• MSG_DONTROUTE per bypassare la funzione di routing.
I dati vengono inviati sulla sottorete associata all'indirizzo di rete
della porta destinazione.
• write(sockfd, buff, nch)  send(sockfd, buff, nch, 0)
65
Altre system call di scrittura su socket
#include <sys/types.h>
#include <sys/socket.h>
int sendto (int sockfd,
char *buff, int nch,
int flags,
struct sockaddr *to, int addrlen);
•
Utilizzabile anche per socket non connessi.
 Cita esplicitamente la porta destinazione tramite gli ultimi due
parametri.
•
Per il resto e' identica a send().
• send(sockfd, buff, nch, flags) 
sendto(sockfd, buff, nch, flags, NULL, 0)
66
Ricezione dati tramite un socket
• Sia che il socket sia CO, sia che il socket sia CL, se e' connesso ad
una porta remota (e.g. tramite connect()) esso puo' essere
utilizzato per ricevere dati utilizzando la normale system call
(bloccante) di lettura del sistema di I/O.
int read (int fd, char *buff, unsigned int nch);
• La funzione ritorna il numero di byte che sono stati effettivamente letti
(ricevuti) e copiati nel buffer riferito da buff, o
(solo per socket CO) 0 se il peer socket e’ stato chiuso e non ci sono
piu’ dati disponibili nel buffer di ricezione locale.
• Il numero ritornato dalla system call puo' essere minore del numero
nch che rappresenta
• la dimensione del buffer riferito da buff,
• in effetti, propriamente, il numero massimo di byte che vogliamo
leggere, che non puo’ eccedere la dimensione del buffer
(noi indicheremo sempre nch come size del buffer),
se (caso socket CO, per socket CL vedi il seguito) nel buffer di 67
ricezione del socket e’ disponibile un numero di byte inferiore a nch.
Ricezione dati tramite un socket
• L'API socket mette a disposizione anche altre system call, oltre alla
read(), per ricevere dei dati.
• Cio' e' necessario perche' la funzione read() non e' utilizzabile per
socket non connessi ad una porta remota.
 Manca il parametro che sul ritorno indica esplicitamente l'indirizzo
del mittente dei dati!
 Se ignoro chi mi ha mandato i byte che sto leggendo come posso
interagire con lui?
•
Equivalenze:
read(sockfd, buff, nch)  recv(sockfd, buff, nch, 0)
recv(sockfd, buff, nch, flags) 
recvfrom(sockfd, buff, nch, flags, NULL, 0)
68
Ricezione di dati tramite un socket
• Se un socket e’ di tipo (CO) SOCK_STREAM non e’ ovviamente
possibile ricevere dati tramite di esso se non e’ stato
precedentemente connesso ad un socket remoto.
• Il mittente dei dati che si ricevono attraverso il socket e’ quindi
noto (implicito).
• Se un socket e’ di tipo (CL) SOCK_DGRAM non e’ pero’ necessario
connetterlo a nessun socket remoto prima di utilizzarlo per ricevere
dati.
• Si possono ad esempio ricevere dati alternativamente e
ripetutamente da diversi mittenti.
• Se il socket non e’ connesso come si fa a conoscere il mittente
di ciascun datagram?
• La system call read() non consente esplicitamente (tramite un
parametro di ritorno) di conoscere l’identita’ del mittente del
datagram che si e’ ricevuto!
• Non e’ quindi possibile utilizzarla per ricevere dati tramite un
69
socket SOCK_DGRAM non connesso.
Ricezione di dati tramite un socket CL
• La ricezione su socket CL e’ a messaggio. Una operazione di lettura
legge tutti e soli i dati di un messaggio
• indipendentemente dal fatto che ci siano altri messaggi gia’
accodati nel buffer di ricezione;
• indipendentemente dal valore di nch, che puo’ essere maggiore,
minore o uguale alla dimensione del messaggio letto;
• in caso di successo il valore tornato (salvo l’eccezione indicata al
punto seguente) e’ quindi la lunghezza del messaggio.
 N.B.: in una operazione su un socket CL se *buff non e’ grande
abbastanza da contenere l'intero datagram (nch e’ minore della
dimensione del messaggio), questo e’ troncato, i byte in eccesso
sono scartati, e il valore ritornato e’ uguale a nch.
• Esercizio: e’ possibile pensare un’API/semantica alternativa che ci
consenta anche di ricevere interamente (senza troncamento)
messaggi di dimensione maggiore di quella di *buff (cioe’ di nch).
70
Ricezione di dati tramite un socket CO
• Nel caso di un socket CO (cioe’, assumiamo, TCP e SOCK_STREAM)
• la system call read() ritorna il valore 0 quando il pari remoto ha
chiuso la connessione e sono gia’ stati letti tutti i dati che esso ci aveva
inviato in precedenza.
• la system call read() e’ bloccante solo se nel buffer di ricezione non
e’ presente nemmeno un byte (e la connessione non e’ stata chiusa).
• il numero di byte letti, ritornato dalla system call, e’ pari al massimo
numero di byte disponibili nel buffer di ricezione della connessione
compatibilmente con nch, indipendentemente dalla granularita’ con cui
essi sono stati trasmessi/ricevuti.
• Cosa puo’ alterare la corrispondenza write()/read() in una
comunicazione CO/TCP?
• Copiatura solo parziale dei dati nel buffer di trasmissione del socket
mittente senza effettiva trasmissione in rete.
• Inserimento dei dati nel buffer di trasmissione del socket mittente senza
effettiva trasmissione in rete (flow control, algoritmo di Nagle).
• Accumulo di dati nel buffer di ricezione del socket destinazione.
• Operazioni di lettura su un buffer utente di dimensione minore di
quella del buffer utilizzato nella trasmissione.
71
Lettura di (esattamente) nch byte su socket CO
int readNch (int fd, char *buffer, int nch) {
int nLeft = nch, nRead;
while (nLeft > 0) {
nRead = read(fd, buffer, nLeft);
if (nRead < 0) return(nRead);
// error
else if (nRead == 0) break;
// EOF
nLeft -= nRead;
buffer += nRead;
}
return(nch-nLeft);
}
Domanda: in base a che cosa un programma decide quanti byte vuole
ricevere in una operazione readNch()? Come fa a sapere a priori la
lunghezza del messaggio che gli e’ stato inviato dal pari remoto?
72
Vedi ad es. readLine().
Lettura di nch byte su socket CO: alternativa
int readNch (int fd, char *buffer, int nch) {
int scan, nRead;
for(scan = 0, scan < nch, scan += 1) {
nRead = read(fd, &buffer[scan], 1);
if (nRead < 0) return(nRead);
// error
else if (nRead == 0) break;
// EOF
}
return(scan);
}
• Funzionalmente le due versioni di readNch() sono equivalenti.
• Come si confrontano dal punto di vista dell’efficienza?
• In realta’ molte implementazioni dell’API socket oggi supportano una
flag MSG_WAITALL che fa si’ che l’operazione di lettura sia
bloccante fino a che non sono diponibili tutti gli nch byte indicati. 73
Lettura di una riga di testo su socket CO
int readLine (int fd, char *line,
int n;
for (n = 1; n < maxLen; n += 1)
int rc; char c;
if ((rc = read(fd, &c, 1)) ==
*line = c;
line += 1;
if (c == '\n') break;
//
} else if (rc == 0) {
//
*line = '\0';
//
return(n-1);
} else {
//
return(rc);
}
}
*line = '\0';
//
return(n);
}
int maxLen) {
{
1) {
\n terminata
EOF
null terminata
rc<0, error
null terminata
74
Altre system call di lettura su socket
#include <sys/types.h>
#include <sys/socket.h>
int recv (int sockfd,
char *buff, int nch, int flags);
• Utilizzabile solo per socket connessi (come la funzione read() non
cita la porta mittente).
• In piu' consente di specificare delle flag (in OR tra loro) per
controllare la modalita' di ricezione:
• MSG_OOB per ricevere dei dati out-of-band (expedited data).
• MSG_PEEK per leggere i dati disponibili in ricezione senza
rimuoverli dal (buffer di ricezione del) socket.
• MSG_WAITALL per leggere esattamente nch byte, rimanendo
bloccati fino a che tutti questi byte non sono stati ricevuti
(modalita’ non presente nella versione originale dell’API e non
supportata da tutte le implementazioni).
• La funzione ritorna il numero di byte che sono stati effettivamente
75
letti (ricevuti) e copiati nel buffer riferito da buff.
Altre system call di lettura su socket
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom (int sockfd,
char *buff, int nch,
int flags,
struct sockaddr *from, int *addrlen );
•
Utilizzabile anche per socket non connessi.
 Cita esplicitamente, come valore di ritorno, la porta mittente
tramite gli ultimi due parametri.
•
Per il resto e' identica a recv(), salvo che:
se in ingresso e' from!=NULL, al ritorno gli ultimi due parametri
contengono l'indirizzo del mittente del datagram.
•
N.B. from e addrlen sono entrambi value-result:
 in ingresso, se non nulli, indicano indirizzo e dimensione del
76
buffer in cui vogliamo avere l’indirizzo del mittente.
Letture/scritture bloccanti
Le operazioni di lettura e scrittura che abbiamo definito sono bloccanti.
• Per una operazione di lettura cio' significa che
• Se non ci sono dati disponibili nel buffer di ricezione del socket al
momento dell'invocazione dell'operazione, il processo chiamante si
blocca in attesa che dei dati diventino disponibili.
• Quando i dati diventano disponibili (nel caso di socket CO anche solo 1
byte, nel caso di socket CL un intero datagram) la system call termina, e i
dati a quel punto disponibili vengono ritornati al chiamante.
• Per una operazione di scrittura cio' significa che
• Se nel buffer di trasmissione del socket non c'e' spazio di memoria per
ospitare i dati (ad es. perche' la rete e' piu' lenta a consumare dati di
quanto sia il processo a produrli), il processo si blocca in attesa che tale
spazio diventi disponibile.
• Quando c'e' spazio disponibile (nel caso di socket CO anche solo 1 byte,
nel caso di socket CL per contenere l’intero datagram) l'esecuzione della
system call riprende: tutti i dati che possono essere copiati nel socket
(perche' c'e' abbastanza spazio) lo sono, e l'operazione termina.
77
Letture/scritture bloccanti
Socket
TCP
write()
read()
IP
Socket
TCP
socket
socket
TX buffer
RX buffer
RX buffer
TX buffer
• Domande:
• Perche’ il buffer di trasmissione puo’ saturarsi?
• Quando e’ che la protocol entity TCP puo’ eliminare dei dati
presenti nel buffer di trasmissione?
(e quindi liberare spazio in questo buffer)
• Quando e’ che la protocol entity TCP puo’ eliminare dei dati
presenti nel buffer di ricezione?
(e quindi liberare spazio in questo buffer)
78
Server CL concorrente
.0
• Ha senso considerare il caso di un server CL concorrente?
 Certo, TFTP e' di norma supportato da un server concorrente!
• Come e' possibile realizzare un server CL concorrente se il socket e'
associato alla porta e non ad una particolare connessione della porta
(e quindi ad un particolare cliente)?
• Per realizzare un server CL concorrente abbiamo bisogno non solo di
diversi socket, ma anche di diverse porte!
 Un socket e una porta (un TSAP UDP) per ciascuna istanziazione
del server, e quindi per ciascun client contemporaneamente attivo!
• Per realizzare un server CL concorrente occorre:
• Stabilire un protocollo applicativo con il lato cliente
(e' quindi necessaria la cooperazione del client).
• Emulare a livello applicativo il modo di operare dell'API socket CO.
• Esercizio: definire il protocollo client/server per supportare la
realizzazione di un server CL concorrente e definire di conseguenza la
79
struttura canonica di un server CL concorrente.
Server CL concorrente
Server Padre
socket server di associazione
porta server well-known
.1
N.B.:
Il server e’ di
norma bloccato
in read sulla
porta well
known in attesa
(del primo
messaggio) di
nuovi clienti
porta effimera
socket client
Client
80
Server CL concorrente
.2
Server Padre
socket server di associazione
socket server di comunicazione
porta server well-known
porta server effimera
porta effimera
socket client
Client
81
Server CL concorrente
Server Padre
fork()
.3
Server Figlio
socket server di associazione
socket server di comunicazione
porta server well-known
porta server effimera
porta effimera
socket client
Client
82
Server CL concorrente
Server Padre
.4
Server Figlio
socket server di associazione
socket server di comunicazione
porta server well-known
porta server effimera
porta effimera
socket client
Client
83
Server CL concorrente
Server Padre
.5
Server Figlio
socket server di associazione
socket server di comunicazione
porta server well-known
porta server effimera
porta effimera
socket client
Client
84
Struttura canonica di un server CL concorrente
// trascuriamo la trattazione degli errori
int sockfd, newSockfd;
struct sockaddr_in client;
sockfd = socket (. . .);
bind(sockfd, . . .);
for (;;) {
recvfrom(sockfd, buff, . . ., &client, . . .);
newSockfd = socket (. . .);
connect(newSockfd, &client, . . .);
if (fork()==0) { // processo figlio
close(sockfd);
doYourJob(newSockfd, buff); // anche buff!!!
close(newSockfd);
exit(0);
} else {
// processo padre
close(newSockfd);
}
85
}
Struttura canonica di un client per server CL concorrente
// trascuriamo la trattazione degli errori
int sockfd;
struct sockaddr_in server, realServer;
sockfd = socket(. . .);
sendto(sockfd, . . ., &server, . . .);
recvfrom(sockfd, . . ., &realServer, . . .);
connect(sockfd , &realServer, . . .);
// per realizzare un server CL concorrente e'
// necessaria la collaborazione dei client!
askForService(sockfd);
close(sockfd);
exit(0);
86
Server CL concorrente
• In realta’ c’e ancora una bella differenza tra il comportamento dei due server
concorrenti, quello CL e quello CO.
• Il server CO lato padre si e’ limitato ad accettare una connessione, a creare il
figlio e a passargliela.
 Il suo comportamento e’ identico per tutti i diversi servizi applicativi, anzi
potremmo pensare di avere un unico padre che attende clienti per tutti i
servizi applicativi e che attiva poi un figlio opportuno a seconda del servizio
richiesto.
 A parte il socket legato alla connessione con il cliente, nel caso CO non c’e’
altro scambio di informazione tra padre e figlio.
• Nel caso CL invece il server padre ha dovuto ricevere un datagram e deve farlo
avere al processo figlio.
 Il figlio sembra dover essere parte del padre (per poter ricevere il datagram).
 E’ anche evidente che il ricorso da parte del padre alla lettura MSG_PEEK
non e’ cosi’ facile:
come fare a condividere e sincronizzare tra padre e figlio l’accesso al socket
(well known) che contiene ancora il primo datagram del cliente?
• Il problema verra’ affrontato nell’Esercitazione 1 sul superserver di rete
inetd di Unix.
87
Server CL concorrente e utilizzo di MSG_PEEK?
// trascuriamo la trattazione degli errori
int sockfd, newSockfd;
struct sockaddr_in client;
sockfd = socket (. . .);
bind(sockfd, . . .);
for (;;) {
recvfrom(sockfd, buff, …, MSG_PEEK, &client, . . .);
newSockfd = socket (. . .);
connect(newSockfd, &client, . . .);
if (fork()==0) { // processo figlio
recvfrom(sockfd, buff, …, 0, . . .);
// corsa critica
doYourJob(newSockfd, buff);
close(sockfd); close(newSockfd);
exit(0);
} else {
// processo padre
close(newSockfd);
}
}
 Problema: su sockfd e’ rimasto accodato il messaggio che ha causato la
generazione del figlio.
Sia padre che figlio devono accedere a sockfd, il padre per aspettare 88
nuovi clienti, il figlio per ricevere il messaggio che deve servire!
Server CL veramente sequenziale
• In realta’ il server CL “sequenziale” non processa i clienti davvero
sequenzialmente:
• Ogni richiesta (messaggio ricevuto) e’ trattata indipendentemente dalle
altre (e’ in realta’ un server stateless, senza stato, cioe’ senza memoria).
• Il trattamento di richieste successive di clienti diversi e’ inframmezzato.
• Il modello di comportamento cui si ispira il server non e’ quello del
negoziante ma quello del cuoco di un ristorante.
• Per realizzare una interazione veramente sequenziale il server dovrebbe
concentrare la sua attenzione su un cliente per volta, e trattare tutte le
richieste provenienti da un cliente prima di prendere in considerazione il
cliente successivo (sequenzializzare le sessioni con i client complete).
• Come e' possibile realizzare un server CL veramente sequenziale se il socket
e' associato alla porta e non ad una particolare connessione della porta
(e quindi ad un particolare cliente)?
 E su quella porta (in particolare, la porta well known del servizio)
chiunque e’ in grado di inviare messaggi e quindi di inframmezzare le
proprie richieste a quelle del client servito in questo momento!
89
Server CL veramente sequenziale
• Per realizzare un server CL sequenziale occorre che il server possa
filtrare i messaggi che riceve sulla sua porta. Due possibilita’:
• Filtraggio a livello applicativo.
• Filtraggio a livello di sistema
(utilizzando la system call connect(): ci sono criticita’?).
• In alternativa (terza possibilita’):
• Per servire il singolo cliente si utilizza una porta effimera.
• La porta well-known e’ utilizzata solo per accettare nuovi clienti
(la porta well known e’ guardata solo se non c’e’ gia’ in corso il
servizio di nessun cliente).
• Un cliente dovrebbe seguire lo stesso paradigma realizzativo di un
cliente di server CL concorrente
 N.B.: questo paradigma funziona comunque,
indipendentemente dalla struttura del server.
Di conseguenza tutti i client CL dovrebbero essere
implementati seguendolo.
90
Disconnessione e ri-connessione di un socket CL
• Si sono visti diversi scenari in cui risulta conveniente connettere un
socket CL ad una porta remota.
• In alcuni di questi scenari risulterebbe conveniente potere anche
• Disconnettere il socket, consentendogli quindi di tornare ad
interagire con qualunque altro socket CL della rete.
• Connettere il socket ad una porta remota diversa da quella cui e’
attualmente connessa (e.g. procedura di query iterativa DNS).
E’ possibile farlo?
• Da “An Advanced 4_4BSD Interprocess Communication Tutorial”:
• Only one connected address is permitted for each socket at one
time.
• A second connect will change the destination address.
• A connect to a null address (family AF_UNSPEC) will disconnect.
• N.B.: in alcuni sistemi e’ possibile disconnettere un socket CL
connettendolo ad un indirizzo invalido, e.g. IPaddr=INADDR_ANY e
91
porta 0.
Esercizi
• Esercizio 1: scrivere lo schema di un server CL veramente
sequenziale secondo ciascuna delle 3 modalita’ indicate.
Utilizzare comunque un solo processo server senza fare il fork() di
nessun figlio.
• Esercizio 2: la seconda delle 3 modalita’ indicate, quella basata sul
filtraggio a livello di sistema dei PDU applicativi ricevuti, e’ soggetta
ad una corsa critica.
• Perche’?
• Come si potrebbe risolvere il problema?
• Esercizio 3: in quali scenari, lato server e lato client, puo’ avere
senso disconnettere o ri-connettere un socket CL?
Perche’ non e’ stata prevista la possibilita’ di fare altrettanto per un
socket CO?
92
Funzioni ausiliarie della libreria
.1
#include <sys/types.h>
#include <sys/socket.h>
int getsockname(int sockfd,
struct sockaddr *localaddr,
int *addrlen);
getsockname() returns the current address to which the socket
sockfd is bound, in the buffer pointed to by localaddr.
The addrlen argument should be initialized to indicate the amount of
space (in bytes) pointed to by localaddr.
On return it contains the actual size of the socket name (address).
 N.B.: quindi addrlen e’ un parametro value-result, come nelle
system call accept() e recvfrom().
93
Funzioni ausiliarie della libreria
.1’
#include <sys/types.h>
#include <sys/socket.h>
int getpeername(int sockfd,
struct sockaddr *peeraddr,
int *addrlen);
getpeername() returns the address of the peer connected to the
socket sockfd, in the buffer pointed to by peeraddr.
The addrlen argument should be initialized to indicate the amount of
space (in bytes) pointed to by peeraddr.
On return it contains the actual size of the name (address) returned.
 N.B.: quindi addrlen e’ un parametro value-result, come nelle
system call accept(), recvfrom() e getsockname() .
94
Funzioni ausiliarie della libreria
.1”
#include <sys/types.h>
#include <sys/socket.h>
unsigned long inet_addr(char *ptr);
The inet_addr() function converts the internet host address *ptr
from decimal dotted notation into binary data in network byte order.
If the input is invalid, INADDR_NONE (-1, ma -1 non e’ unsigned e
255.255.255.255 e’ un indirizzo IP valido!) is returned.
char *inet_ntoa(struct in_addr inaddr);
E' l'inversa di inet_addr(). The inet_ntoa() function converts the
internet host address inaddr, given in network byte order, to a string in
decimal dotted notation.
The string is returned in a statically allocated buffer, which subsequent
calls will overwrite (la funzione non e’ thread safe!).
95
Funzioni ausiliarie della libreria: Esercizi
• Indicare degli scenari d’uso della system call getsockname().
 Quando e’ che un programma non conosce il numero della porta
associata ad un socket che sta utilizzando?
 Ha senso che un server utilizzi una porta di questo genere?
Se si’, che cosa deve essere presente come parte del sistema di
programmazione di rete?
 E se una applicazione distribuita volesse utilizzare un secondo
canale di comunicazione oltre quello primario? Esempio?
 Pensare anche all’esercitazione 1: perche’ il superserver ha
bisogno di questa system call?
• Indicare degli scenari d’uso della system call getpeername().
 Ricordate che un server CO e’ costretto ad accettare le richieste
di connessione “al buio”, senza conoscere l’identita’ del cliente.
 Pensare anche all’esercitazione 1: perche’ il superserver puo’
avere bisogno di questa system call?
 Cosa succede in presenza di clienti provenienti da intranet che
96
utilizzano indirizzi IP privati?
Funzioni ausiliarie della libreria
.2
#include <sys/types.h>
#include <netinet/in.h>
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
• Queste funzioni forniscono la rappresentazione di rete di un intero
(network byte order) indipendentemente dalla sua rappresentazione
locale (che si assume comunque essere binaria/complemento-a-2).
• L'utilizzo di queste funzioni consente di scrivere programmi portabili
tra calcolatori big-endian e little-endian.
• Esistono anche le funzioni duali ntohs() e ntohl().
• Quando accedo in lettura o scrittura ad un sockaddr_in devo
utilizzare esplicitamente queste funzioni:
 La definizione dell’API socket e’ basata sulla logica della mappa
di byte
(campi sockaddr_in.sin_port e
sockaddr_in.sin_addr.s_addr in network byte order)
97
e non sulla definizione astratta di una struttura informativa!
Esempio: eco server
.1
Una semplice applicazione di eco:
1. il client legge una riga da standard input;
2. il client invia la riga al server;
3. il server legge la riga dalla rete;
4. il server genera l'eco della riga sulla rete verso il client;
5. il client legge da rete l'eco della riga;
6. il client stampa l'eco su standard output.
Diverse varianti:
• Dominio di trasporto Internet, comunicazione a stream, server
concorrente.
• Dominio di trasporto Internet, comunicazione a stream, server
sequenziale.
• Dominio di trasporto Internet, comunicazione datagram, server
“sequenziale”.
• Dominio di trasporto Unix, comunicazione a stream, server
concorrente.
• Dominio di trasporto Unix, comunicazione datagram, server
“sequenziale”.
98
Riceve ed echeggia su socket stream
#define MAXLINE 512
void str_echo(int sockfd) {
int n;
char line[MAXLINE];
for (;;) {
n = readLine(sockfd, line, MAXLINE);
if (n == 0) {
return;
// connessione terminata
} else if (n < 0) {
err_dump("fatal read error");
} else if (writeNch(sockfd, line, n) != n) {
err_dump("fatal write error");
}
}
}
99
Riceve ed echeggia su socket datagram
#define MAXLINE 2048
void dg_echo(int sockfd,
struct sockaddr *cli_addr,
int maxAddrLen) {
int n, cliLen;
char line[MAXLINE];
for (;;) {
// non ritorna mai
cliLen = maxAddrLen;
n = recvfrom(sockfd, line, MAXLINE, 0,
cli_addr, &cliLen);
if (n < 0) {
err_dump("fatal read error");
} else if (sendto(sockfd, line, n, 0,
cli_addr, cliLen) != n) {
err_dump("fatal write error");
}
}
}
100
Accesso a socket e accesso a file
.1
• Dal testo di str_echo() si vede se sto accedendo a un socket
AF_INET o a un socket AF_UNIX?
 NO!
• Dal testo di str_echo() si vede se sto accedendo a un socket
piuttosto che ad un file?
 SI’, ma solo perche’ accedo ad uno stesso file descriptor sia in
lettura che in scrittura!
 Si sarebbe potuto definire str_echo() come
void str_echo(int in_fd, int out_fd);
e passare il socket descriptor come parametro attuale sia di
in_fd che di out_fd e la differenza sarebbe scomparsa!
101
Accesso a socket e accesso a file
.2
• Dal testo di dg_echo() si vede se sto accedendo a un socket
piuttosto che ad un file?
 SI’, ma solo perche’ tramite un’unica porta sono (voglio essere)
in grado di comunicare con tanti interlocutori diversi, e non con
uno solo!
• Il socket UDP server non e’ stato connesso ad uno specifico
cliente.
• Di conseguenza si deve operare tramite sendto() /
recvfrom() anziche’ tramite write() / read().
• Nota che se il socket UDP server fosse stato connesso avrei
potuto utilizzare anche in questo caso un prototipo del tipo
void dg_echo(int in_fd, int out_fd);
 Si vede pero’ anche che si sta comunicando a messaggi!
• Ma questo non sarebbe stato molto percepibile operando con
un socket pre-connesso (anche se se ne sarebbe dovuto
tenere conto! In che modo?).
102
Server concorrente TCP .1
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<strings.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
#define SERV_TCP_PORT 6000
int main(int argc, char *argv[]) {
int
struct sockaddr_in
sockfd, newsockfd,
clilen, childpid, tmp;
cli_addr, serv_addr;
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockfd < 0) {
err_dump("server: can't open socket");
}
103
Server concorrente TCP .2
bzero((char *) &serv_addr, sizeof(serv_addr));
/* bzero(b, n) scrive 0 in n byte consecutivi
a partire dall’indirizzo b */
serv_addr.sin_family
= AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
// N.B.: INADDR_ANY e’ gia’ (intrinsecamente) in
// network byte order
serv_addr.sin_port
= htons(SERV_TCP_PORT);
// N.B.: sin_port contiene il numero della porta
// in rappresentazione binaria (si assume) e in
// network byte order (forzato tramite htons())
tmp = bind(sockfd, (struct sockaddr*) &serv_addr,
sizeof(serv_addr));
if (tmp < 0) {
err_dump("server: can't bind local socket");
}
listen(sockfd, 5);
// niente caso di errore?
104
Server concorrente TCP .3
for (;;) {
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd,
(struct sockaddr*) &cli_addr,
&clilen);
if (newsockfd < 0) {
err_dump ("server: accept error");
}
if ((childpid = fork()) < 0) {
err_dump ("server: fork error");
}
if (childpid == 0) {
// child process
close(sockfd);
str_echo(newsockfd);
close(newsockfd); exit(0);
} else {
// parent process
close(newsockfd);
}
}
}
105
Stampa dell’indirizzo del cliente
• Come potremmo fare a stampare l’indirizzo del cliente che
sappiamo essere contenuto in cli_addr?
 Ma che e’ in una rappresentazione poco conveniente:
• La porta e’ in network byte order.
• L’indirizzo IP e’ in formato binario (e in network byte order).
• ntohs(cli_addr.sin_port)
da’ il numero di porta del cliente in rappresentazione concreta locale
(facilmente stampabile tramite printf() formattata).
• inet_ntoa(cli_addr.sin_addr)
da’ l’indirizzo IP del cliente in decimal dotted notation, quindi come
una (particolare) stringa di caratteri.
106
Server sequenziale TCP .1
#include
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<strings.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
<unistd.h>
#define SERV_TCP_PORT 6000
int main(int argc, char *argv[]) {
int
struct sockaddr_in
sockfd, newsockfd,
clilen, childpid, tmp;
cli_addr, serv_addr;
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockfd < 0) {
err_dump("server: can't open socket");
}
107
Server sequenziale TCP .2
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family
= AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port
= htons(SERV_TCP_PORT);
tmp = bind(sockfd, (struct sockaddr*) &serv_addr,
sizeof(serv_addr));
if (tmp < 0) {
err_dump("server: can't bind local socket");
}
listen(sockfd, 5); // niente caso di errore?
•
•
Perche’ se si controlla il valore di ritorno delle system call
socket() e bind() non si controlla quello della system call
listen()?
Domanda: quali possono essere delle possibili ragioni non banali di
fallimento per le system call socket(), bind() e listen()?
108
Server sequenziale TCP .3
for (;;) {
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd,
(struct sockaddr*) &cli_addr,
&clilen);
if (newsockfd < 0) {
err_dump ("server: accept error");
}
str_echo(newsockfd);
close(newsockfd);
}
}
109
Client TCP .1
#include
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<strings.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
<unistd.h>
#define SERV_HOST_ADDR "138.132.202.1"
#define SERV_TCP_PORT 6000
#define MAXLINE 512
int main(int argc, char *argv[]) {
int
struct sockaddr_in
sockfd, tmp;
serv_addr;
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockfd < 0) {
err_dump("client: can't open socket");
110
}
Client TCP .2
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family
= AF_INET;
serv_addr.sin_addr.s_addr =
inet_addr(SERV_HOST_ADDR);
serv_addr.sin_port
= htons(SERV_TCP_PORT);
// bind implicito e automatico ad una porta
// effimera
tmp = connect(sockfd, (struct sockaddr*) &serv_addr,
sizeof(serv_addr));
if (tmp < 0) {
err_dump("client: can't connect to server");
}
str_cli(sockfd);
close(sockfd);
exit(0);
}
111
Client TCP .3
void str_cli (int sockfd) {
int n;
char sendLine[MAXLINE+1], recvLine[MAXLINE+1];
while (fgets(sendLine, MAXLINE, stdin) != NULL) {
n = strlen(sendLine);
if (writeNch(sockfd, sendLine, n) != n) {
err_dump("client: write error on socket");
}
n = readLine(sockfd, recvLine, MAXLINE);
if (n < 0) {
err_dump("client: read error on socket");
}
recvLine[n] = '\0';
fputs(recvLine, stdout);
}
if (ferror(stdin)) {
err_dump("client: read error on standard input");
}
}
112
Accesso contemporaneo a piu’ risorse di rete
• Nota bene: la realizzazione del client di echo e’ molto facile perche’ in
ogni istante esso deve accedere ad una sola risorsa per volta (la
console o la rete), e sa anche a priori a quale delle 2 risorse deve
accedere ad ogni istante.
 Ma se non fosse cosi’?
• Immaginiamo un programma che dovesse operare come un data
switch full-duplex tra 2 connessioni TCP:
 Ad ogni istante esso dovrebbe essere sospeso in read() su
entrambi i socket associati alle due connessioni e questo e’
ovviamente impossibile!
 Deve esserci un meccanismo che renda possibile lo sviluppo di
applicazioni di questo tipo!
• Lo stesso problema si avrebbe se il client fosse il client di un terminale
remoto (N.B.: in questo caso non si conoscerebbe a priori la lunghezza
della risposta generata dal server):
 In questo caso il processo client dovrebbe essere, in ogni istante,
in ricezione contemporaneamente sia da stdin sia dal socket di
113
comunicazione verso il server.
Server “sequenziale” UDP
#include
#include
#include
#include
#include
#include
.1
<stdio.h>
<strings.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
#define SERV_UDP_PORT 6000
int main(int argc, char *argv[]) {
int
struct sockaddr_in
sockfd, tmp;
cli_addr, serv_addr;
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0) {
err_dump("server: can't open socket");
}
114
Server “sequenziale” UDP
.2
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family
= AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port
= htons(SERV_UDP_PORT);
tmp = bind(sockfd, (struct sockaddr*) &serv_addr,
sizeof(serv_addr));
if (tmp < 0) {
err_dump("server: can't bind local socket");
}
dg_echo(sockfd, (struct sockaddr *) &cli_addr,
sizeof(cli_addr));
// non ritorna!!
}
115
Client “sequenziale” UDP
#include
#include
#include
#include
#include
#include
#include
#include
.1
<stdio.h>
<stdlib.h>
<strings.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
<arpa/inet.h>
#define SERV_HOST_ADDR "138.132.202.1"
#define SERV_UDP_PORT 6000
#define MAXLINE 512
int main(int argc, char *argv[]) {
int
struct sockaddr_in
sockfd, tmp;
cli_addr, serv_addr;
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0) {
err_dump("client: can't open socket");
}
116
Client “sequenziale” UDP
.2
bzero((char *) &cli_addr, sizeof(cli_addr));
cli_addr.sin_family
= AF_INET;
cli_addr.sin_addr.s_addr = INADDR_ANY;
cli_addr.sin_port
= htons(0);
// vedi nota
tmp = bind(sockfd, (struct sockaddr*) &cli_addr,
sizeof(cli_addr));
if (tmp < 0) {
err_dump("client: can't bind local socket");
}
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family
= AF_INET;
serv_addr.sin_addr.s_addr =
inet_addr(SERV_HOST_ADDR);
serv_addr.sin_port
= htons(SERV_UDP_PORT);
dg_cli(sockfd, (struct sockaddr *) &serv_addr,
sizeof(serv_addr));
close(sockfd);
exit(0);
}
117
Client UDP .nota
•
L’indirizzo locale del socket client e’ settato esplicitamente tramite
bind().
 Il numero della porta assegnata al socket e’ pero’ irrilevante: basta
che sia univoco.
 Possiamo quindi chiedere al sistema stesso di assegnare un
numero di porta qualsiasi, ma univoco, al socket.
•
La richiesta e’ esplicitata assegnado il valore 0 al campo sin_port:
 questo assegnamento sta in realta’ ad indicare la richiesta di
associazione automatica ad una porta libera con numero nel range
1024..5000 (range obsoleto), cioe’ ad una porta effimera.
•
Nella bind() abbiamo anche specificato che non ci interessa quale
sia il particolare valore di indirizzo IP mittente che viene inserito nei
datagram trasmessi tramite il socket (indirizzo IP = INADDR_ANY):
 Quando viene inviato un datagram tramite il socket il sistema
sceglie l’indirizzo mittente da usare in base all’interfaccia di rete
effettivamente usata per la trasmissione.
118
Client UDP .3
void dg_cli(int sockfd, struct sockaddr *pserv_addr,
int servlen) {
int n;
char sendLine[MAXLINE+1], recvLine[MAXLINE+1];
while (fgets(sendLine, MAXLINE, stdin) != NULL) {
n = strlen(sendLine);
if (sendto(sockfd, sendLine, n, 0
pserv_addr, servlen) != n) {
err_dump("client: write error on socket");
}
n = recvfrom(sockfd, recvLine, MAXLINE, 0,
NULL, 0);
if (n < 0) err_dump("client: read error on sock");
recvLine[n] = '\0';
fputs(recvLine, stdout);
}
if (ferror(stdin))
err_dump("client: read error on stdin");
}
119
Server concorrente Unix stream
#include
#include
#include
#include
#include
#include
.1
<stdio.h>
<stdlib.h>
<strings.h>
<sys/types.h>
<sys/socket.h>
<unistd.h>
#define UNIXSTR_PATH "/tmp/unixstr"
int main(int argc, char *argv[]) {
int
struct sockaddr_un
sockfd, newsockfd, servlen,
clilen, childpid, tmp;
cli_addr, serv_addr;
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0) {
err_dump("server: can't open socket");
}
120
Server concorrente Unix stream
.2
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sun_family
= AF_UNIX;
strcpy(serv_addr.sun_path, UNIXSTR_PATH);
servlen = strlen(serv_addr.sun_path) +
sizeof(serv_addr.sun_family);
tmp = bind(sockfd, (struct sockaddr*) &serv_addr,
servlen);
if (tmp < 0) {
err_dump("server: can't bind local socket");
}
listen(sockfd, 5);
121
Server concorrente Unix stream
.3
for (;;) {
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd,
(struct sockaddr*) &cli_addr,
&clilen);
if (newsockfd < 0) {
err_dump ("server: accept error");
}
if ((childpid = fork()) < 0) {
err_dump ("server: fork error");
}
if (childpid == 0) {
// child process
close(sockfd);
str_echo(newsockfd);
close(newsockfd); exit(0);
} else {
// parent process
close(newsockfd);
}
}
}
122
Server concorrenti Unix stream e TCP
•
N.B.: a parte l'inizializzazione, il corpo del programma del server
concorrente Unix stream (la system call listen() e il ciclo for) e'
identico a quello del programma del server concorrente TCP,
•
Questo grazie al fatto che l'API socket e' largamente protocol
independent.
•
La stessa cosa capita ovviamente:
• Per i corrispondenti programmi client stream oriented,
• Per i corrispondenti programmi server datagram oriented,
• Per i corrispondenti programmi client datagram oriented,
nei due domini di comunicazione Unix e Internet.
123
Client Unix stream
#include
#include
#include
#include
#include
#include
.1
<stdio.h>
<stdlib.h>
<strings.h>
<sys/types.h>
<sys/socket.h>
<unistd.h>
#define UNIXSTR_PATH "/tmp/unixstr"
#define MAXLINE 512
int main(int argc, char *argv[]) {
int
struct sockaddr_un
sockfd, servlen, tmp;
serv_addr;
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0) {
err_dump("client: can't open socket");
}
124
Client Unix stream
.2
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sun_family
= AF_UNIX;
strcpy(serv_addr.sun_path, UNIXSTR_PATH);
servlen = strlen(serv_addr.sun_path) +
sizeof(serv_addr.sun_family);
tmp = connect(sockfd, (struct sockaddr*) &serv_addr,
sizeof(serv_addr));
if (tmp < 0) {
err_dump("client: can't connect to server");
}
str_cli(sockfd);
// vedi str_cli() definita per il client TCP
close(sockfd);
exit(0);
}
125
Socket options
.1
• Queste opzioni consentono di controllare alcuni aspetti del
funzionamento (comportamento, proprieta’) di un socket
(e delle risorse di rete accessibili per il suo tramite).
• Il valore di queste opzioni e’ leggibile e scrivibile tramite le system call
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level,
int optname, void *optval,
int *optlen);
int setsockopt(int sockfd, int level,
int optname, void *optval,
int optlen);
• Ogni opzione e’ level/protocol specific: e’ cioe’ interpretata da un
particolare livello del SW di rete.
• Una opzione relativa ad un certo protocollo e’ specificabile solo se il
126
socket e’ associato (anche) a quello specifico protocollo.
Socket options
.2
• level indica a quale layer del SW di rete l’opzione optname e’
relativa; e.g.
• SOL_SOCKET indica il layer dell'API socket
• IPPROTO_TCP indica l’entita’ di protocollo TCP
• optname e’ l’opzione di cui si vuole leggere/scrivere il valore.
• optval riferisce una variabile che contiene il valore dell’opzione
che si vuole scrivere (system call setsockopt()) oppure la
variabile che al ritorno dalla system call conterra’ il valore
dell’opzione (system call getsockopt()).
•
Il tipo del valore optval associato ad una opzione e’ specifico di
quella opzione
(la maggior parte delle opzioni sono specificate tramite un valore di
tipo int; char* qui sta per void*).
•
Quando l’opzione e’ una flag, essa e’ rappresentata da un valore
intero (int), ==0 se l’opzione e’ (deve essere) disabilitata, !=0 se
127
l’opzione e’ (deve essere) abilitata.
Socket options
.3
• optlen indica la size (in byte) della variabile riferita da optval.
• Nel caso della system call getsockopt() optlen e' un parametro
value-result.
• Il valore ritornato dalle system call indica successo (0) o fallimento
(-1).
• esempio:
int on = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on,
sizeof on);
// sizeof on == sizeof(on) == sizeof(int)
128
Socket options
.4
• In Linux, one can specify the system's default receive buffer size for network
packets. Is it possible for an application to override system's defaults by
specifying the receive buffer size per socket at runtime?
• You can increase the value from the default, but you can't increase it beyond
the maximum value. Use setsockopt() to change the SO_RCVBUF option:
int n = 1024 * 1024;
if (setsockopt(socket, SOL_SOCKET, SO_RCVBUF, &n,
sizeof(n)) == -1) {
// deal with failure,
// or ignore if you can live with the default size
}
• Linux has had autotuning for a while now (since 2.6.7, and with reasonable
maximum buffer sizes since 2.6.17), which automatically adjusts the receive
buffer size based on load.
On kernels with autotuning, it is recommended that you not set the receive
buffer size using setsockopt(), as that will disable the kernel's autotuning.
• Using setsockopt() to adjust the buffer size may still be necessary on other
platforms, however.
129
Socket options: parametro optname .1
• TCP_MAXSEG indica la massima dimensione del segmento TCP (MTU).
• TCP_NODELAY consente di disabilitare l'algoritmo di Nagel utilizzato
normalmente dalle protocol entity TCP
(per utenti TCP che trasmettono tanti segmenti piccoli che non sono
echeggiati dal ricevente, o per X-term/VNC, o per applicazioni realtime).
• SO_ERROR ritorna e azzera il valore della variabile di sistema
so_error definita in <sys/socketvar.h> ed equivalente ad errno.
• SO_KEEPALIVE (per stream socket Internet) abilita la trasmissione di
segmenti periodici di keep-alive in caso il cliente non richieda di
effettuare trasmissioni.
Questo consente di monitorare lo stato della connessione e di
considerarla abortita se il segmento keep-alive non e' riscontrato.
• TCP_KEEPIDLE specifies the number of seconds of idle time on a
connection after which TCP sends a keepalive packet. This socket
option value is inherited from the parent socket from the accept system
130
call. The default value is 7200 seconds.
Socket options: parametro optname .2
• SO_REUSEADDR consente ad un processo di eseguire il bind() di un
socket ad una porta TCP gia’ coinvolta in connessioni possedute da altri
processi
(e.g. ad una riattivazione del superserver inetd di fare il bind() alle
porte well known, anche se sono ancora vive istanze di servizi attivate
in precedenza che usano connessioni che coinvolgono quelle porte).
• Di norma il sistema non consente a due processi di associarsi
entrambi ad una stessa porta.
• In particolare non consente ad un processo di associarsi ad una
porta se c'e' gia' un altro processo che ha una connessione attiva
tramite quella porta.
(anche se non c'e' alcuna ambiguita': il secondo socket e' in realta'
associato alla connessione e non alla porta)
• Attivare l'opzione consente di disabilitare il check di associazione
unica alla porta per il socket, quando questo check e' inopportuno (in
pratica sempre, lato server).
• SO_BROADCAST set or get the broadcast flag. When enabled, datagram
sockets are allowed to send packets to a broadcast address. This option
has no effect on stream-oriented sockets.
131
Socket options: parametro optname .3
• SO_LINGER lingers on a close() if data is present. This option
controls the action taken when unsent messages queue on a socket
and close() is performed. If SO_LINGER is set, the system shall
block the calling thread during close() until it can transmit the data or
until the time expires. If SO_LINGER is not specified, and close() is
issued, the system handles the call in a way that allows the calling
thread to continue as quickly as possible.
• SO_SNDBUF sets send buffer size. This option takes an int value.
• SO_RCVBUF sets receive buffer size. This option takes an int value.
• SO_RCVTIMEO sets the timeout value that specifies the maximum
amount of time an input function waits until it completes. It accepts a
timeval structure with the number of seconds and microseconds
specifying the limit on how long to wait for an input operation to
complete. If a receive operation has blocked for this much time without
receiving additional data, it shall return with a partial count or errno set
to [EAGAIN] or [EWOULDBLOCK] if no data is received. The default for
this option is zero, which indicates that a receive operation shall not
time out. This option takes a timeval structure. Note that not all
implementations allow this option to be set.
132
Socket options: fcntl()
.1
• Alcune modalita' di funzionamento di un socket e dei protocolli
sottostanti sono controllabili tramite la normale system call Unix
#include <fcntl.h>
int fcntl(int fd, int cmd, int arg);
• I valori di cmd rilevanti per noi sono
• F_GETFL, F_SETFL
• F_GETOWN, F_SETOWN
• In realta’ il vero prototipo di fcntl() e’
int fcntl(int fd, int cmd, ...);
in quanto il terzo parametro e’ opzionale (non e’ utilizzato ad esempio
per cmd uguale a F_GETFL o a F_GETOWN), e quando e’ presente puo’
essere di tipi diversi a seconda del valore di cmd.
• La descrizione data di seguito per i casi cmd uguale a F_GETOWN o a
F_SETOWN e’ semplificata, sia per quanto riguarda Unix che per
quanto riguarda Linux. In questi casi lo stesso prototipo di fcntl() in
133
Linux e’ diverso da quello indicato.
Socket options: fcntl()
.2
• F_GETFL e F_SETFL permettono rispettivamente di leggere e di
settare un insieme di flag in OR tra loro (indicate dal valore del
parametro arg). Le flag rilevanti sono:
• FASYNC che permette al processo cliente di interagire in modo
asincrono (tramite signal) con il socket
• FNDELAY che trasforma in non-bloccante la semantica delle
system call sul socket.
• F_GETOWN e F_SETOWN permettono rispettivamente di leggere e di
settare l’identita’ del processo che riceve i segnali SIGIO e SIGURG
per eventi correlati al file descriptor fd.
• Per F_GETFL e F_GETOWN il valore richiesto e' fornito da fcntl()
come valore di ritorno della funzione (e il terzo parametro di input e’
non significativo: in Linux il caso F_GETOWN e’ diverso).
134
Socket options: fcntl()
.3
• Quando voglio modificare una flag non voglio normalmente alterare il
valore delle altre flag.
• Il protocollo di modifica del valore di una flag prevede quindi che per
prima cosa si acquisisca il valore della parola di tutte le flag, che in
questa parola sia modificata la sola flag che ci interessa, e che la
parola di tutte le flag modificata sia poi settata di nuovo sul file
descriptor.
int flags;
flags = fcntl(fd, F_GETFL, 0);
// N.B.: l’ultimo parametro in questo caso non e’
// significativo
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
// per settare il fd a comportamento bloccante
// FNDELAY == O_NONBLOCK
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
// per settare fd a comportamento non bloccante
135
Letture/scritture non bloccanti
• Su un socket non bloccante una operazione che non puo' essere
eseguita completamente (e immediatamente) non e' eseguita del
tutto, e termina immediatamente (in errore) ritornando al chiamante.
• La variabile errno e' settata al valore EWOULDBLOCK.
• La semantica non bloccante e' selezionabile per le operazioni
• accept()
• connect()
• read(), recv(), recvfrom(), . . .
• write(), send(), sendto(), . . .
• Una connect() su un socket datagram e' intrinsecamente non
bloccante.
• Una connect() su un socket stream e' necessariamente “bloccante”.
• Essa viene comunque iniziata e se possibile portata a termine.
• Essa termina comunque immediatamente ma in questo caso
errno assume il valore EINPROGRESS.
• Una operazione di scrittura su socket stream e' bloccante solo se lo
136
spazio di buffer disponibile e' nullo.
Operazioni asincrone
• Un cliente dell'interfaccia socket puo' anche decidere di operare in
lettura (cio' comprende anche la system call accept()) in modalita'
asincrona.
• In questo caso egli viene informato tramite segnali (SIGIO e
SIGURG) della disponibilita' del socket per nuove operazioni.
• Per operare in modo asincrono il cliente deve:
• Settare la flag FASYNC per abilitare il socket a generare segnali.
• Settare tramite fcntl(cmd==F_SETOWN) la destinazione dei
segnali generati dal socket a se stesso.
• Definire (tramite system call signal()) una procedura handler
per i segnali SIGIO e, eventualmente, SIGURG.
(N.B.: la system call signal() notifica anche al sistema
operativo l’interesse del processo chiamante a ricevere il segnale
indicato)
137
Unix system programming
• void (* signal(int sig, void (*func)()))(int);
• sig è l’intero (o il nome simbolico) che individua il segnale da
gestire
• il parametro func è un puntatore a una funzione che indica l’azione
da associare al segnale; in particolare func può:
• puntare alla routine di gestione dell’interruzione (handler)
• valere SIG_IGN (nel caso di segnale ignorato)
• valere SIG_DFL (nel caso di azione di default)
• La system call signal() ritorna un puntatore a funzione:
• al precedente gestore del segnale
• SIG_ERR (-1), nel caso di errore
• uno handler e’ una funzione che in ingresso si aspetta un intero
(l’identificativo del segnale ricevuto) e in uscita non ritorna alcun
parametro.
• Il segnale SIGCHLD (SIGCLD) indica al processo padre che un processo
138
figlio e’ terminato.
Unix system programming
• Ci sono segnali che non possono essere ignorati e segnali che non
sono catturati se non su richiesta esplicita (sono normalmente ignorati:
e.g. SIGCHLD).
• Se uno handler ritorna, l’esecuzione del programma riprende dal punto
in cui era stata interrotta.
• La ricezione di un segnale interrompe (sblocca) l’esecuzione di una
system call bloccante (che e’ bloccata):
 In questo caso la system call termina con il codice di errore EINTR.
 Non si tratta di un vero errore, ma e’ una condizione che deve
essere gestita esplicitamente dal programma.
139
Input/output multiplexing
• E’ possibile che un cliente debba interagire contemporaneamente con piu' di un
socket (in generale, file descriptor):
 Capita per esempio al printer server che utilizza contemporaneamente due
socket, uno Unix e uno Internet, per ricevere contemporaneamente richieste
di clienti locali e remoti.
 Capita al lato client di una applicazione di terminale remoto, in cui sono
presenti 2 sorgenti di input, il terminale fisico locale e la connessione con il
server remoto.
• Per risolvere il problema il cliente potrebbe:
•
Operare a polling sui due file descriptor (I/O non bloccante).
•
Operare in modo asincrono sui due file descriptor (SIGIO + I/O non
bloccante).
•
Attivare una seconda copia di se stesso, in modo che ogni copia gestisca un
solo file descriptor (e.g. multithreading Java, vedi Esercitazione 2).
• Esiste pero’ una quarta possibilita’: utilizzare la system call select().
 N.B.: la system call select() puo’ operare su qualunque tipo di file, non
solo su socket!
140
La system call select()
.1
#include <sys/types.h>
#include <sys/time.h>
int select(int maxfdpl,
fd_set *readfds, fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
•
Una chiamata alla select() ha il seguente significato:
dimmi se
• qualcuno dei file citati nell’insieme di file descriptor readfds e’
pronto per essere letto (ha dei dati disponibili o ha accettato una
connessione), o
• se qualcuno dei file citati nell’insieme di file descriptor writefds
e’ pronto per essere scritto (ha spazio nei buffer di scrittura), o
• se su qualcuno dei file citati nell’insieme di file descriptor
exceptfds e’ presente una situazione eccezionale.
•
N.B.: unica situazione eccezionale considerata:
ricezione di dati out-of-band.
141
La system call select()
.2
• Il tipo fd_set realizza il tipo di dato astratto “insieme di file” (insieme di
file descriptor).
• Concretamente, in Unix, il tipo fd_set e’ rappresentato come un array
di bit ciascuno dei quali e’ associato posizionalmente ad un file
descriptor.
• La rappresentazione concreta di Unix si basa sull’assunzione che un
file descriptor sia rappresentato concretamente tramite un intero non
negativo di piccola dimensione.
• Il tipo di dato astratto fd_set e’ comunque disponibile anche nei
sistemi Windows, dove un file descriptor, ed in particolare un socket
descriptor, non e’ rappresentato concretamente tramite un intero di
piccola dimensione.
• maxfdpl indica la lunghezza significativa massima delle 3 bit string
fd_set.
 L’utilizzo di maxfdpl e’ significativo (solo nei sistemi Unix!) per
aumentare l’efficienza sia della system call select() che del
codice chiamante, in quanto consente di limitare il numero di 142
file descriptor da esaminare durante la scansione del set.
La system call select()
.3
•
I parametri:
• readfds
• writefds
• exceptfds
sono value-result. Di ritorno, indicano quali sono i file pronti per
completare la relativa operazione di I/O.
•
Se non siamo interessati ad una certa classe di operazioni basta
porre a NULL il corrispondente parametro fd_set* nella chiamata.
•
La funzione ritorna:
• <0, in caso di errore
• 0, se nessun socket e' diventato pronto per una delle
operazioni di I/O richieste prima che scadesse il timeout.
• Il numero (positivo) dei socket che sono pronti per l'operazione
di I/O richiesta.
• Quindi la system call select() ritorna quanti e quali dei file
descriptor passati in ingresso sono pronti per essere acceduti in143
modo (sicuramente) non bloccante.
La system call select()
•
.4
Per capire quali socket sono pronti per l’operazione di I/O richiesta il
programma deve scandire gli array (insiemi) di socket descriptor e
testarne ciascun bit rilevante con l’operazione FD_ISSET().
 N.B.: questa e’ una descrizione basata sull’implementazione, non astratta
(funzionale). Come sarebbe la corrispospondente descrizione astratta?
•
Il tipo di dato astratto fd_set puo’ essere manipolato tramite le
seguenti operazioni
• void FD_ZERO(fd_set *fdset);
azzera fdset, e quindi rende vuoto il set.
• void FD_SET(int fd, fd_set *fdset);
inserisce il file descriptor (di numero) fd nel set fdset.
• void FD_CLR(int fd, fd_set *fdset);
elimina il file descriptor (di numero) fd dal set fdset.
• int FD_ISSET(int fd, fd_set *fdset);
verifica se il file descriptor (di numero) fd e’ presente (valore
144
ritornato !=0) o no (valore ritornato ==0) nel set fdset.
La system call select()
.5
• La struct timeval e' definita come
struct timeval {
long tv_sec;
long tv_usec; };
// seconds
// microseconds
• Il comportamento della system call select() e' controllato dal
valore del parametro timeout:
• Se timeout, !=NULL, riferisce una struct con tutti i campi nulli la
funzione e' non bloccante, ritorna subito dopo avere controllato lo
stato dei socket indicati nei primi quattro parametri.
• Se timeout==NULL la funzione e' bloccante, a tempo indefinito,
ritorna solo dopo che almeno uno dei socket indicati dai primi
quattro parametri e' pronto per l'operazione di I/O richiesta.
• Se timeout, !=NULL, riferisce una struct in cui non tutti i campi
sono nulli la funzione e' bloccante (in attesa che uno dei socket
sia pronto per l'operazione di I/O richiesta) ma solo per la
145
quantita' di tempo indicata da timeout.
La system call select(): ricetta
// consideriamo solo fd in lettura
int
readyNum, maxFd;
fd_set
readySet;
. . .
FD_ZERO(&readySet);
maxFd = compila(&readySet);
// readySet == set degli FD che interessano
// maxFd == fd massimo inserito in readySet + 1
if ((readyNum = select(maxFd, &readySet,
NULL, NULL, NULL)) < 0) {
err_dump ("server: select error");
}
// readyNum == numero degli fd pronti tra quelli che
//
interessavano
// readySet == set degli fd pronti tra quelli che
//
interessavano
. . .
 Domanda: dato il valore dei parametri di ingresso e’ possibile che in ritorno
146
sia readyNum==0?
Select(): esempio
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
.1
<stdio.h>
<stdlib.h>
<time.h>
<netdb.h>
<strings.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<arpa/inet.h>
<unistd.h>
#define SERV_TCP_PORT 6000
int main(int argc, char *argv[]) {
int
struct sockaddr_in
fd_set
struct timeval
sockfd, newsockfd,
clilen, tmp;
cli_addr, serv_addr;
ready;
tOut;
147
Select(): esempio
.2
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockfd < 0) {
err_dump("server: can't open socket");
}
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family
= AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port
= htons(SERV_TCP_PORT);
tmp = bind(sockfd, (struct sockaddr*) &serv_addr,
sizeof(serv_addr));
if (tmp < 0) {
err_dump("server: can't bind local socket");
}
listen(sockfd, 5);
148
Select(): esempio
.3
for (;;) {
FD_ZERO(&ready);
FD_SET(sockfd, &ready);
tOut.tv_sec = 5;
tOut.tv_usec = 0;
if ((tmp = select(sockfd+1, &ready,
NULL, NULL, &tOut)) < 0) {
err_dump ("server: select error");
}
if (tmp == 0) { // timeout expired
printf("no connection pending on socket\n");
149
Select(): esempio
.4
} else { // connection(s) must be pending on socket
if (!FD_ISSET(sockfd, &ready)) {
err_dump ("server: select-FD_ISSET error");
}
printf("connection(s) pending on socket\n");
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd,
(struct sockaddr*) &cli_addr,
&clilen); // non blocking!
if (newsockfd < 0) {
err_dump ("server: accept error");
}
close(newsockfd);
}
}
}
150
Esercizio: Comunicazione a messaggi su TCP
.1
• Nelle applicazioni di rete e’ spesso piu’ conveniente utilizzare una
comunicazione a messaggi piuttosto che a stream.
• Un esempio di cio’ si vedra’ parlando di applicazioni che fanno uso di
XDR.
• In effetti il servizio di trasporto OSI COTS e’ a messaggi e una
comunicazione affidabile a messaggi e’ offerta anche nel dominio
Xerox NS (AF_NS/PF_NS) tramite socket di tipo SOCK_SEQPACKET.
• Esercizio: Definire e implementare un servizio affidabile di
comunicazione a messaggi basato sul (che fa uso per la sua
implementazione del) servizio di trasporto TCP.
(vedi prossima pagina per i dettagli)
Definire anche un tipo di socket adatto a supportare questo servizio,
e.g. SOCK_TCPPACKET.
• Come deve essere definito tenendo conto che in effetti si sta
utilizzando una comunicazione TCP?
• E quale deve essere il protocollo indicato nella system call 151
socket()?
Esercizio: Comunicazione a messaggi su TCP
.2
• Dovete fare sostanzialmente 2 cose:
• Definire un protocollo che consenta di segmentare lo stream di byte
TCP in una sequenza di messaggi.
• Implementate le due seguenti funzioni:
int writeMsg(int sockfd, char *buff, int nch);
int readMsg(int sockfd, char *buff, int nch);
• Il comportamento di queste funzioni deve essere identico a quello delle
corrispondenti operazioni sui socket UDP (system call read() e
write() dell’API socket), salvo per il fatto che, basandosi sul TCP, il
servizio risultera’ affidabile.
• Per le altre funzioni necessarie per completare l’API del servizio (quali?)
si possono utilizzare direttamente le corrispondenti system call dell’API
socket.
• Il vostro servizio definisce una dimensione massima del messaggio o
no?
• Chiarite esattamente tutte le ipotesi necessarie al buon funzionamento
152
delle funzioni scritte.
Esercizio: Comunicazione a messaggi su TCP
.3
• Per la risoluzione dell’esercizio precedente ci si aspetta che il protocollo che
avete definito per consentire la segmentazione dello stream di byte TCP in una
sequenza di messaggi sia ispirato a quanto e’ stato fatto nell’Esercitazione 3.
• C’e’ pero’ una maniera alternative di realizzare la segmentazione in messaggi
dello stream TCP: in trasmissione ogni messaggio viene fatto precedere dal
carattere speciale STX e seguire dal carattere speciale ETX. In ricezione questi
caratteri, che saranno rimossi, consentono di effettuare la segmentazione.
• Ovviamente deve pero’ essere possibile trasmettere i caratteri STX e ETX
come parte del testo del messaggio. Per consentire cio’ ogni carattere STX o
ETX contenuto nel testo del messaggio sara’ prefissato in trasmissione dal un
carattere ESC, che indichera’ al ricevente che il carattere successivo non dovra’
essere interpretato ma dovra’ essere considerato come parte del testo. Anche
ogni carattere ESC contenuto all’interno del testo dovra’ essere prefissato in
trasmissione da un altro carattere ESC.
• I caratteri STX, ETX e ESC sono definiti nell’alfabeto ASCII con significato
analogo a quello utilizzato in questo esercizio (STX = start of text = 0x2, ETX =
end of text = 0x3, ESC = escape = 0x1B).
• Come si confrontano le due soluzioni dal punto di vista dell’efficienza, sia di
elaborazione sui due nodi che di comunicazione sulla rete?
• Distinguete i due casi in cui l’algoritmo di Nagle e’ o non e’ abilitato.
153
Esercizio: system call available()
• Realizzare utilizzando l’API socket in C una funzione analoga in
significato al metodo available() della classe InputStream di
Java (vedi lezione sull’API socket in Java).
• La funzione deve avere la seguente interfaccia:
int available(int sockfd);
• In ingresso ha un socket descriptor.
• In uscita ritorna:
• 1 se sul socket sono gia’ disponibili dei dati per la lettura, cosi’
che una chiamata della system call read() sul socket stesso
risulterebbe non bloccante,
• 0 in caso contrario.
• Ovviamente si assume un comportamento bloccante della system call
read() e delle altre system call analoghe.
• Come sarebbe possibile realizzare una funzione esattamente analoga
a quella Java, capace cioe’ di ritornare anche una stima del numero di
byte disponibili per la lettura nel socket?
154
Esercizio: interfaccia funzionale, protocollo, API
• Descrivere un semplice scenario di apertura di una connessione in cui
compaiano:
• Le due protocol entity TCP, initiator e responder;
• Le rispettive entita’ client;
• I TPDU scambiati tra le due protocol entity TCP;
• Le primitive funzionali previste dall’OSI sull’interfaccia funzionale del
layer di Trasporto;
• Le system call dell’API socket utilizzate effettivamente dai clienti del
TCP per inteargire con esso.
• Descrivere alcuni altri scenari significativi di tentativi riusciti o falliti di
apertura di connessione.
• Relazione interfaccia funzionale - API:
• Come si mappa l’interfaccia funzionale sull’API socket?
• L’API socket introduce dei vincoli rispetto all’interfaccia funzionale?
• Se si’, sono vincoli significativi rispetto al modello di interazione
155
client-server?
Esercizio: interazioni asincrone e non bloccanti
.1
• Scrivere in C un client di terminale remoto (vedi Esercitazione 2) basato
sull’utilizzo delle system call di accesso ai socket e ai file descritte in
questo capitolo.
Una applicazione di questo genere richiede che il processo sia in attesa
di input contemporaneamente sia dal socket che da standard input
(input multiplexing).
• In questo capitolo abbiamo visto 3 possibili soluzioni al problema che
abbiamo chiamato input multiplexing:
1. Utilizzo della system call select()
2. Utilizzo di operazioni di I/O non bloccanti e poll periodico delle
diverse sorgenti di input
3. Utilizzo di operazioni di I/O non bloccanti e di interazioni asincrone
(notifica asincrona della disponibilita’ di dati da parte del file
descriptor).
• L’esercizio richiede di implementare tutte e tre le soluzioni.
 Continua alla pagina successiva
156
Esercizio: interazioni asincrone e non bloccanti
.2
• Per quello che riguarda la terza soluzione vale quanto segue:
• Le operazioni con standard input e con il socket di comunicazione con il
server devono essere definite come non bloccanti.
• Standard input e il socket di comunicazione con il server devono essere
abilitati a generare un signal SIGIO verso il nostro processo client quando
hanno dati disponibili per la lettura.
• Il processo client deve registrare sul sistema operativo il proprio interesse a
gestire il signal SIGIO associandogli una opportuna procedura di handling
(N.B.: sara’ proprio questa procedura a svolgere tutto il lavoro del client di
terminale remoto dopo che l’infrastruttura di comunicazione con il server e’
stata messa in piedi).
• Le operazioni di read() effettuate nel contesto della funzione di signal
handling sono non bloccanti: e’ quindi possibile, nel contesto dell’esecuzione
di questa funzione, verificare la disponibilita’ di dati in ingresso sia su
standard input che sul socket.
• Il test del programma puo’ essere fatto utilizzando come server una shell
attivata dal superserver di rete inetd realizzato in Esercitazione 1 (vedi).
157
Esercizio: talk
• Un esercizio analogo al precedente consiste nella realizzazione di una
applicazione tipo talk, un programma di chat testuale da sempre disponibile sui
sistemi Unix
(vedi http://en.wikipedia.org/wiki/Talk_(software)).
• Anche in questo caso esiste un problema di input multiplexing, e anche in
questo caso esso puo’ essere risolto in un contesto di programmazione
tradizionale C/Unix utilizzando una delle tre tecniche gia’ esaminate
nell’esercizio precedente.
• Affrontando questo problema in Java la soluzione ovvia e’ naturalmente quella
di utilizzare il multithreading.
• Uno dei parametri di attivazione del programma sviluppato deve indicare se il
comportamento deve essere quello lato client o quello lato server
dell’applicazione.
• La differenza tra i due lati e’ limitata alla fase di setup della connessione: una
volta che questa e’ stata instaurata i due lati si comportano in modo
assolutamente identico.
• Non preoccupatevi dell’interfaccia uomo-macchina: quando ricevete qualcosa
dalla rete visualizzatelo immediatamente a terminale indipendentemente dal
fatto che l’utente locale stesse a sua volta digitando qualcosa; unica accortezza,
158
fare precedere e seguire il testo ricevuto da “\n”.
Scarica

socket