Esercizi LabOS

LEZIONE del 15 Dicembre


Implementazione dello scambio messaggi


Se si devono trasmettere messaggi booleani caratterizzati, pertanto, da due valori distinguibili, le Mailbox prendono il nome di Eventi. Le primitive di comunicazione, in questo caso, assumono il nome più significativo di await e cause e sono così introdotte
Possiamo allora definire le Mailbox come canali entro i quali trasmettere i messaggi a cui si aggiungono ulteriori informazioni di controllo che servono per la corretta gestione del traffico di messaggi all'interno del canale. Le informazioni di controllo sono trattate come eventi.
type Mailbox = record
                 Chn : Channel;
                 Ready : Event;
                 Busy : Event
               end;
In questo senso, Ready stabilisce quando un canale è pronto a ricevere un nuovo messaggio, mentre Busy notifica che il canale è temporaneamente occupato dal transito di qualche messaggio. Le primitive di comunicazione sono allora facilmente implementabili nel modo seguente
procedure send( var Mbx : Mailbox; Msg : Message );
begin
  with Mbx do
    begin
      await( Ready );
      Put( Chn, Msg );
      cause( Busy )
    end
end;

procedure send( var Mbx : Mailbox; var Msg : Message );
begin
  with Mbx do
    begin
      await( Busy );
      Get( Chn, Msg );
      cause( Ready )
    end
end;
Questo, naturalmente, non è l'unico modo possibile di implementare la comunicazione. Lo schema così realizzato è noto anche come protocollo RS232 ed è normalmente realizzato dalle porte seriali che connettono gli elaboratori ad una periferica di tipo terminale alfanumerico (ad esempio, un terminale VT100).

Nel caso, invece, di una connessione su coassiale di tipo ethernet si utilizza un protocollo di tipo CSMA/CD, il cui acronimo significa Carrier Sense Multiple Access/Carrier Detect. Sulla rete di comunicazione viene introdotta l'intestazione del messaggio e, se viene rilevato il transito di un messaggio, la spedizione viene ritardata.

Si noti, comunque, che l'introduzione e l'estrazione dei messaggi all'interno del canale viene fatta dalle due primitive

dove Channel è connesso al modo con cui i messaggi sono trasmessi lungo il canale mentre Message determina l'unità di informazione trasmessa.

Implementazione dello scambio messaggi in termini di socket

Il meccanismo dello scambio messaggi è implementabile utilizzando lo strumento delle socket, il cui scopo è quello di introdurre punti terminali di comunicazione ai quali viene associato univocamente un nome.

Le socket sono definite con un tipo il cui scopo è quello di rappresentare un dominio di comunicazione, che deve intendersi come un'astrazione che viene introdotta per raggruppare le proprietà comuni a tutti i processi che comunicano con quel tipo di socket. Una di queste proprietà è lo schema utilizzato per denominare le socket.

Le socket sono tipizzate secondo le proprietà di comunicazione visibili all'utente. Si presume che i processi comunichino solamente fra socket dello stesso tipo benchè non vi sia alcuna preclusione alla comunicazione fra socket di tipo diverso purchè il sottostante protocollo di comunicazione lo permetta. Normalmente sono disponibili tre tipi di socket che prendono il nome di

A parte la bidirezionalità del flusso di dati, una coppia di socket stream interconnessa fornisce un'interfaccia molto simile a quella delle pipe. Caratteristica importante delle socket datagram è la non alterazione della delimitazione dei record costituenti il messaggio. Questa socket modella il comportamento di molte reti a commutazione di pacchetto come Ethernet.

Definito un dominio, vi si possono introdurre un certo numero di socket ed accedervi mediante le seguenti operazioni primitive:

A questo punto si possono realizzare le operazioni di scambio messaggi precedentemente introdotte. In particolare, per quanto riguarda l'operazione send, tenendo presente che il cliente necessita della connessione solamente per il tempo necessario a spedire il messaggio, si ha
  procedure Send( var Mbx : Mailbox; Msg : Message );
  var S : Socket;
  begin
    MakeSocket( DOMAIN, S );
    Connect( S, Mbx );
    Put( S, Msg );
    Close( S )
  end;
dove il nome della socket Mbx è, in parte, determinato dalla scelta di DOMAIN, il dominio prescelto all'interno del quale viene stabilita la comunicazione. Come si intuisce facilmente, ogni qualvolta il cliente necessita di inviare un messaggio alla mailbox Mbx, viene creata temporaneamente una socket di trasmissione entro la quale viene introdotto il messaggio; successivamente la socket viene distrutta.

Per quanto riguarda, invece, l'operazione receive, questa può essere implementata nel modo seguente

  procedure Receive( var S : Socket; var Msg : Message );
  var P : Socket;
  begin
    Listen( S, N );		{ S = socket di trasmisione }
    Accept( S, P ); 		{ P = socket di comunicazione }
    if  fork = 0  then
      begin                 	
        Close( S );
        Get( S, Msg );
        Close( P )
      end}
  end;
nella quale si fa riferimento alla Mailbox con la corrispondente socket piuttosto che col suo nome globale. Questo è possibile purchè si supponga che esiste un unico server che ascolta sulla socket, cioè, i nomi globali devono considerarsi identificativi di porte. Si deve aggiungere, pertanto, la procedura StartReceive che appare nel seguito
  procedure StartReceive( Mbx : Mailbox; var S : Socket );
  begin
    MakeSocket( DOMAIN, S );
    Bind( S, Mbx ) 		{ S = socket di trasmissione }
  end;
