LEZIONE del 10 Dicembre

Nelle lezioni precedenti è stato affrontato il problema della comunicazione fra processi, a cui è stata data una soluzione generale fornendo un ambiente di comunicazione, noto col termine IPC, Inter-Process Communication, in cui ciascun processo è libero di dotarsi di un punto terminale d'ascolto, o socket.

Questo tipo di comunicazione prevede la possibilità di implementare lo schema generale di interazione fra processi, noto come schema multi-client/multi-server. L'impiego di tale schema è assai comune nelle reti di calcolatori, dove prende la forma ridotta ma non per questo meno generale di schema multi-client/single server.

Connessione Connessa

Si è visto come realizzare il lato server, nell'ipotesi di connessione connessa, ossia determinata da un protocollo in cui il cliente, che richiede un servizio al server, deve necessariamente e preventivamente negoziare una richiesta di connessione. La figura seguente evidenzia il ruolo delle diverse procedure eseguite sulle socket.

Le operazioni primitive di connect, lato client, e listen, lato server, consentono alle socket in questione di negoziare la connessione la quale, successivamente, dà luogo al vero e proprio trasferimento dati con le operazioni di accept e recv, lato server e send, lato client. Al termine, entrambi i processi coinvolti chiudono la connessione, tramite una close.

La realizzazione del server è stata presentata indipendentemente dal servizio fornito, la cui specifica implementazione è quella che risulta nell'ipotesi venga utilizzato l'usuale client TELNET per le connessioni remote su internet.

Si ricorda che il tipo di comunicazione, in questo caso, è inerentemente asimmetrico, a causa della struttura del protocollo di trasporto TCP realizzato dalla rete internet, sinonimo per la suite di protocolli trasporto-rete TCP/IP.

Connessione non Connessa

Nel seguito viene presentato l'esempio di una connessione fra processi con protocollo non connesso UDP il quale, non richiedendo una preventiva negoziazione della connessione, fa si che i lati client e server siano fra loro intercambiabili. Questo si riflette anche nel diverso tipo di primitive sendto e recvfrom utilizzate dalle socket dei due processi.

ESEMPIO DI CLIENTE

Affinchè il cliente possa interagire con il server è necessario che riesca a stabilire con esso uno scambio di informazioni coerente con il suo stato interno. La connessione proposta di seguito per il client è realizzata dal protocollo "non connesso" UDP, che si appoggia sul protocollo di rete IP della rete internet.

Viene riportato di seguito l'implementazione di client elementare che utilizza una connessione non connessa mediante socket UDP. Sulla falsa riga delle implementazioni con socket TCP verrà preso in esame esempio d'uso del servizio daytime che utilizza la connessione UDP. Il servizio è definito nell'RFC 867, che nel caso di uso di UDP prescrive che il client debba inviare un pacchetto UDP al server (di contenuto non specificato), il quale risponderà inviando a sua volta un pacchetto UDP contenente la data. Tuttavia, per generalità, nell'esempio in questione la porta viene passata come parametro.

/*
    udpcli.c : UDP Client
*/

#include     <stdio.h>
#include     <string.h>
#include     <netdb.h>
#include     <sys/socket.h>
#include     <netinet/in.h>
#include     <stdlib.h>

#define      MAXLINE     80
#define	     NAMESIZE	120

