Unix: sistema operativo per reti.


Programmazione distribuita: benefici.

Breve storia di Unix

La comunicazione tra processi in Unix

Introduzione ai servizi di rete

Hangman in rete

Server concorrenti

Client per hangman

 

Mini data base in rete

 


 

 

1. Programmazione distribuita: benefici.

Possiamo raggruppare i benefici che si hanno con la programmazione distribuita in quattro categorie:

1. tool building: suddividere la soluzione di un problema in step per poter sfruttare programmi general purpose esistenti

2. concorrenza: utilizzo di più processori contemporaneamente

3. parallelismo: possibilità di far avanzare contemporaneamente più processi che collaborano per la soluzione di un problema

4. condivisione di risorse: le risorse possono essere distribuite su più computer

1.1 tool building.

Un esempio di costruzione di tool, utilizzando la shell di Unix è la pipe:

ls -l | more

find / -name core -print | mail root

La filosofia con cui è stato progettato Unix era proprio questa: molti "comandi semplici", facilmente combinabili per risolvere problemi complessi. Il vantaggio di questo approccio non è sicuramente la velocità, ma la flessibilità nel poter adattare il sistema a nuove e diverse esigenze.

1.2 concorrenza.

La potenza di un processore, sino a qualche anno fa, poteva essere approssimata da questa equazione:

potenza = 2 ^ (anno- 1984) MIPS

Questa legge ora comincia a rivelarsi errata. Ai nostri giorni si ottengono ottimi risultati aumentando il numero delle cpu di una macchina, più facilmente che aspettando evoluzioni tecnologiche che portino ad aumentare le già elevate prestazioni delle singole cpu.

Esistono differenti metodi per classificare le architetture di macchine multiprocessore: ai due estremi troviamo comunque macchine con memoria condivisa e macchine con memoria distribuita.

Se la memoria è condivisa al crescere del numero delle cpu la memoria diventa un collo di bottiglia, poichè tutte le cpu si contendono lo stesso bus per accedervi. Quindi arrivati a poche decine di cpu le prestazioni decadono. Al contrario la memoria distribuita permette di utilizzare anche centinaia di cpu, ma in questo caso i problemi devono essere scomposti in modo tale da ridurre al minimo la necessità di comunicazione tra i processori. In questo caso il problema diventa avere software adeguato in grado di sfruttare al meglio le molte cpu. Ed ai nostri giorni non è ancora possibile.

1.3 parallelismo.

Risolvere un problema in parallelo è spesso la soluzione più semplice. I problemi in questo caso nascono sia dall'abitudine dei programmatori a lavorare sequenzialmente, sia dal fatto che la maggioranza dei linguaggi di programmazione non ha strutture di controllo per il parallelismo. In C, ad esempio, non ci sono istruzioni per segnalare che una serie di passi possono essere svolti parallelamente. Su macchine Unix si può parzialmente risolvere il problema sviluppando più processi che comunicano tra loro i risultati. Altre tecniche prevedono l'utilizzo di librerie che sviluppano i thread. Alcuni recenti nuovi linguaggi, come java, hanno come costrutti del linguaggio la possibilità di utilizzare i thread e quindi di descrivere la soluzione di un problema in modo non sequenziale.

1.4 condivisione di risorse.

Una decina di anni fa se in una organizzazione diverse persone da uffici diversi avessero avuto la necessità di accedere a risorse di calcolo era necessario avere a disposizione un computer con sistema operativo time sharing, e con i terminali distribuiti negli uffici. In questo caso condividere file o stampante era semplice. Peró veniva condiviso, anche per problemi di costo, sia la memoria che la cpu. Al crescere del numero degli utenti le prestazione del sistema decadevano velocemente. Con l'avvento dei pc questa situazione migliorava ma tornava ad essere un problema la condivisione di file o di stampanti: i file potevano essere condivisi solo a costo di duplicazioni inutili e pericolose. La soluzione di questi problemi è stata la rete. Ogni utente ha la propria stazione di lavoro ma i file, la stampante e tutte quelle risorse giudicate importanti, ad esempio per il loro costo, vengono condivise.

Assieme alla rete nasce un altro concetto molto importante: la trasparenza. Trasparenza significa che si può accedere ad una risorsa remota nello stesso modo con cui si accede alla risorsa locale. Anzi un utente non si deve accorgere se la risorsa richiesta è locale o remota perchè è il sistema stesso che si fa carico di procurarla. Nel caso di reti basate su Unix la trasparenza è veramente elevata.

2. Breve storia di Unix distribuito.

Nel 1970 Unix divenne un sistema operativo multiuser e timesharing operante su computer con singolo processore, inizialmente PDP 11 e in seguito VAX, entrambi della Digital. I supporti per la programmazione distribuita era inesistenti. Unix permetteva il multitasking. Un programma poteva essere eseguito in background, aggiungendo & al termine della linea di comando, ed era possibile connettere tra loro con le pipe più processi. L'apetto innovativo era la chiamata di sistema fork(), che permetteva, e che anche attualmente permette, di crere facilmente nuovi processi. Un'altra chiamata di sistema veramente innovativa era la pipe(), che permetteva la costruzione di un canale di comunicazione tra un processo padre ed un processo figlio.

System V.

Negli anni successivi Unix accumula nuove utilities, ma il meccanismo di creazione e di comunicazione tra i processi rimane lo stesso. Circa nel 1984 AT&T introduce nuovi sistemi per IPC (inter process comunication). Questi sono la memoria condivisa, i semafori, le code. Questi permettono la comunicazione uno-a-uno come con le pipe ma anche molti-a-molti, garantendo maggiori possibilità di sincronizzazione tra processi. Tuttavia questi strumenti erano funzionanti solo nel caso in cui i processi avanzavano sulla singola macchina. System V non aveva supporti per la rete.

Unix networking.

L'evento più importante nella storia di Unix è stata l'adozione da parte di ARPA (US Advanced Research Projects Agency) come sistema per offrire ai numerosi ricercatori distribuiti nel Nord America un mezzo per collaborare su progetti. Il supporto di rete che ARPA sviluppò fu una implementazione del TCP/IP (Trasmission Control Protocol/Internet Protocol), che permetteva una facile comunicazione tra processi che avanzano su macchine distanti fra loro anche migliaia di chilometri.

É importante sottolineare che nel 1984 lo Unix 4.2 BSD della Berkeley University of California, offriva un insieme di chiamate di sistema che permetteva ai programmatori l'accesso ai servizi di reti basate su TCP/IP. Questo insieme di funzioni divenne il socket.

Con 4.2 BSD e con i socket divenne possibile sviluppare meccanismi di comunicazione tra processi su computer diversi e quindi sviluppare applicazioni distribuite su più computer.

In questo periodo nascono applicazioni quali telnet, che permette il login su macchine remote, ed ftp, per il trasferimento di file tra macchine. Il team di Berkeley scrisse altri programmi derivati dai normali comandi Unix, a cui è stato aggiunto un prefisso r, come rlogin, rcp, rsh.

Il modello di comunicazione che si sviluppa insieme ai socket è quello client-server. Il client richiede ad un server l'esecuzione di alcuni compiti ed attende la risposta. L'unica ragione per colloquiare col server remoto è che le risorse gestite dal server non sono disponibili localmente.

Nel 1986 AT&T sviluppa una nuova interfaccia verso il livello di trasporto, TLI, concettualmente equivalente ai socket.

Workstation.

I socket nascono nel 1984 su macchine Unix multiutente come il VAX. Ma in quel periodo nascevano i primi processori a 16/32 bit con elevata potenza, per cui l'idea di avere pc sufficientemente potenti da supportare Unix divenne realizzabile. Poichè il termine PC era giá stato utilizzato da IBM come nome per una famiglia di computer, il nuovo tipo fu chiamato workstation. Sun Microsystem fu una delle prime aziende ad offrire workstation con processore Motorola 68000, memorymanagement system, bit-mapped screen, scheda di rete (ethernet) e un sistema operativo: BSD Unix. Il fondatore di Sun, Bill Joy, era stato uno dei progettisti di BSD UNIX a Berkeley.

Trasparenza.

Lo Unix delle workstation è multitasking e multiuser, ma offre il massimo delle prestazioni quando è utilizzato in single user. La necessità di condividere le risorse diventa importante e non basta più telnet o ftp. In un sistema multiutente un utente effettua un login da qualunque terminale ed ha lo stesso environment. Si deve poter fare la stessa cosa in rete: un utente deve avere sempre la stessa area di lavoro a prescindere a quale workstation sia collegato. Per questo Sun sviluppa una nuova tecnologia, NFS (Network File System).

NFS permette di collegare una parte di file system remoto ad una directory locale, in modo che l'utente può accedere ai file locali o remoti, nello stesso modo, con gli stessi comandi. Lo stesso avviene per il programmatore, che utilizza le stesse chiamate di sistema sia per i file locali, sia per quelli remoti. Se si opera correttamente NFS permette di accedere a file remoti utilizzando sempre lo stesso pathname. NFS è quindi trasparente. Sun pubblicò le specifiche di NFS che fu accettato de facto come standard da tutti i costruttori.