il cui compito è quello di attivare il punto di connessione sul quale il server esegue le accettazioni di messaggi, essendo N il loro numero massimo in attesa di ricezione.

Esempio di Server


Il programma C che segue è un esempio di server iterativo costituito da una routine principale main.c che rappresenta l'ossuatura del server e la subroutine hangman.c che ne implementa la funzionalità. Per compilarlo usare il comando
gcc main.c hangman.c -o hangman -lsocket -lnsl
Per l'utilizzo del server procedere nel seguente modo.

Il programma principale

Segue la struttura del server che utilizza le primitive per la gestione delle socket. La signal serve per evitare che i figli terminino senza che il padre faccia una wait(0) per 'consumare' il return code.
/*
* main.c
*      main program implementing the skeleton of the server
*
*/
#include 
#include 
#include 
#include 
#include 
#include 
#include 

extern time_t time();

void play_hangman(int in, int out);

/* -------------------- main() -------------------- */
main (int argc, char *argv[])
{
  int port, sock, fd, client_len, value;
  struct sockaddr_in server, client;

 if (argc != 2) {
   fprintf(stderr, "Sintassi: %s ", argv[0]);
   exit(4);
 }
  port = atoi(argv[1]);

  setbuf(stdout,NULL);
       /* per emettere immediatamente l'output in stdout */

  srand((int)time((long *)0));
       /* randomize the seed */

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

  server.sin_family = AF_INET;
       /* per internet */
  server.sin_addr.s_addr = htonl(INADDR_ANY);
       /* indirizzo nullo: sceglie il sistema */
  server.sin_port = htons(port);

 /* quanto segue serve per per poter riusare piu' volte
  * lo stesso indirizzo
  *  (cioe' per piu' server lanciati successivamente )
  */

  value = 1;
  if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&value, sizeof(value)) 
< 0) {
    perror("during setsockopt");
    exit(5);
  }
  
  if (bind(sock, (struct sockaddr *) &server, sizeof(server)) < 0) {
    perror("binding socket");
      exit(2); 
  }

  listen(sock,2);
      /* il valore 2 indica la lunghezza coda attesa */

  signal(SIGCHLD,SIG_IGN); /* vedi nota in testa */

      /* il server e' implementato in modo concorrente, */
      /* cioe' viene clonato ad ogni nuova richiesta    */

  while (1) { 
    client_len = sizeof(client);
    if ((fd = accept(sock, (struct sockaddr *) &client, &client_len)) < 0) {
      perror("accepting connection");
      exit(3);
    }
    if (fork() == 0) {
         /* figlio */
      play_hangman(fd,fd);
      exit(0);
         /* alla fine il figlio termina (e chiude la connessione)*/
    } else
      close(fd);
         /* il padre chiude la connessione */
  }
}

La subroutine hangman


Definisce l'azione del server che, in questo caso specifico, rappresenta il Il gioco dell'impiccato. Ad ogni ciclo viene letta una riga dal file di input. Il primo carattere della riga e' la proposta del cliente. In output viene generata una stringa con le lettere indovinate ( il caratterre'-' indica una lettera non ancora scoperta) e il numero di possibilita' rimanenti.
Il gioco termina quando la parola e' stata indovinata o quando il cliente non ha piu' scampo.
/*
* hangman.c
*      play_hangman subroutine - implements the action of the server
*      Si noti che play_hangman() non sa se sta manipolando files,
*      socket o altri dispositivi
*/

#include 

int maxlives = 12;

char *word[] = {
#include "words"
};

#define NUM_OF_WORDS (sizeof(word)/sizeof(word[0]))
#define MAXLEN 80
  
void 
play_hangman(int in, int out)
{
  char *whole_word, part_word[MAXLEN], /* parola parziale */
       guess[MAXLEN], outbuf[MAXLEN];
  int lives = maxlives;
  int game_state = 'I'; /* incomplete */
  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));

  /* scegli una parola a caso */
  whole_word = word[rand() % NUM_OF_WORDS];
  word_length = strlen(whole_word);
  /* syslog(LOG_USER|LOG_INFO,
	 "hangman server chose word %s", whole_word);
  printf("hangman server chose word %s", whole_word);

  /* all'inizio non ci sono lettere indovinate */
  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') {
  /* leggi la proposta del cliente */
    while (read(in, guess, MAXLEN)<0) {
      if (errno != EINTR) {
	perror("while reading");
	exit(4); /* se altro errore termina */
      }
      printf("Restarting read\n"); /* se interrotto da un signal */
    }
  /* aggiorna la parola parziale */
    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';	/* client won */
    else if (lives == 0) {
      game_state = 'L';	/* client lost */
  /* mostra la soluzione */
      strcpy(part_word, whole_word);
    }
    sprintf(outbuf, " %s  %d\n", part_word, lives);
    write(out, outbuf, strlen(outbuf));
  }
}

Il file delle parole da indovinare

La routine hangman usa come database di parole il file words che contiene le stringhe, che rappresentano parole, nel seguente formato
"programmazione",
"robocup",
"riferimento",
"distribuito",
"sistema",
"concorrente"
"giocatore",
"orologio"