int main(int argc, char **argv) { 

  unsigned int portno;
  char hostname[NAMESIZE];
  struct hostent *host;
  struct sockaddr_in sin; 
  int sd; 
  char buf[MAXLINE];
  int len;

   if (argc < 3) {
     fprintf(stderr, "Sintassi: %s <Host> <Port>\n", argv[0]);
     exit(1); }

   strcpy(hostname, argv[1]);
   host = gethostbyname(hostname);
   portno = atoi(argv[2]);

   /* create socket */ 
   if ( (sd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { 
        perror("Socket creation error"); 
        return -1; }

   /* initialize address */ 

   memset((void *) &sin, 0, sizeof(sin));    /* clear server address */
   sin.sin_family = AF_INET;                     /* address type is INET */
   sin.sin_port = portno;                        /* daytime port is 13 */
   memcpy(&sin.sin_addr.s_addr, host->h_addr, host->h_length);

   /* send request packet */

   len = sendto(sd, NULL, 0, 0, (struct sockaddr *)&sin, sizeof(sin)); 
   if (len < 0) { 
        perror("Request error"); 
        return -1; 
   } 
   len = recvfrom(sd, buf, MAXLINE, 0, NULL, NULL); 
   if (len < 0) { 
        perror("Read error"); 
        return -1; 
   } 
    /* print results */ 
   if (len > 0) { 
        buf[len]=0; 
        if (fputs(buf, stdout) == EOF) {          /* write daytime */ 
            perror("fputs error"); 
            return -1; } 
   } 
   /* normal exit */ 
   return 0; 
}

Il codice che segue, implementa esplicitamente il servizio daytime con l'accortezza di definire la porta di ascolto per mezzo di un parametro passato esplicitamente durante la fase di lancio del servizio. Si noti la simmetricità d'impiego delle procedure sendto e recvfrom sia nel client che nel server.

/*
    udpserv.c : UDP Server
*/

#include     <stdio.h>
#include     <stdlib.h>
#include     <string.h>
#include     <netdb.h>
#include     <sys/socket.h>
#include     <netinet/in.h>

#define      TRUE        1
#define      MAXLINE    80

int main(int argc, char **argv) { 

  unsigned int portno;
  struct sockaddr_in sin;
  int sd;
  char buf[MAXLINE];
  int n, len;
  int verbose;
  time_t timeval;

   if (argc < 2) {
     fprintf(stderr, "Sintassi: %s [-v] <Port>\n", argv[0]);
     exit(1); }

   strcpy(buf, argv[1]);
   if ((buf[0] == '-')&&(buf[1] == 'v')) {
     verbose = 1;
     portno = atoi(argv[2]); }
   else {
     verbose = 0;
     portno = atoi(buf);
   }

   /* create socket */ 
   if ((sd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { 
        perror("Socket creation error"); 
        exit(-1); } 

   /* initialize address */ 

   memset((void *)&sin, 0, sizeof(sin));     /* clear server address */
   sin.sin_family = AF_INET;                     /* address type is INET */
   sin.sin_port = portno;                        /* daytime port is 13 */ 
   sin.sin_addr.s_addr = htonl(INADDR_ANY);      /* connect from anywhere */ 

   /* bind socket */ 

   if (bind(sd, (struct sockaddr *)&sin, sizeof(sin)) < 0) { 
        perror("bind error"); 
        exit(-1); } 

   /* write daytime to client */

   while (TRUE) { 
     n = recvfrom(sd, buf, MAXLINE, 0, (struct sockaddr *)&sin, &len);
     if (n < 0) { 
          perror("recvfrom error");
          exit(-1); }
     if (verbose) {
         inet_ntop(AF_INET, &sin.sin_addr, buf, sizeof(buf));
         printf("Request from host %s, port %d\n", buf, ntohs(sin.sin_port));}

     timeval = time(NULL);
     snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&timeval)); 
     n = sendto(sd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin)); 
     if (n < 0) { 
          perror("sendto error"); 
          exit(-1); } 
   }
   /* normal exit */
   exit(0);
}

Schema Client-Server: Protocolli e Servizi

Nella pratica implementativa lo schema client-server si basa su un più generale meccanismo di comunicazione che utilizza le socket come risorsa base di cui un processo deve dotarsi per poter effettivamente comunicare con un altro processo. A sua volta tale struttura utilizza la rete di comunicazione sottostante per poter trasferire i pacchetti, nei quali i messaggi vengono spezzati e, poichè una rete di elaboratori è determinata, oltre all'hardware che la realizza fisicamente, dai protocolli di rete e di trasporto, queste informazioni devono apparire necessariamente nei parametri che caratterizzano il tipo di socket impiegato.

Si è visto anche che la connessione realizzata fra un processo client ed un processo server può essere sia di tipo connesso che di tipo non connesso. Nel primo caso si usa come trasporto il protocollo TCP mentre nel secondo caso il protocollo utilizzato si chiama UDP. Nella connessione connessa i due processi eseguono un "preambolo" nel quale negoziano i parametri della connessione mentre nel secondo caso viene direttamente trasferito il messaggio e null'altro.

Si ricorda, infine, che l'interazione client-server non viene fatta direttamente sui processi ma sulle porte che sono il sostegno fornito dal kernel all'effettivo funzionamento delle socket le quali sono univocamente determinate, nel caso della rete internet, da un indirizzo IP e da un numero di porta.

Per quanto riguarda i servizi forniti dai server questi sono associati da numeri di porta ben specificati, come nella tabella riportata di seguito

FTP 21
SSH 22
TELNET 23
SMTP 25
DOMAIN 53
WHOIS 63
BOOTPS 67
BOOTPC 68
TFTP 69
GOPHER 70
FINGER 79
HTTP 80
POP2 109
POP3 110
NTP 123
IMAP 143
SNMP 161
IMAP3 220
HTTPS 443
DHCP 546

L'impiego del protocollo UDP e, quindi, della connessione non connessa si ha in tutti quei casi in cui l'informazione da trasmettere ha una dimensione limitata rispetto alla necessaria intestazione di gestione del flusso che il pacchetto TCP introdurrebbe. Tale protocollo è particolarmente utile nelle applicazioni in cui l'interazione con gli altri sistemi gioca un ruolo cruciale e va gestita in tempo reale come insieme di eventi asincroni ai quali va data comunque risposta.

Cliente per Connessioni UDP

Se si pensa di realizzare l'interazione fra processi cliente attraverso un server che genera messaggi come eventi asincroni, allora l'utilizzo di una connessione non connessa di tipo UDP permette di simulare in modo accettabile il fatto che tali messaggi non debbano necessariamente raggiungere il destinatario. Il codice riportato di seguito caratterizza la più semplice struttura di programma cliente per connettersi ad un server di tipo non connesso.

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

#define PORT        0x1600
                   /* REPLACE with your server machine name*/
#define HOST        "myhost.mydomain"
#define MSGSIZE     9600

main(argc, argv)
int argc; char **argv;
{
        char hostname[100];
        char    msg[MSGSIZE];
        int     mbx, sd;
        int     from;
        struct sockaddr_in server;
        struct sockaddr_in client;
        struct hostent *hp;

        strcpy(hostname,HOST);
        if (argc>2)
            { strcpy(hostname,argv[2]); }

        /* go find out about the desired host machine */

        if ((hp = gethostbyname(hostname)) == 0) {
                perror("gethostbyname");
                exit(1);
        }

        /* fill in the socket structure with server information */

        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = ((struct in_addr *)(hp->h_addr))->s_addr;
        server.sin_port = htons(PORT);

        if ((mbx = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
                perror("server socket");
                exit(1);
        }

        /* send a message to the server PORT on machine HOST */

        if (sendto(mbx, argv[1], strlen(argv[1]), 0,
                      (struct sockaddr *) &server, sizeof(server)) == -1) {
                perror("sendto");
                exit(1);
        }

        /* fill in the socket structure with client information */

        memset(&client, 0, sizeof(client));
        client.sin_family = AF_INET;
        client.sin_addr.s_addr = INADDR_ANY;
        client.sin_port = htons(PORT);

        if ((sd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
                perror("client socket");
                exit(1);
        }

        /* wait for a message to come back from the server */

        from = sizeof(server);
        if (recvfrom(sd, msg, MSGSIZE, 0,
                        (struct sockaddr *) &server, &from) == -1) {
                perror("recv");
                exit(1);
        }

        /* print out the results here! */

        printf("%s\n", msg);

        close(sd);
}