In seguito Sun sviluppó una nuova interfaccia, chiamata RPC, (Remote Protocol Call) che permette ad un programma di fare chiamate a procedure che verrano eseguite su macchina remota. I risultati delle chiamate vengono indirizzati al programma chiamante in esecuzione su altro computer.

Un nuovo servizio basato su RPC è il NIS, che permette di distribuire su più computer i dati di configurazione. Ad esempio è possibile avere gli "utenti di rete".

L'evoluzione di Unix verso la rete è proseguita e dal 1992 è possibile avere potenti server di rete con più processori.

Inoltre si sono sviluppate nuove librerie per i "processi leggeri" (thread), che permettono di suddividere un processo in più processi leggeri che avanzano contemporaneamente su più processori.

L'aspetto comunque più rilevante di Unix è che tutti i protocolli di rete utilizzati non sono proprietari, per cui collegare in rete computer di venditori diversi è ora una realtà.

Cap. 3. La comunicazione tra processi in Unix.

3.1 Introduzione.

Lo schema a livelli che utilizzeremo in queste pagine è il seguente:


                                                           ------------------------------
                                                           |           USER             |
                                                           ------------------------------
                                                                             ^
                                                                             |
                                                                             |
                                                                             V
                                                 -------------------------------------------
                                                 |              User application           |
                                                 |                    program              |
                                                 |======================= ===============  |
                                                 |           Library  functions            |
                                                 |-----------------------------------------|
                                                                             ^
                                                                             |
                                                                             |
                                                                             V
                          -------------------------------------------------------------------------------
                          |                            SYSTEM CALL INTERFACE                             |
                          |                                                                              |
                          |   Memory management                                Process management        |
                          |                                                                              |     UNIX KERNEL
                          |                                                                              |
                          |   Network transport services                 File system management          |
                          |                                                                              |
                         --------------------------------------------------------------------------------|
                                                                             ^
                                                                             |   
                                                                             |
                                                                             V
                          ----------------------------------------------------------------------------------
                          |       HARDWARE:                 (CPU, memory, dischi, terminali                |
                          |                                      nastri, schede di rete, .......           |
                          ----------------------------------------------------------------------------------

Il livello 0 è il livello hardware. I comandi a disposizione a questo livello sono molto semplici, e sono del tipo: posiziona il braccio del disco sulla traccia 40, seleziona la testina 2 e leggi il settore0.

Il software agisce direttamente sull'hardware, non è portabile, e fa riferimento direttamente ai dettagli hardware, quali registri, controller, indirizzo di DMA, ed altro.

Il livello superiore è il livello kernel. Alcune importanti componenti di un kernel sono le seguenti:

Y Device driver: sono le routine che permettono la comunicazione con l'hardware poichè ne conoscono le caratteristiche.

Y gestione del file system: permette di organizzare la memorizzazione dei dati in file e directory, nascondendo la reale organizzazione del disco, fatta di blocchi, liste libere ed altro.

Y gestione dei processi: permette lo scheduling tra i processi utente, realizza il timesharing tra essi, e assegna ad ogni processo le risorse necessarie.

Y gestione della memoria: permette ai processi di condividere la memoria fisica garantendo ad essi accessi corretti e fornendo l'illusione ad ogni processo di essere il solo ad usufruire della memoria.

Y servizi di rete: permette la comunicazione macchina-macchina o processo-processo sulla rete.

In prima approssimazione il kernel può essere pensato come ad una serie di routines messe a disposizione dei programmi utente. L'insieme di queste routine definiscono una macchina virtuale Unix indipendente dalla piattaforma hardware.

Bisogna comunque ricordare che le routine del kernel non sono direttamente a disposizione degli utenti. Gli accessi al kernel sono controllati molto scrupolosamente. Un programma utente per accedervi deve effettuare una system call attraverso una istruzione trap . Questa istruzione modifica lo stato della cpu ponendola nello stato detto kernel e saltando ad un indirizzo di memoria ben definito, l'indirizzo di risoluzione dell'istruzione trap. I parametri passati insieme alla trap indicano al kernel il servizio richiesto. Quando l'operazione è terminata il processore torna allo stato user ed il controllo ritorna al programma chiamante.

Si può paragonare questo modo di operare al modo con cui vengono serviti i clienti di una panetteria. Si entra nel negozio, dietro al bancone sono esposti i prodotti; saremmo in grado di servirci da soli ma si devono richiedere al commesso che procede a servire un cliente alla volta.

Oltre ai servizi messi a disposizione dal kernel ci sono, a disposizione dei programmi, numerose funzioni di libreria che offrono parecchi servizi. Queste funzioni si possono dividere in due categorie: quelle che prevedono l'esecuzione con cpu in stato utente, e che quindi vengono collegate (linkate) allo spazio utente, e quelle che prevedono una system call. Ad esempio le funzioni che operano sulle stringhe, come strcmp(), strlen() operano in modalità utente. Le funzioni come printf() o scanf() fanno delle system call quali write() e read().

É buona norma non utilizzare direttamente in un programma le system call sia perchè non tutti i compilatori lo permettono sia per problemi di portabilità o compatibilità. Una chiamata alla funzione di libreria è più sicura perchè realizzerà in modo corretto la chiamata di sistema. Inoltre in release successive le chiamate di sistema potrebbero essere modificate, per cui i programmi che le utilizzano dovrebbero essere riscritti.

Le chiamate di sistema sono documentate nella sezione due del manuale, le funzioni di libreria nella sezione 3.

3.2. I.P.C. in Unix

I problemi relativi a Inter Process Comunication sono stati affrontati a lezione e sono trattati sugli appunti relativi al sistema operativo e sul libro di testo di Callegarin.

Per quanto riguarda le chiamate Unix per le pipe(), fork(), exec(), la gestione della memoria condivisa e dei semafori si rimanda agli appunti tratti dal libro Unix programmazione avanzata e alle esercitazioni svolte in laboratorio.

Rimane invece da sviluppare la parte relativa ai segnali e alla loro gestione da parte di un processo. Per ora ricordo solo che i segnali vengono inviati ad un processo dalla funzione kill().

Una trattazione completa di questo meccanismo di comunicazione verrá svolta in una prossima revisione di questi appunti.

4. Introduzione ai servizi di rete.

In questo capitolo verranno presentate alcune delle utility che Unix fornisce per operare in un sistema distribuito di rete. Tutto quanto vedremo é basato sul modello client-server.

4.1 Terminologia.

Un server é un processo che fornisce un servizio. Ad esempio rende disponibile risorse ad altri programmi in esecuzione sulla stessa macchina o su altre macchine della rete. Le risorse possono essere sia hardware che software.

Il server é attivo sulla macchina su cui é disponibile la risorsa ed attende passivamente fino a quando viene richiesto quel determinato servizio. A questo punto entra in esecuzione per soddisfare la richiesta.

Un client é un processo che necessita di una risorsa. La risorsa puo' essere indifferentemente sulla macchina su cui il processo sta avanzando o su un'altra macchina in rete. Un client, attraverso la rete, richiede i servizi di un server per avere a disposizione la risorsa necessaria. Attraverso il meccanismo client/server é possibile accedere ad una risorsa indipendentemente dal punto della rete su cui é allocata. Il client attraverso la rete effettua una connessione al server che gestisce la risorsa.

Da quanto detto sopra, un server o un client non sono dei computer ma dei processi, dei programmi in esecuzione. Addirittura puo' succedere che lo stesso processo puo' essere server e client contemporaneamente. Ad esempio un server, perché gestisce una parte di un file system, ed un cliente, per accedere ai servizi di una stampante remota.

Nell'ambiente Unix si usa spesso il termine demone per indicare un server. In genere questo significa che il server é sempre disponibile perché attivato, con particolari procedure, al momento del boot. Questo significa che il servizio fornito dal server é un servizio di sistema. Ma questo aspetto non é importante per le osservazioni svolte in precedenza. Ci possono essere particolari servizi svolti da server di utenti. Spesso in ambiente Unix si utilizza la lettera d alla fine di un nome per indicare che quel processo é un demone, cioé un server.

Un protocollo é un insieme di regole che descrive come un server e un client devono comportarsi per interagire, per capirsi. Definisce i comandi accettabili dal server e il preciso formato del messaggio che viene scambiato tra il client e il server. I protocolli possono essere di tipo testo, di tipo binario o addirittura possono utilizzare le strutture del C per definire il formato del messaggio. I protocolli devono anche permettere di indicare le situazioni di errore che possono verificarsi nel server, e come vengono segnalati al client.

4.2 Come individuare un server sulla rete.

Il problema che ogni client deve risolvere é come individuare il processo server sulla rete a cui rivolgersi per un servizio.

In genere il client deve per prima cosa individuare la macchina su cui il server sta avanzando. Ogni macchina in rete ha un nome, che é una stringa di caratteri. A livello piu' basso, ad ogni nome é associato un indirizzo di IP, che é un numero a 32 bit. Su ogni macchina sono attivi in genere diversi server, che hanno un ID diverso. Il sistema utilizzato per individuare il server corretto é il seguente: ogni server é in ascolto su una determinata porta, individuata da un numero a 16 bit. Il client, se conosce il numero utilizzato da un particolare server, esegue la richiesta su quella determinata porta ricevendo il servizio dal server in ascolto. Si puó paragonare ad un centralino telefonico, con diversi uffici e con numeri diversi. La prima parte del numero telefonico individua il centralino, la seconda parte l'ufficio. Conoscendo entrambi i numeri si puó colloquiare con la persona di quel particolare ufficio e ricevere il servizio offerto. Sulle macchine Unix l'elenco dei servizi con relative porte si trova descritto nel file /etc/services.

L'elenco dei servizi potrebbe non risiedere su tutte le macchine ma su una sola. In questo caso, attraverso apposite chiamate di rete é possibile ottenere l'informazione necessaria. In ogni caso il programmatore di un client ha a disposizione particolari routine, dette resolver, che permettono di avere le risposte corrette sui servizi, indipedentemente dalla tecnica utilizzata sulla rete.

Come osservazione possiamo dire che il metodo é primitivo perché bisogna sempre indicare la macchina a cui si richiede il servizio, e non si puó formulare la domanda come `` ho bisosgno del servizio di stampa''.

Su ogni macchina le prime 1024 porte sono riservate, che significa che solo i processi di root le possono utilizzare.

4.3. NFS

Uno dei problemi che si hanno utilizzando la rete é quello di distribuire i dati attraverso tutti i computer collegati in rete. Questo in genere avviene attraverso un file system distribuito. Il piú utilizzato e conosciuto é NFS.

NFS é stato sviluppato e distribuito dalla Sun Microsystem nel 1984. Le specifiche del protocollo utilizzato dalla Sun sono state subito rese di pubblico dominio, e questo ha permesso il successo di NFS, che di fatto é diventato indipendente dai venditori, ed é subito stato adottato da quasi tutti gli sviluppatori di sistemi Unix. Per capire come funziona NFS é necessario vedere come viene gestito il file system in Unix.

In genere un computer Unix ha uno o piú dischi suddivisi in una o piú partizioni. Per ciascuna partizione é definito un file speciale nella directory /dev, e ciascuna ha un file system completo con una tabelle degli inode.

Ciascuna di queste partizione diventa disponibile quando la device viene ``montata'' su una directory. Le directory sono una struttura gerarchica, alla cui radice c'é la root directory, contenuta nella root partition. A partire da questa partizione si possono creare delle directory vuote, che serviranno come punto di aggancio per un'altra partizione. Supponiamo di avere due partizioni, /dev/dsk/c0t1s0 e /dev/dsk/c0t1s1 che significa controller 0, drive 1 partition 0, e partition 1.

Supponiamo che la partition 0 sia la root partition. Supponiamo di aver creato una directory vuota /home.

Con il comando

mount /dev/dsk/c0t1s1 /home

colleghiamo la partition 1s1 alla directory /home. A questo punto il sistema é in grado di manipolare qualunque file della partizione 1 con il semplice riferimento alla directory /home. Queste operazioni avverranno in modo trasparente. L'utente non sa su quale partizione sta lavorando.

Partendo da questo concetto, con NFS é possibile montare parti di file system di un computer come se fossero partizioni di un altro computer.

Il comando da utilizzare é sempre mount:

mount -F nfs machine:/remote_dir /local_dir

Il file /etc/exports contiene le directory che sono `` esportabili'' mentre il file /etc/fstab contiene le device, locali o remote, che devono essere ``montate '' al boot.

Come esempio si puó vedere il file /etc/exports di venus e il file /etc/fstab di moon. In venus é esportata la directory /home che viene montata su moon alla directory /venushome. Tutti gli utenti di moon hanno come area di lavoro la stessa degli utenti di venus, condividono le stesse directory. Per poter fare questo é necessario che gli utenti di moon e quelli di venus siano identificati con lo stesso UID, User ID, nel file passwd.

4.4 Routine di accesso ad internet.

Le comunicazioni attraverso i computer dispersi in Internet avvengono in genere utilizzando le seguenti forme:

· telnet, che permette il login remoto;

· ftp, che permette il trasferimento di file;

· e-mail, che permette lo scambio di comunicazioni personali.

4.3.1. telnet.

Il telnet client, insieme con il corrispondente telnetd server, permette il login remoto su una rete basata su TCP/IP. Il controllo per l'accesso é svolto dal server. Se il server é attivo su una macchina Unix, per poter avere un login é necessario avere una utenza sulla macchina. Per avere l'accesso é allora necessario fornire il nome e la password, esattamente come per un utente locale. Inoltre é necessario, per lavorare correttamente, individuare un tipo di terminale, conosciuto sia dal client che dal server. In genere questo avviene individuando un semplice terminale virtuale di rete, che peró rischia di essere troppo semplice. La prima operazione che deve quindi essere effettuata dopo un login é cercare di individuare un tipo di terminale piu' sofisticato, conosciuto sia dal cliente che dal server, e chiedere di utilizzarlo, settando la variabile di envirnment TERM.

In genere il server telnetd é in ascolto sulla porta 23. Il client telnet puo` anche essere utilizzato per interrogare altre porte.

4.3.2. ftp.

FTP sta per file transfer protocol, ed é utilizzato sia come nome del protollo, sia come nome del client che utilizza tale protocollo. Il server é in genere chiamato ftpd. L'accesso é gestito in modo simile al telnet, in quanto viene richiesto un account, cioé un nome di utente ed una password. Esiste peró una eccezione, ed é FTP anonimo. Il server ftpd fornisce una shell ristretta di comandi per navigare attraverso il file system e per spedire o ricevere file. Anche in questo caso la macchina su cui sta avanzando ftpd non necessariamente é una macchina Unix.

ftp anonimo permette l'accesso in sola lettura ad una macchina su cui non si ha account. Questo é un meccanismo in genere utilizzato per prelevare software di pubblico dominio in Internet. ftp anonimo significa che esiste un utente anonymous, che non ha password, e nella cui area, accessibile in sola lettura, si trovano i file. In alcuni casi viene richiesta come password l'indirizzo di posta elettronica dell'utente che si vuole collegare.

5. Programmazione client-server utilizzando i socket.

Nelle pagine precedenti abbiamo visto quali meccanismi sono giá disponibili per sfruttare le risorse di una rete. In questo capitolo vedremo come scrivere nostri client o server.

Per fare questo é necessario conoscer qualche cosa in piú sul TCP/IP.

5.1. Livelli di protocollo.

Esaminiamo a basso livello come avviene la comunicazione su una rete basata sui protocolli TCP/IP. La figura seguente mostra lo stack dei protocolli coinvolti.  ---------------------                   FTP Protocol                --------------------- !    FTP Client       !     <------------------------------------  !   FTP Server        !  ---------------------                                               ---------------------      ^          !                                                         !       ^      !          V                                                         V       !  ---------------------                    TCP Protocol               --------------------- !  TCP               !      <-----------------------------------   !         TCP        !  ---------------------                                               ---------------------      ^          !                                                         !       ^      !          V                                                         V       ! ---------------------                     IP Protocol                -------------------- !        IP          !      <------------------------------------  !          IP       ! ---------------------                                                --------------------      ^          !                                                         !       ^      !          V                                                         V       ! ------------------------                Ethernet frames             ----------------------- ! Ethernet controller !    <----------------------------------    ! Ethernet controller   ! ------------------------                                            ------------------------           !                                                                    !           !                                                                    ! ========================================================================================

Il livello piu' alto dello stack nell'esempio sopra é occupato da FTP Client e da FTP server. Sono le due applicazioni che devono comunicare tra loro. Sono due applicazioni utente, e questo significa che sono state scritte, compilate ed eseguite nel solito modo.

Subito sotto ci sono due strati, detti TCP ed IP, che in genere risiedono nel kernel, anche se una piccola parte di essi puó essere implementata attraverso funzioni di libreria, e che quindi possono essere poi linkati ad ogni programma che lo richieda.

L'ultimo strato dello stack, detto Ethernet, é in genere una implementazione hardware, attraverso la scheda di rete.

Nel nostro esempio il client ed il server comunicano tra loro utilizzando il protocollo FTP. Questo protocollo definisce il formato dei messaggi e noi possiamo immaginare che il client ed il server comunicano tra loro direttamente. In realtá i dati passano al livello sottostante dello stack.

Le entitá di uno stack poste allo stesso livello sono dette peer, e la comunicazione tra i livelli corrispondenti é detta peer to peer.

Un programmatore per scrivere una applicazione come ftp dell'esempio precedente deve quindi conoscere due diverse interfacce:

&middot; il protocollo peer to peer dell'applicazione, per poter stabilire come devono essere inviati i messaggi

&middot;

&middot; l'interfaccia verso il livello di trasporto, (TCP) nel kernel. Questa consiste in alcune chiamate di libreria che permettono di ottenere i servizi di trasporto dal kernel.

5.1.1 Il livello ethernet.

Questo livello corrisponde approssimativamente al livello fisico e di datalink nel modello ISO OSI. Si interessa della trasmissione fisica di dati tra due macchine collegate sulla stessa rete. L'unitá di trasmissione é detta frame. Il formato di un frame é il sguente:                      indirizzo di           indirizzo di     tipo di            dati   preambolo           destinazione             partenza      pacchetto    (IP datagram)       CRC ------------------------------------------------------------------------------------------------------ !    64 bits       !     48 bits        !   48 bits        !  16 bits    ! 368-12000 bits !  32 bits  ! ------------------------------------------------------------------------------------------------------ Brevemente:

&middot; Il preambolo é una sequenza di bit che serve per aiutare la scheda ricevente a sincronizzarsi.

&middot;

&middot; l'indirizzo di destinazione, spesso indicato con il termine indirizzo-fisico, specifica la scheda di interfaccia a cui il messaggio é indirizzato. Ogni scheda di rete ha infatti un indirizzo, memorizzato sull'hardware della scheda, e che é diverso per tutte le schede.

&middot;

&middot; l'indirizzo sorgente indica quale scheda hardware ha inviato il frame.

&middot;

&middot; il tipo di pacchetto indica, attraverso un numero, a quale protocollo superiore nello stack deve essere inviato il frame. Ad esempio il valore 800 (hex) indica il protocollo IP. Su una stessa rete possono quindi coesistere diversi protocolli.

&middot;

&middot; i dati, che nel caso di TCP/IP sono un IP datagram.

&middot; il CRC serve per la verifica dei dati trasmessi.

5.1.2 Incapsulamento dei dati.

Prima di procedere con la descrizione degli altri livelli dobbiamo chiarire un importante concetto: quello dell'incapsulamento. In pratica nel passaggio da un livello a quello sottostante ai dati viene aggiunta una testata (un header ), che servirá per comunicare informazioni al livello corrispondente sulla macchina di destinazione. Ogni testata viene letta ed interpretata e quindi eliminata dal livello corrispondente sulla macchina di destinazione, che cosí saprá come comportarsi. Quindi ogni testata aggiunta da un livello fará parte del campo dati del livello sottostante. Quando il messaggio giunge a destinazione si procede in modo inverso, e quindi passando dal livello piu' basso a quello piu' alto vengono tolte le varie testate.

Il meccanismo sopra descritto puo' essere rappresentato dal seguente schema:

                                                                       ----------------------------------
                                                                      !    Dati dell'applicazione       !
                                                                      !                                 !
                                                                       ----------------------------------
                                                                      :                                              :
                                                                      :                                              :
                                                             --------------------------------------------
                                                             ! TCP    !    Dati dell'applicazione       !
                                                             ! header !                                 ! 
                                                             ---------------------------------------------
                                                             :                                                            :
                                                             :                                                            :
                                                ---------------------------------------------------------
                                                !  IP        !  TCP   !     Dati dell'applicazione      !
                                                ! header     !        !                                 !
                                                --------------------------------------------------------- 
                                                :                                                                              :
                                                :                                                                              :
                                 ------------------------------------------------------------------------
                                ! Ethernet      !   IP       !  TCP   !     Dati dell'applicazione      !
                                !  header       ! header     !        !                                 !
                                 ------------------------------------------------------------------------

5.1.3 Livello IP.

Il livello IP (Internet protocol) corrisponde approssimativamente al livello di rete nel modello ISO OSI, e sovraintende al trasferimento dei pacchetti da una macchina all'altra, realizzando l'importante funzione di instradamento (routing) del pacchetto, sfruttando, quando necessario le macchine gateway. Questo permette anche di inviare dati a macchine che non sono direttamente connesse alla stessa rete. Il pacchetto dati inviato dal livello IP si chiama IP datagram. Il metodo utilizzato per instradare un IP datagram attraverso le reti si basa non sull'indirizzo fisico di una scheda ma sull'indirizzo di livello piu' alto, detto indirizzo di IP. L'indirizzo di IP é un intero di 32 bit che é logicamente diviso in due parti. La prima parte individua la rete, ed é quindi utilizzata per l'instradamento, la seconda individua un host connesso alla rete. Attraverso il livello IP i datagram vengono inviati sulla rete senza una particolare connessione tra il client ed il server. Ogni datagram viaggia indipendentemente dagli altri, per cui puo' percorrere strade diverse. La conseguenza é che l'ordine di arrivo dei datagram non sempre corrisponde all'ordine con cui sono stati inviati. Il livello IP garantisce comunque che ogni datagram raggiunga la destinazione.

Il formato IP datagram:

------------------------------------------------------------------------------------------------------------
|            |                | Informazioni  |  T  |  protocol | Checksum | Sour. | Dest. | Op   | dati ..
|            |                | per frammen-  |  T  |           |          | addr  |  addr |      |      
|            |                |  tazione      |  L  |           |          |  IP   |  IP   |      |  
------------------------------------------------------------------------------------------------------------

&middot; il terzo campo permette al datagram di essere suddiviso e ricomposto se fosse troppo grosso per essere trasmesso in un frame ethernet.

&middot;

&middot; il campo successivo é il Time To Live, il tempo di vita: questo impedisce ad un datagram di viaggiare sulla rete superato un determinato tempo

&middot;

&middot; protocol indica il tipo di protocollo ( ad esempio TCP, UDP )

&middot;

&middot; Checksum permette di controllare la validitá della testata IP

5.1.4 Livello di trasporto.

Procedendo verso l'alto nello stack troviamo il livello di trasporto, che nelle macchine Unix in genere é o il protocollo TCP, o il protocollo UDP.

Questi due protocolli possono essere paragonati al servizio postale o al servizio telefonico.

Prendiamo in considerazione il servizio postale.

Ogni lettera ha indicato l'indirizzo di recapito. La lettera viene indirizzata attraverso il servizio postale tramite questo indirizzo. Non serve leggere la lettera per indirizzarla. Allo stesso modo con UDP si indica il punto di arrivo verso il quale il messaggio viene inviato.

Il servizio postale non deve confermare al mittente che la lettera é giunta a destinazione. Quindi quando ho inviato una lettera non ho la certezza che la lettera stessa sia giunta a destinazione. Allo stesso modo con il protocollo UDP non esiste un meccanismo che avverta del ricevimento del messaggio.

Il servizio postale non garantisce che le lettere giungano nello stesso ordine con cui sono state spedite. Lo stesso succede con il protocollo UDP, dove i datagram non é detto che giungano nello stesso ordine con cui sono stati inviati.

Vediamo ora le caratteristiche del servizio telefonico.

Quando voglio telefonare devo comporre un numero di telefono ( l'indirizo di destinazione ), ma una sola volta, prima di iniziare la conversazione. Durante la conversazione, non é piu' necessario indicare il destinatario. Similmente con il TCP indico il destinatario una volta sola, prima di iniziare la conversazione. Quando il contatto é avvenuto semplicemente invio i messaggi che voglio e questi vengono ricevuti dal destinatario.

Quando parlo al telefono la persona all'altro capo riceve le parole che pronuncio nello stesso ordine con cui le ho pronunciate. TCP garantisce che i dati vengono ricevuti nello stesso ordine con cui sono stati inviati.

La conversazione al telefono é bidirezionale, come con il TCP.

Con TCP se invio 100 bytes, poi altri 100, poi 50, il ricevente si vede notificato 250 bytes, che puo' leggere come desidera, o tutti 250, o 10 bytes alla volta, o diversamente. Con UDP viene conservata la forma del datagram, e quindi viene sempre letto un datagram alla volta.

5.1.4.1 Transmission contro protocol. (TCP)

Il TCP usa il numero di porta per inviare il messaggio, sia la porta del sorgente, sia quella del destinatario. Inoltre sono previsti bit per il controllo della correttezza del messaggio. In teoria il protocollo TCP funziona nel seguente modo. Quando viene inviato un TCP datagram viene attivato un timer. Quando il pacchetto giunge alla macchina di destinazione, questa invia un ACK al mittente che trasmette il successivo pacchetto, dopo ave peró azzerato il timer. Se per qualche motivo il destinatario non invia un ACK perché i dati risultano scorretti, perché il messaggio non é arrivato, o perché la macchina di destinazione risulta spenta, il sorgente, trascorso un certo tempo invia nuovamente lo stesso pacchetto. In pratica le fasi di invio e di ACK vengono parzialmente sovrapposte per velocizzare la trasmissione.

5.2 Socket

In Unix l'accesso al livello di trasporto, sia esso TCP o UDP, é realizzato attraverso una libreria, socket, che mette a disposizione otto nuove chiamate. Attraverso i socket i programmi applicativi si interfacciano con il livello di trasporto della rete. É come se i programmi inserissero i messaggi che devono viaggiare sulla rete in una speciale ``cassetta'' dove anche il livello di trasporto accede. Quando trova un messaggio lo invia utilizzando quanto previsto dal livello stesso. Questo permette di scrivere software senza conoscere nei dettagli come verranno inviati i messaggi.

5.3 Indirizzamento dei socket.

Quando due processi devono cooperare, devono avere qualche cosa in comune che permette loro la comunicazione. Ad esempio, come visto nelle esercitazioni di laboratorio, nel caso di memoria condivisa e di semafori, i processi fanno riferimento ad una chiave numerica comune. Nel caso di pipe si fa riferimento ad un descrittore di file che deve essere condiviso. Nel caso dei socket, ci sono differenti metodi per condividere il supporto comune, chiamati sui manuali address families.

Il primo schema di indirizzamento, chiamato UNIX address family, individua il socket come Unix file name. In questo caso un socket é simile ad una pipe. Il difetto di questo metodo é che il client ed il server devono risiedere sullo stesso computer.

Il metodo che utilizzeremo in seguito é chiamato internet domain addressing. In questo schema il nome del socket é comosto da due numeri: il primo, 32 bit, é l'indirizzo ip del computer dove il socket viene creato, mentre il secondo, 16 bit, é la porta interessata.

Le chiamate alle librerie che hanno in input l'indirizzo del socket sono flessibili e permettono entrambi gli indirizzamenti. Sono quindi definite delle strutture per entrambi i tipi di indirizzamento:

struct sockaddr_un {
        short sun_family;                         /* Tag: AF_UNIX */
        char sun_path[108];                     /* path name */
};

oppure

struct sockaddr_in {
        short                         sin_family;         /* Tag: AF_INET */
        u_short                     sin_port;             /* port number */
        struct in_addr           sin_addr;            /* IP address */
        char sin_zero[8];
};
 
 

Entrambi le strutture hanno in testa un tag che deve essere settato per indicare il tipo di indirizzamento utilizzato, poiché alcune funzioni dei socket ricevono in input il puntatore alla struttura e devono poter identificare il formato della struttura analizzando i primi due byte.

La struttura struct in_addr é cosí:


struct in_addr {
	u_long s_addr;
};

Esiste anche una struttura socket generica:

struct sockaddr {
	u_short sa_family;
	char sa_data[14];
};

In questo modo é possibile far riferimento alle funzioni in modo indipendente dall'indirizzamento scelto.

Un'altra complicazione nell'uso delle librerie é dovuta al fatto che ogni volta che si deve passare l'indirizzo di un socket, é necessario passare alla funzione anche la lunghezza della struttura indirizzo utilizzata.

5.4 Tipi di socket.

Indicano quale tipo, nel livello di trasporto, utilizzare:

SOCK_STREAM /* trasporto tipo TCP */

SOCK_DGRAM /* trasporto tipo UDP */

SOCK_RAW /* utilizzato alcune volte per riferirsi direttamente al livello IP */

5.5 Socket con tcp.

Per prima cosa é necessario stabilire una connessione tra il client e il server. Come spiegato in precedenza il server é in attesa su una porta per soddisfare le richieste dei client. Non sa né da dove né quando arriveranno le richieste. Il client invece sa quando deve collegarsi al server. Questo significa che client e server svolgeranno sequenze diverse di chiamate al socket.. La figura che segue mostra le sequenze di chiamata del server e del client.
 

Server 

Client

CREATE socket

 
   

Associa (BIND) un numero 

 

di porta al socket

 
   

Crea una coda di acolto (LISTEN) sulla porta 

CREATE socket

   

ACCEPT una connessione del server

CONNECT alla porta

   

READ dalla connessione 

WRITE nella connessione

   

WRITE nella connessione 

READ dalla connessione

   

END-OF-FILE per fine lavoro 

CLOSE la connessione

5.5.1 creazione del socket.

Il socket deve essere creato:
int sock;
sock = socket( AF_INET, SOCK_STREAM, 0);

Il primo argomento specifica in tipo di indirizzamento, il secondo il tipo, il terzo puó specificare il tipo di protocollo. Con 0 lascio la scelta al sistema.

In risposta ho il socket descriptor, che serve come riferimento al socket.

5.5.2. Indirizzo.

Al socket deve essere inviato un indirizzo, in questo caso un indirizzo di ip ed un numero di porta. Questo avviene utilizzando una struttura sockaddr_in, che deve essere inizializzata e passata con una chiamata bind().

#define SERVER_PORT 4333

struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(SERVER_PORT);
bind(sock, (strust sockaddr *) &server, sizeof server );

La porta indicata nell'esempio deve essere concordata con il client.In ogni caso non si devono utilizzare le prime 1024 porte che in genere sono riservate. La funzione htons converte il numero di porta in una forma comprensibile a tutte le macchine della rete.

INADDR_ANY significa che questo socket accetterá le connessioni da qualunque scheda di interfaccia presente sulla macchina. In genere comunque ogni macchina ha una sola scheda di interfaccia ed un solo numero IP.

5.5.3 Creazione della coda.

A questo punto si é pronti per segnalare al kernel che si é pronti ad accettare connessioni sul socket.

listen ( sock, 5);

Il numero 5 specifica il numero massimo di richieste di connessione non ancora esudite che il sistema puó accettare. Questo non é il numero di connessioni contemporanee ma quante richieste di connessione possono rimanere in sospeso prima che intervenga il sistema rifiutandole.

5.5.4 Attesa di richiesta.

A questo punto il server é pronto per mettersi in attesa di richieste di connessione.

struct sockaddr_in client;

int fd, client_len;

client_len = sizeof client;

fd = accept( sock, 7client, &client_len);

Con questa chiamata il sistema si pone in attesa di una richiesta di connessione.

Il secondo argomento é l'indirizzo di una struttura nella quale verrá posto l'indirizzo del cliente che si connette. Questo indirizzo non serve se non si devono svolgere controlli particolari sugli accessi dei client. Il terzo argomento serve per indicare quanto é grande l'area per il socket e viene modificato con il valore di quanti dati sono stati posti nel socket.

Accept restituisce un nuovo descrittore, che indica la nova connessione stabilita con il client. Questo nuovo descrittore puó essere pensato come un file descriptor, e quindi si possono compiere con esso tutte le operazioni di read e write.

É necessario prestare attenzione tra quanto abbiamo chiamato sock e fd.

Il primo indica il descrittore utilizzato per creare il collegamento. Con questo descrittore non si possono svolgere operazioni di input/output. Il secondo indica il collegamento, ed é utilizzato per l'input/output. É quello che serve per comunicare.

5.6 Esempi.

Gli esempi proposti sono tratti dal libro: Unix: distributed programming di Chris Brown, edito da Prentice hall. L'ultimo é tratto da Unix programmazione avanzata di Marc J. Rochkind e modificato utilizzando i socket.

5.6.1 Hangman in rete.

/* Network server for hangman game			*/
/* File: hangserver.c                       */

#include <sys/types.h
#include <sys/socket.h
#include <netinet/in.h
#include <stdio.h
#include <syslog.h
#include <signal.h
#include <errno.h

extern time_t time();

int maxlives = 12;
char *word[] = {
#include "words"
				};
#define NUM_OF_WORDS	(sizeof(word)/sizeof(word[0]))
#define MAXLEN  80		/* Maximum size of any string in the world */
#define HANGMAN_TCP_PORT 	1066

main()
{
	int sock, fd, client_len;
	struct sockaddr_in server, client;
	
	srand((int)time((long *)0));	/* randomize the seed */
	
	sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0) {
		perror("creating stream socket");
		exit(1);
	}
	
	server.sin_family		= AF_INET;
	server.sin_addr.s_addr	= htonl(INADDR_ANY);
	server.sin_port			= htons(HANGMAN_TCP_PORT);
	
	if(bind(sock, (struct sockaddr *) &server, sizeof (server)) < 0) {
		perror("binding soclet");
		exit(2);
	}
	
	listen(sock, 5);
	
	while(1) {
		client_len = sizeof(client);
		if((fd = accept(sock, (struct sockaddr *) &client, &client_len)) <0) {
			perror("accepting connection");
			exit(3);
		}
		play_hangman(fd, fd);
		close(fd);
	}
}

/* ----------------  play_hangman() ----------------------------------*/

play_hangman(int in, int out)
{
	char *whole_word, part_word[MAXLEN],
		  guess[MAXLEN], outbuf[MAXLEN];
	
	int lives = maxlives;
	int game_state ='I';
	int i, good_guess, word_length;
	char hostname[MAXLEN];
	
	gethostname(hostname, MAXLEN);
	sprintf(outbuf, "Playing hangman on host %s:\n\n", hostname);
	write(out, outbuf, strlen(outbuf));
	
	/* Pick a word at random from the list */
	whole_word = word[rand() % NUM_OF_WORDS];
	word_length = strlen(whole_word);
	syslog(LOG_USER|LOG_INFO, "hangman server chose word %s", whole_word);
	
	/* No letters are guessed initially */
	for(i=0; i < word_length; i++)
		part_word[i]='-';
	part_word[i]='\0';
	
	sprintf(outbuf, " %s     %d\n", part_word, lives);
	write(out, outbuf, strlen(outbuf));
	
	while( game_state == 'I')
		/* get a guess letter from player */
		{ while (read(in, guess, MAXLEN) < 0) {
			if(errno != EINTR)
				exit(4);
				printf("re-startin the read\n");
			}  /* re-start read() if interrupted by signal  */
		good_guess = 0;
		for(i=0; i<word_length; i++) {
			if(guess[0] == whole_word[i]) {
				good_guess = 1;
				part_word[i] = whole_word[i];
			}
		}
		if(!good_guess) lives--;
		if(strcmp(whole_word, part_word)==0)
			game_state = 'W';  /* W == User Won  */
		else if(lives==0) {
			game_state = 'L';  /* L== User Lost  */
			strcpy(part_word, whole_word);	/* Show User the word  */
		}
		sprintf(outbuf, " %s    %d\n", part_word, lives);
		write(out, outbuf, strlen(outbuf));
	}
}  							

É importante osservare la funzione play_hangman. Ha in input due file descriptors. Ad essi é associata la connessione con il client. play_hangman legge e scrive da questi due files, senza conoscere quello che ad essi é associato. Questo é lo schema comune a tutti i programmi che usano socket.

Quando il server viene eseguito su una macchina lo si puó consultare utilizzando il telnet. Basta indicare il nome della macchina e la porta che deve essere consultata!!

5.6.2 server concorrenti.

Nell'esempio precedente il server puó giocare una nuova partita solo quando la precedente é terminata. Per permettere di giocare piú partite si puó creare un nuovo processo figlio che gioca la partita mentre il padre é in attesa di un nuovo collegamento.

La modifica da apportare al ciclo while di accettazione delle connessioni é la sguente:

while (1) {

fd = accept( sock, .......);

if(fork() == 0) { /* é il processo figlio allora gioca! */

play_hangman(fd, fd);

exit(0);

} else /* il processo padre chiude la connessione */

close (fd);

}

Il programma modificato e completo é:

/* Network server for hangman game			*/
/* File: hangserver.c                       */

#include <sys/types.h
#include <sys/socket.h
#include <netinet/in.h
#include <stdio.h
#include <syslog.h
#include <signal.h
#include <errno.h

extern time_t time();

int maxlives = 12;
char *word[] = {
#include "words"
				};
#define NUM_OF_WORDS	(sizeof(word)/sizeof(word[0]))
#define MAXLEN  80		/* Maximum size of any string in the world */
#define HANGMAN_TCP_PORT 	1066

main()
{
	int sock, fd, client_len;
	struct sockaddr_in server, client;
	
	srand((int)time((long *)0));	/* randomize the seed */
	
	sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0) {
		perror("creating stream socket");
		exit(1);
	}
	
	server.sin_family		= AF_INET;
	server.sin_addr.s_addr	= htonl(INADDR_ANY);
	server.sin_port			= htons(HANGMAN_TCP_PORT);
	
	if(bind(sock, (struct sockaddr *) &server, sizeof (server)) < 0) {
		perror("binding soclet");
		exit(2);
	}
	
	listen(sock, 5);
	
	while(1) {
		client_len = sizeof(client);
		if((fd = accept(sock, (struct sockaddr *) &client, &client_len)) <0) {
			perror("accepting connection");
			exit(3);
		}
		if(fork() == 0) {
			play_hangman(fd, fd);
			exit(0);
		} else
			close(fd);
	}
}

/* ----------------  play_hangman() ----------------------------------*/

play_hangman(int in, int out)
{
	char *whole_word, part_word[MAXLEN],
		  guess[MAXLEN], outbuf[MAXLEN];
	
	int lives = maxlives;
	int game_state ='I';
	int i, good_guess, word_length;
	char hostname[MAXLEN];
	
	gethostname(hostname, MAXLEN);
	sprintf(outbuf, "Playing hangman on host %s:\n\n", hostname);
	write(out, outbuf, strlen(outbuf));
	
	/* Pick a word at random from the list */
	whole_word = word[rand() % NUM_OF_WORDS];
	word_length = strlen(whole_word);
	syslog(LOG_USER|LOG_INFO, "hangman server chose word %s", whole_word);
	
	/* No letters are guessed initially */
	for(i=0; i < word_length; i++)
		part_word[i]='-';
	part_word[i]='\0';
	
	sprintf(outbuf, " %s     %d\n", part_word, lives);
	write(out, outbuf, strlen(outbuf));
	
	while( game_state == 'I')
		/* get a guess letter from player */
		{ while (read(in, guess, MAXLEN) < 0) {
			if(errno != EINTR)
				exit(4);
				printf("re-startin the read\n");
			}  /* re-start read() if interrupted by signal  */
		good_guess = 0;
		for(i=0; i<word_length; i++) {
			if(guess[0] == whole_word[i]) {
				good_guess = 1;
				part_word[i] = whole_word[i];
			}
		}
		if(!good_guess) lives--;
		if(strcmp(whole_word, part_word)==0)
			game_state = 'W';  /* W == User Won  */
		else if(lives==0) {
			game_state = 'L';  /* L== User Lost  */
			strcpy(part_word, whole_word);	/* Show User the word  */
		}
		sprintf(outbuf, " %s    %d\n", part_word, lives);
		write(out, outbuf, strlen(outbuf));
	}
}  							

5.6.3 defunct

Se si prova ad utilizzare il codice visto sopra e si testa la tabella dei processi con ps si scoprirá che ci saranno dei processi segnati come defunct ma non ancora estratti dalla tabella. Questo perché ogni processo figlio deve restituire exit status al processo padre che nel nostro caso non é predisposto per riceverlo. Allora il figlio si mette in attesa per poter restituire questa informazione. Per eliminare questo problema é necessario aggiungere al figlio:

#include <signal.h

signal (SIGCHLD, SIG_IGN);

Il programma cosí modificato é:

/* Network server for hangman game			*/
/* File: hangserver.c                       */

#include <sys/types.h
#include <sys/socket.h
#include <netinet/in.h
#include <stdio.h
#include <syslog.h
#include <signal.h
#include <errno.h

extern time_t time();

int maxlives = 12;
char *word[] = {
#include "words"
				};
#define NUM_OF_WORDS	(sizeof(word)/sizeof(word[0]))
#define MAXLEN  80		/* Maximum size of any string in the world */
#define HANGMAN_TCP_PORT 	1066

main()
{
	int sock, fd, client_len;
	struct sockaddr_in server, client;
	
	srand((int)time((long *)0));	/* randomize the seed */
	
	sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0) {
		perror("creating stream socket");
		exit(1);
	}
	
	server.sin_family		= AF_INET;
	server.sin_addr.s_addr	= htonl(INADDR_ANY);
	server.sin_port			= htons(HANGMAN_TCP_PORT);
	
	if(bind(sock, (struct sockaddr *) &server, sizeof (server)) < 0) {
		perror("binding soclet");
		exit(2);
	}
	
	listen(sock, 5);

	signal(SIGCHLD, SIG_IGN);

	while(1) {
		client_len = sizeof(client);
		if((fd = accept(sock, (struct sockaddr *) &client, &client_len)) <0) {
			perror("accepting connection");
			exit(3);
		}
		if(fork() == 0) {
			play_hangman(fd, fd);
			exit(0);
		} else
			close(fd);
	}
}

/* ----------------  play_hangman() ----------------------------------*/

play_hangman(int in, int out)
{
	char *whole_word, part_word[MAXLEN],
		  guess[MAXLEN], outbuf[MAXLEN];
	
	int lives = maxlives;
	int game_state ='I';
	int i, good_guess, word_length;
	char hostname[MAXLEN];
	
	gethostname(hostname, MAXLEN);
	sprintf(outbuf, "Playing hangman on host %s:\n\n", hostname);
	write(out, outbuf, strlen(outbuf));
	
	/* Pick a word at random from the list */
	whole_word = word[rand() % NUM_OF_WORDS];
	word_length = strlen(whole_word);
	syslog(LOG_USER|LOG_INFO, "hangman server chose word %s", whole_word);
	
	/* No letters are guessed initially */
	for(i=0; i < word_length; i++)
		part_word[i]='-';
	part_word[i]='\0';
	
	sprintf(outbuf, " %s     %d\n", part_word, lives);
	write(out, outbuf, strlen(outbuf));
	
	while( game_state == 'I')
		/* get a guess letter from player */
		{ while (read(in, guess, MAXLEN) < 0) {
			if(errno != EINTR)
				exit(4);
				printf("re-startin the read\n");
			}  /* re-start read() if interrupted by signal  */
		good_guess = 0;
		for(i=0; i<word_length; i++) {
			if(guess[0] == whole_word[i]) {
				good_guess = 1;
				part_word[i] = whole_word[i];
			}
		}
		if(!good_guess) lives--;
		if(strcmp(whole_word, part_word)==0)
			game_state = 'W';  /* W == User Won  */
		else if(lives==0) {
			game_state = 'L';  /* L== User Lost  */
			strcpy(part_word, whole_word);	/* Show User the word  */
		}
		sprintf(outbuf, " %s    %d\n", part_word, lives);
		write(out, outbuf, strlen(outbuf));
	}
}  							

5.6.4 Client per hangman.

/*   hangclient.c  -  Client per hangman server.  */

#include <stdio.h
#include <sys/types.h
#include <sys/socket.h
#include <netinet/in.h
#include <netdb.h

#define LINESIZE 		80
#define HANGMAN_TCP_PORT	1066

int main(int argc, char *argv[])
{
	struct sockaddr_in server;	/* Server's address assembled here  */
	struct hostent *host_info;
	int sock, count;
	char i_line[LINESIZE];
	char o_line[LINESIZE];
	char *server_name;

/*  Get server name from command line. If none, use 'localhost'  */

server_name = (argc1) ? argv[1] : "localhost";

/*  Create the socket  */
	sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0) {
		perror("Creating stream socket");
		exit(1);
	}

	host_info = gethostbyname(server_name);
	if(host_info == NULL) {
		fprintf(stderr, "%s: unknown host: %s\n", argv[0], server_name);
		exit(2);
	}

/*  Set up the server's socket address, then connect  */

	server.sin_family = host_info-h_addrtype;
	memcpy((char*)&server.sin_addr, host_info-h_addr, host_info-h_length);
	server.sin_port = htons(HANGMAN_TCP_PORT);

	if(connect(sock, (struct sockaddr *)&server, sizeof server) < 0) {
		perror("connecting to server");
		exit(3);
	}
/*
 
OK: connected to the server.  
Prendi una linea dal server e mostrala, prendi una linea dall'utente e inviala al server. 
Ripeti sino a quando il server interrompe la connessione.

*/

	printf("connected to server %s\n", server_name);
	while((count = read(sock, i_line, LINESIZE))  0) {
		write(1, i_line, count);
		count = read(0, o_line, LINESIZE);
		write(sock, o_line, count);
	}
}

5.6.5 Mini database.

Questo ultimo esempio é piú complicato di quanti visti in precedenza. Si tratta di un server in grado di gestire alcune operazioni elementari su file e di un client che richiede tali operazioni. Puó essere preso come spunto per creare un vero gestore file (data base). Un tentativo é giá disponibile su hp (presto anche su Linux) ed é stato realizzato come progetto dagli alunni di 5 B i nell'anno scolastico 1990-91. In quel caso la comunicazione tra il server ed i client avveniva utilizzando la memoria condivisa ed i semafori. Sono a disposizione per chi fosse eventualmente interessato alla vecchia realizzazione.

Il file ha come record RCD, mentre il messaggio inviato tra client e server é MESSAGE.


/*******************************/
/*        dbms.h               */
/*******************************/
 
typedef struct {
        char name[20];
        char street[15];
        char city[10];
        char state[3];
        char zip[6];
        char tel[15];
} RCD;
 
typedef enum {OK, NOTFOUND, ERROR} STATUS;
 
typedef struct {
        long mtype;
        long clientkey;
        char cmd;
        char file[20];
        RCD rcd;
        STATUS status;
        int errno;
} MESSAGE;

Il client puó compiere alcune operazioni su un file. Per le operazioni é utilizzata la seguente libreria appositamente creata.

/*
	file name: libcli.c
	rev 1 del 26-2-97
	aggiunto socket ed eliminata memoria condivisa per comunicazione
*/
 
#define NULL 0
#include "dbms.h"
 
extern int errno;
static MESSAGE m;
extern int sock;
 
static STATUS dbmscall(RCD *r)
{
        char *getenv();
 
        if (m.clientkey == 0)
                m.clientkey = getpid();
        if (write(sock, &m, sizeof(m)) != sizeof(m))
                return (ERROR);
        if (read (sock, &m, sizeof (m)) != sizeof(m))
                return (ERROR);
        if (r != NULL)
                *r = m.rcd;
        if (m.status == ERROR)
                errno = m.errno;
        else
                return (m.status);
}
 
STATUS Dopen (char *file)
{
        m.cmd = 'o';
        strcpy (m.file,file);
        return (dbmscall(NULL));
}
 
STATUS Dcreate (char *file)
{
        m.cmd = 'c';
        strcpy (m.file,file);
        return(dbmscall(NULL));
}
 
STATUS Dclose( void )
{
        STATUS status;
 
        m.cmd = 'q';
        status = dbmscall(NULL);
        return (status);
}
 
STATUS Dtop( void )
{
        m.cmd = 't';
        return (dbmscall(NULL));
}
 
STATUS Dget (char *name, RCD *r)
{
        m.cmd = 'g';
        strcpy (m.rcd.name,name);
        return (dbmscall (r));
}
 
STATUS Dgetnext (RCD *r)
{
        m.cmd = 'n';
        return (dbmscall (r));
}
 
STATUS Dput (RCD *r)
{
        m.cmd = 'p';
        m.rcd = *r;
        return (dbmscall (NULL));
}
 
STATUS Ddelete (char *name)
{
        m.cmd = 'd';
        strcpy (m.rcd.name,name);
        return (dbmscall (NULL));
}
 

Come si puó osservare le operazioni di dbmscall utilizzano sock che é il fd della connessione.

Il server ha una sua libreria:


/*
	filename: libser.c
	rev.	26-2-1997
	primitive di un file server utilizzando librerie tcp 
	e funzione select.
	riferimento: 	per il gestore di file il riferimento e' 
			Unix programmazione avanzata di Rochkind
			per la comunicazione il riferimento e'
			Unix distributed programming di Brown	
*/
 
#include <stdio.h
#include <errno.h
#include <fcntl.h
#include "dbms.h"
 
static int fd = -1;
 
static STATUS Dopen(char* file)
{
        if ((fd = open (file, O_RDWR, 0)) == -1)
                return (ERROR);
        return (OK);
}
 
static STATUS Dcreate (char* file)
{
        if ((fd = open (file, O_RDWR | O_CREAT | O_TRUNC, 0666)) == -1)
                return (ERROR);
        return (OK);
}
 
static STATUS Dclose( void )
{
        if (close (fd) == -1)
                return (ERROR);
        return (OK);
}
 
long lseek();
extern int errno;
 
static STATUS Dtop ( void )
{
        if (lseek (fd, 0L, 0) == -1)
                return (ERROR);
        return (OK);
}
 
static STATUS Dget (char* name, RCD *r)
{
        int nread;
 
        if (Dtop () != OK)
                return (ERROR);
        while ((nread = read (fd, r, sizeof(RCD)))
                                == sizeof(RCD))
                if (strcmp (r-name, name) == 0) {
                        if (lseek (fd, -(long) sizeof (RCD), 1) == -1)
                                return (ERROR);
                        return (OK);
                }
        switch (nread) {
        case 0:
                return (NOTFOUND);
        case -1:
                return (ERROR);
        default:
                errno = 0;
                return (ERROR);
        }
}
 
static STATUS Dgetnext (RCD *r)
{
        while (1)
                switch (read (fd, r, sizeof (RCD))) {
                case sizeof (RCD):
                        if (r-name[0] == '\0')
                                continue;
                        return (OK);
                case 0:
                        return (NOTFOUND);
                case -1:
                        return (ERROR);
                default:
                        errno = 0;
                        return (ERROR);
                }
}
 
static STATUS Dput(RCD *r)
{
        RCD rcd;
 
        switch (Dget (r-name, &rcd)) {
        case NOTFOUND:
                if (lseek (fd, 0l, 2) == -1)
                        return (ERROR);
                break;
        case ERROR:
                return (ERROR);
        }
        switch (write (fd, r, sizeof (RCD))) {
        case sizeof (RCD):
                return (OK);
        case -1:
                return (ERROR);
        default:
                errno = 0;
                return (ERROR);
        }
}
 
static STATUS Ddelete (char* name)
{
        RCD rcd;
 
        switch (Dget (name, &rcd)) {
        case NOTFOUND:
                return (OK);
        case ERROR:
                return (ERROR);
        }
                                         
        rcd.name[0] = '\0';
        switch (write (fd, &rcd, sizeof  (RCD))) {
        case sizeof (RCD):
                return (OK);
        case -1:
                return (ERROR);
        default:
                errno = 0;
                return (ERROR);
        }
}
 
int process_request( int sfd ) 
{
        MESSAGE m;
	char name[30];
 
        if(read(sfd, &m, sizeof (m)) != sizeof (m)) return -1; 
 	switch (m.cmd) {
	case 'o': m.status = Dopen (m.file); break;
	case 'c': m.status = Dcreate (m.file); break;
	case 'q': m.status = Dclose(); break;
	case 'g': strcpy (name, m.rcd.name); m.status = Dget (name, &m.rcd); break;
	case 'n': m.status = Dgetnext (&m.rcd); break;
	case 'p': m.status = Dput (&m.rcd); break;
	case 'd': m.status = Ddelete (m.rcd.name); break;
	case 't': m.status = Dtop(); break;
	default: errno = EINVAL; m.status = ERROR;
	}
	m.errno = errno;
	if (write(sfd, &m, sizeof (m)) != sizeof (m)) return -1;
	if (m.cmd == 'q') return -1;
}

Ogni funzione utilizza fd per comunicare i risultati. process_request legge la richiesta del client ed esegue, in base al messaggio ricevuto, l'opportuna funzione.

Il client richiede il socket e predispone il collegamento:


/*
	file name: dbclient.c
	rev. 26-02-97
	client per accesso a file gestito da dbserver.c con socket.
	riferimenti: 	unix programmazione avanzata di rochkild
			unix distribuited programming di brown
*/	

#include <stdio.h
#include <sys/types.h
#include <sys/socket.h
#include <netinet/in.h
#include <netdb.h

#define DBSERVER_TCP_PORT 7777	

extern int dbclient( int );
 
int main(int argc, char *argv[])
{
	struct sockaddr_in server;	/* Server's address assembled here  */
	struct hostent *host_info;
	int sock, count;
	char *server_name;

/*  Get server name from command line. If none, use 'localhost'  */

	server_name = (argc1) ? argv[1] : "localhost";

/*  Create the socket  */
	sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0) {
		perror("Creating stream socket");
		exit(1);
	}

	host_info = gethostbyname(server_name);
	if(host_info == NULL) {
		fprintf(stderr, "%s: unknown host: %s\n", argv[0], server_name);
		exit(2);
	}

/*  Set up the server's socket address, then connect  */

	server.sin_family = host_info-h_addrtype;
	memcpy((char*)&server.sin_addr, host_info-h_addr, host_info-h_length);
	server.sin_port = htons(DBSERVER_TCP_PORT);

	if(connect(sock, (struct sockaddr *)&server, sizeof server) < 0) {
		perror("connecting to server");
		exit(3);
	}

	printf("connected to server %s\n", server_name);
	dbclient( sock );
}

Il programma vero e proprio è:


/*
	file name: db_cli.c
	rev. 26-02-97
	programma principale client per gestione file  
*/

#include <stdio.h
#include <errno.h
#include <fcntl.h
#include "defs.h"
#include "dbms.h"

int sock;
 
 
static void prompt (char *msg, char *result, int max, BOOLEAN required)
{
        char s[200];
        int len;
        while (1) {
                printf ("\n%s?", msg);
                if (gets (s) == NULL)
                        exit (0);
                len = strlen (s);
                if (len = max) {
                        printf ("Risposta troppo lunga\n");
                        continue;
                }
                if (len == 0 && required) {
                        printf ("Valore obbligatorio\n");
                        continue;
                }
                strcpy (result, s);
                return;
        }
}
 
static void rcdprint (RCD *r)
{
        printf ("Nome\t%s\n", r-name);
        printf ("Indirizzo\t%s\n", r-street);
        printf ("Citta'\t%s\n", r-city);
        printf ("Stato\t%s\n", r-state);
        printf ("Cap\t%s\n", r-zip);
        printf ("Telefono\t%s\n", r-tel);
}
 
int dbclient( int sfd )
{
        char cmd[5], file[50], name[30];
        RCD rcd;
        extern int errno;

	sock = sfd;
 
        while (1) {
                prompt ("comando (? per aiuto)",cmd,sizeof (cmd), TRUE);
                if (strlen (cmd) != 1) {
                        printf ("Una sola lettera\n");
                        continue;
                }
                switch (cmd[0]) {
                case '?':
                        printf (" o apre database\n");
                        printf (" c crea un database\n");
                        printf (" p inserisce un record\n");
                        printf (" d cancella un record\n");
                        printf (" g trova un record per key\n");
                        printf (" n trova il record successivo\n");
                        printf (" t si posizione in testa al database\n");
                        printf (" q fine esecuzione\n");
                        continue;
 
                case 'o':
                        prompt ("File da aprire", file, sizeof(file), TRUE);
                        if (Dopen (file) == OK)
                                printf ("OK\n");
                        else
                                printf ("Non riesco; errno=%ld\n", errno);
                        continue;
                case 'c':
                        prompt ("File da creare", file, sizeof(file), TRUE);
                        if (Dcreate (file) == OK)
                                printf ("OK\n");
                        else
                                printf ("Non riesco; errno=%ld\n", errno);
                        continue;
                case 'q':
                        if (Dclose() == OK)
                                printf ("OK\n");
                        else
                                printf ("Non riesco; errno=%ld\n", errno);
                        exit(0);
                case 'g':
                        prompt ("Name", name, sizeof(name), TRUE);
                        switch (Dget (name, &rcd)) {
                        case OK:
                                rcdprint (&rcd);
                                continue;
                        case NOTFOUND:
                                printf("Non trovato\n");
                                continue;
                        case ERROR:
                                printf ("Non riesco; errno=%ld\n", errno);
                                continue;
                        }
                case 'n':
                        switch (Dgetnext (&rcd)) {
                        case OK:
                                rcdprint (&rcd);
                                continue;
                        case NOTFOUND:
                                printf("Non trovato\n");
                                continue;
                        case ERROR:
                                printf ("Non riesco; errno=%ld\n", errno);
                                continue;
                        }
                case 'p':
                        prompt ("Nome", rcd.name, sizeof (rcd.name), FALSE);
                        prompt ("Indirizzo", rcd.street, sizeof (rcd.street), FALSE);
                        prompt ("Citta'", rcd.city, sizeof (rcd.city), FALSE);
                        prompt ("Stato", rcd.state, sizeof (rcd.state), FALSE);
                        prompt ("Cap", rcd.zip, sizeof (rcd.zip), FALSE);
                        prompt ("Telefono", rcd.tel, sizeof (rcd.tel), FALSE);
                        if (Dput (&rcd) == OK)
                                printf ("OK\n");
                        else
                                printf ("Non riesco; errno=%ld\n",errno);
                        continue;
                case 'd':
                        prompt ("Nome", name, sizeof (rcd.name), FALSE);
                        if (Ddelete(name) == OK )
                                printf ("OK\n");
                        else
                                printf ("Non riesco; errno=%ld\n", errno);
                        continue;
                case 't':
                        if (Dtop() == OK)
                                printf ("OK\n");
                        else
                                printf ("Non riesco; errno=%ld\n", errno);
                        continue;
                default:
                        printf ("Comando sconosciuto - usare ? per aiuto\n");
                }
        }
}
 

Il server utilizza la funzione select per verificare quale fd é pronto con i dati da leggere. Infatti la funzione read() si pone in attesa dei dati che potrebbero tardare ad arrivare. in questo caso il server rimarrebbe bloccato mentre ad esempio altri client potrebbero invece richiedere operazioni. Con select si controlla quale fd ha i dati pronti per leggere e si procede alla lettura ``a colpo sicuro''.


/* 
	file name: dbserver.c
	rev.	26-2-1997
	realizza un gestore file con
	single process, concurrent server using TCP transport and select()
*/

#include <sys/types.h
#include <sys/socket.h
#include <netinet/in.h
#include <sys/time.h
#include <sys/resource.h
#include <stdio.h

#define STOCK_PORT 7777

extern int process_request( int );

int main( void )
{
	int sock, fd, client_len;
	struct sockaddr_in server, client;
	int max_fd;
	
/* file descriptor sets used by select() 	*/
	fd_set test_set, ready_set;


	sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0) {
		perror("creating stream socket");
		exit(1);
	}

	server.sin_family = AF_INET;
	server.sin_addr.s_addr = htonl(INADDR_ANY);
	server.sin_port = htons(STOCK_PORT);

	if(bind(sock, (struct sockaddr *) &server, sizeof (server)) < 0) {
		perror("binding socket");
		exit(2);
	}

	listen(sock, 5);
	max_fd = sock;

	FD_ZERO(&test_set);
	FD_SET(sock, &test_set);

	while(1) {
		memcpy(&ready_set, &test_set,sizeof test_set);
		select(max_fd + 1, &ready_set, NULL, NULL, NULL);

		if(FD_ISSET(sock, &ready_set)) {
			client_len = sizeof client;
			fd = accept(sock,(struct sockaddr*)&client, &client_len);
			FD_SET(fd, &test_set);
			if(fd  max_fd) max_fd = fd;
		}

		for(fd=0; fd<=max_fd; fd++) {
			if((fd != sock) && FD_ISSET(fd, &ready_set)) {
				if(process_request(fd) < 0) {
					close(fd);
					FD_CLR(fd, &test_set);
				}
			}
		}
	}
}