Reti di Calcolatori L’interfaccia socket Alma Mater Studiorum - Universita' di Bologna Sede di Cesena II Facolta' di Ingegneria 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-2013 by Claudio Salati. Lez. 4
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 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.
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 Trasportodi 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).
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 socket che attraverso l'API TLI.
API, servizi, protocolli .1’ OSI TCP Xerox NS socket API 127.0.0.1 IP A1.A2.A3.A4 UDP Unix AF_inet loopback Ethernet
API, servizi, protocolli .1” OSI TCP Xerox NS TLI API 127.0.0.1 IP A1.A2.A3.A4 UDP Unix AF_inet loopback Ethernet
Comunicazioni locali socket API TCP UDP Unix OSI Xerox NS AF_inet 127.0.0.1 IP A1.A2.A3.A4 loopback Ethernet
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 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 positivo 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 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).
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' realizzato tramite i protocolli IP e ICMP.
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
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 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().
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 i 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.
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?
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.
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 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’.
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.
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!
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 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.
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.
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 unsigned char u_char; // 1 byte typedef unsigned short u_short; // 2 byte typedef unsigned int u_int; typedef unsigned long u_long; // 4 byte
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. 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. 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)
Indirizzo di un socket AF_INET Un socket AF_INET e’ definito come #include <sys/socket.h> struct sockaddr_in { u_short sin_family; // AF_INET u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; /* pad */ }; dove 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' 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.
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?
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.
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.
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).
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 (e questo e' 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 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 associato 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 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 entrambe gli end-point siano connessi tra loro, entrambi devono connettersi attivamente. Per i socket CL non esiste una operazione di apertura passiva di connessione.
Connessione passiva (accettazione) .1 Si applica solo a socket CO. E' tipica di un server. Per prima cosa il server CO indica che e' pronto ad accettare fino ad un certo numero 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 ricevute dai clienti sono accettate dalla macchina di protocollo dell'entita' di trasporto e accodate (fino a 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' andare ad accettare (in realta’, a prendere in carico) la prossima connessione (richiesta da un cliente e gia’ accettata dalla protocol entity TCP). Se nessuna connessione e' pendente, il server si sospende in attesa che una richiesta di connessione arrivi (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): a cosa serve?
Connessione passiva (accettazione) .3 TCP server side TCP client side connect ok listen(2) reject 1 conn. bufferata 2 conn. bufferate accept
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 dai client. 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. (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 che provengono da clienti graditi.
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 cliente 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.
La system call accept() In un contesto CO un socket rappresenta (sostanzialmente se non formalmente) il TSAP che consente di accedere e controllare: Una connessione di trasporto (gia’ instaurata)(socket di tipo 1). Una porta su cui aspettare la creazione di una connessione di trasporto da parte di un pari remoto. Apertura passiva della connessione. La connessione che e’ stata 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 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.
Creazione e utilizzo di una connessione - Esercizi 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. Introdurre in questi scenari anche la system call bind(). Da quale istante in poi, in questi scenari, un client puo’ effettivamente cominciare a inviare dati al server? Da quale istante in poi, in questi scenari, un server puo’ effettivamente cominciare ad acquisire e processare i dati inviatigli dal client? 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? 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”. 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?
Scenari per domanda 6 .1 user initiator TCP initiator side TCP responder side user responder listen.call listen.return connect.call SYN connect.return SYN+ACK ACK accept.call accept.return
Scenari per domanda 6 .2 user initiator TCP initiator side TCP responder side user responder listen.call listen.return accept.call connect.call SYN connect.return SYN+ACK ACK accept.return
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!
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 2 Porta client 1 Socket client (sc-ac-1) Socket client (sc-ac-2) Connessione 1 Connessione 2 Porta client 0 Socket client (sc-ac-0)
Server sequenziali e concorrenti Si possono immaginare due modalita' operative che possono essere utilizzate dal server nel rapportarsi con i suoi clienti (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 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 (tante 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.
Struttura canonica di un server CO sequenziale // trascuriamo la trattazione degli errori int sockfd, newSockfd; sockfd = socket(. . .); bind(sockfd, . . .); 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()?
Struttura canonica di un server CO concorrente // trascuriamo la trattazione degli errori int sockfd, newSockfd; sockfd = socket(. . .); bind(sockfd, . . .); 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); }
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);
Struttura canonica di un server CL "sequenziale" // trascuriamo la trattazione degli errori int sockfd; sockfd = socket(. . .); bind(sockfd, . . .); for (;;) { recvfrom(. . ., buff, . . .); doYourJob(buff); // prepara la risposta sendTo(. . .); } 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
Struttura canonica di un client CL // trascuriamo la trattazione degli errori int sockfd; sockfd = socket(. . .); bind(sockfd, . . .); // non sempre necessaria: // serve solo nel dominio // AF_UNIX connect(sockfd, . . .); // non necessaria askForService(sockfd); close(sockfd); exit(0);
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.
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()?
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 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 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, …).
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 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);
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 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.
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().
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 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?
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 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).
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. 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.
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? 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.
Lettura di una riga di testo su socket CO int readLine (int fd, char *line, int maxLen) { int n; for (n = 1; n < maxLen; n += 1) { int rc; char c; if ((rc = read(fd, &c, 1)) == 1) { *line = c; line += 1; if (c == '\n') break; // \n terminata } else if (rc == 0) { // EOF *line = '\0'; // null terminata return(n-1); } else { // rc<0, error return(rc); } *line = '\0'; // null terminata return(n);
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).
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 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.
Letture/scritture bloccanti 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) Socket TCP IP Socket TCP socket socket write() TX buffer RX buffer read() RX buffer TX buffer
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) 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 struttura canonica di un server CL concorrente.
Server CL concorrente .1 N.B.: Il server e’ di norma bloccato in read sulla porta well known in attesa (del primo messaggio) di nuovi clienti Server Padre socket server di associazione porta server well-known Client socket client porta effimera
Server CL concorrente .2 Server Padre socket server di associazione porta server well-known socket server di comunicazione porta server effimera Client socket client porta effimera
Server CL concorrente .3 fork() Server Padre socket server di associazione porta server well-known Server Figlio socket server di comunicazione porta server effimera Client socket client porta effimera
Server CL concorrente .4 Server Padre socket server di associazione porta server well-known Server Figlio socket server di comunicazione porta server effimera Client socket client porta effimera
Server CL concorrente .5 Server Padre socket server di associazione porta server well-known Server Figlio socket server di comunicazione porta server effimera Client socket client porta effimera
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 }
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);
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.
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. 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!
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.
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 porta 0.
Esercizi Esercizio 1: scrivere lo schema di un server CL veramente sequenziale secondo ciascuna delle 3 modalita’ indicate. 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?
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().
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() .
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!).
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 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) 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”.
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"); }
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"); }
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!
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); E perche’ la comunicazione e’ a messaggi! Ma questo non sarebbe stato molto percepibile operando con un socket pre-connesso.
Server concorrente TCP .1 #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERV_TCP_PORT 6000 int main(int argc, char *argv[]) { int sockfd, newsockfd, clilen, childpid, tmp; struct sockaddr_in cli_addr, serv_addr; sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sockfd < 0) { err_dump("server: can't open socket"); }
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 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?
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); exit(0); } else { // parent process close(newsockfd);
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: In network byte order. L’indirizzo IP in formato binario. 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.
Server sequenziale TCP .1 #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define SERV_TCP_PORT 6000 int main(int argc, char *argv[]) { int sockfd, newsockfd, clilen, childpid, tmp; struct sockaddr_in cli_addr, serv_addr; sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sockfd < 0) { err_dump("server: can't open socket"); }
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 delle system call listen()? Quali possono essere delle possibili ragioni non banali di fallimento per le system call socket(), bind() e listen()?
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);
Client TCP .1 #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <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 sockfd, tmp; struct sockaddr_in serv_addr; sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sockfd < 0) { err_dump("client: can't open socket"); }
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);
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");
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, 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 che dal socket di comunicazione verso il server.
Server “sequenziale” UDP .1 #include <stdio.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERV_UDP_PORT 6000 int main(int argc, char *argv[]) { int sockfd, tmp; struct sockaddr_in cli_addr, serv_addr; sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sockfd < 0) { err_dump("server: can't open socket"); }
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!!
Client “sequenziale” UDP .1 #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <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 sockfd, tmp; struct sockaddr_in cli_addr, serv_addr; sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sockfd < 0) { err_dump("client: can't open socket"); }
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);
Client UDP .nota L’indirizzo locale del socket cliente 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.
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, NULL); 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");
Server concorrente Unix stream .1 #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #define UNIXSTR_PATH "/tmp/unixstr" int main(int argc, char *argv[]) { int sockfd, newsockfd, servlen, clilen, childpid, tmp; struct sockaddr_un cli_addr, serv_addr; sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd < 0) { err_dump("server: can't open socket"); }
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);
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); exit(0); } else { // parent process close(newsockfd);
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.
Client Unix stream .1 #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #define UNIXSTR_PATH "/tmp/unixstr" #define MAXLINE 512 int main(int argc, char *argv[]) { int sockfd, servlen, tmp; struct sockaddr_un serv_addr; sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd < 0) { err_dump("client: can't open socket"); }
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);
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, char *optval, int *optlen); int setsockopt(int sockfd, int level, 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 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 l’opzione e' (deve essere) abilitata.
Socket options .2’ 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);
Socket options: parametro optname .3 Esempi: 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 real-time). 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.
Socket options: parametro optname .4 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).
Socket options: fcntl() 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 rilevanti di cmd sono F_GETFL, F_SETFL F_GETOWN, F_SETOWN F_GETFL e F_SETFL permettono rispettivamente di leggere e di settare (a 1, quindi di abilitare) 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. Per F_GETFL il valore delle flag e' fornito dal valore di ritorno della funzione.
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 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)
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 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.
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. Operare in modo asincrono sui due file descriptor. Attivare una seconda copia di se stesso, in modo che ogni copia gestisca un solo file descriptor. 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!
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.
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 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 l'operazione di I/O relativa. 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 in 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 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; // seconds long tv_usec; }; // 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 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 fd pronti
Select(): esempio .1 #include <stdio.h> #include <stdlib.h> #include <time.h> #include <netdb.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define SERV_TCP_PORT 6000 int main(int argc, char *argv[]) { int sockfd, newsockfd, clilen, tmp; struct sockaddr_in cli_addr, serv_addr; fd_set ready; struct timeval tOut;
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);
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");
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);
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 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 delle funzioni scritte.
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.
Interfaccia funzionale, protocollo, API: esercizio 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 client-server?