Background Client-Server

E' un mondo di client-server, piccola. Quasi tutto nelle reti ha a che fare con processi client che parlano con processi server e viceversa. Prendete il comando telnet, ad esempio. Quando vi collegate ad un host remoto sulla porta 23 con telnet (il client), un programma su quell'host (detto telnetd, il server) viene richiamato in vita. Esso gestisce la connessione in arrivo, vi da un prompt di login, etc.

Figure 2. Interazione Client-Server .

[Diagramma dell'Interazione Client-Server]

Lo scambio di informazionitra client e server è riassunto nella Figura 2.

Notate che la coppia client-server può comunicare con SOCK_STREAM, SOCK_DGRAM, o qualsiasi altra cosa (fintanto che continuano a parlare entrambi nello stesso modo.) Alcuni buoni esempi di coppie client-server sono telnet/telnetd, ftp/ftpd, o bootp/bootpd. Ogni volta che usate ftp, c'è un programma remoto, ftpd, che vi offre il servizio.

Spesso, ci sarà solo un server su una certa macchina, e quel server gestirà client multipli usando fork(). La routine di base è: server attende una connessione, la accetta con accept(), e crea un processo figlio con fork() per gestirla. Questo è quello che fa il nostro server d'esempio nella prossima sezione.

Un Semplice Stream Server

Tutto ciò che fa questo server è inviare la stringa "Ciao a tutti!\n" su una connessione di tipo stream. Tutto ciò che dovete fare per provare questo server è eseguirlo in una finestra, e collegarvi ad esso tramite telnet da un'altra con:

    $ telnet hostnameremoto 3490

dove hostnameremoto è il nome della macchina sulla quale sta girando il server.

Il codice del server: (Nota: una backslash su di una linea indica che quella linea continua a quella seguente.)

    /*
    ** server.c -- un esempio di server con socket stream
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <sys/wait.h>
    #include <signal.h>

    #define MYPORT 3490    // la porta che useranno gli utenti per collegarsi

    #define BACKLOG 10     // quante connessioni verrano mantenute in coda

    void sigchld_handler(int s)
    {
        while(wait(NULL) > 0);
    }

    int main(void)
    {
        int sockfd, new_fd;  // ascolta su sock_fd, le nuove connessioni su new_fd
        struct sockaddr_in my_addr;    // informazioni sul mio indirizzo
        struct sockaddr_in their_addr; // informazioni sull'indirizzo di chi si collega
        int sin_size;
        struct sigaction sa;
        int yes=1;

        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }
        
        my_addr.sin_family = AF_INET;         // host byte order
        my_addr.sin_port = htons(MYPORT);     // short, network byte order
        my_addr.sin_addr.s_addr = INADDR_ANY; // automaticamente riempita con il mio IP
        memset(&(my_addr.sin_zero), '\0', 8); // azzzera il resto della struttura

        if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))
                                                                       == -1) {
            perror("bind");
            exit(1);
        }

        if (listen(sockfd, BACKLOG) == -1) {
            perror("listen");
            exit(1);
        }

        sa.sa_handler = sigchld_handler; // raccoglie tutti i processi morti
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        if (sigaction(SIGCHLD, &sa, NULL) == -1) {
            perror("sigaction");
            exit(1);
        }

        while(1) {  // principale loop di accept() 
            sin_size = sizeof(struct sockaddr_in);
            if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
                                                           &sin_size)) == -1) {
                perror("accept");
                continue;
            }
            printf("server: connessione stabilita da %s\n",
                                               inet_ntoa(their_addr.sin_addr));
            if (!fork()) { // questo è il processo figlio
                close(sockfd); // il figlio non ha bisogno del socket in ascolto
                if (send(new_fd, "Ciao a tutti!\n", 14, 0) == -1)
                    perror("send");
                close(new_fd);
                exit(0);
            }
            close(new_fd);  // il genitore non ha bisogno di questo
        }

        return 0;
    } 

In caso foste curiosi, Io ho messo il codice in una sola grande main() perchè (Mi pare) sintatticamente chiaro. Sentitevi liberi di dividerlo in funzioni più piccole se vi fastare meglio.

(Inoltre, tuttaquella cosa con sigaction() potrebbe essere nuova per voi-- è tutto-- è tutto ok. Quel codice è responsabile di elminare i processi zombie che appaiono appena i processi figli creati con fork() terminano. Se create troppi zombie e non li raccogliete, il vostro amministratore di sistema potrebbe turbarsi.)

Potete ottenere i dati da questo server usando il codice del client nella prossima sessione.

Un Semplice Stream Client

Ragazzi, questo è anche più semplice del server. Tutti cio che fa questo client è collegarsi all'host che specificate da riga di comando, sulla porta 3490. E prende la stringa che il server gli manda.

il sorgente del client:

    /*
    ** client.c -- un semplice esempio di client che usa gli stream socket 
    */

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

    #define PORT 3490 // la porta che userà il client per collegarsi

    #define MAXDATASIZE 100 // massimo numero di byte che potremo leggere in una volta 

    int main(int argc, char *argv[])
    {
        int sockfd, numbytes;  
        char buf[MAXDATASIZE];
        struct hostent *he;
        struct sockaddr_in their_addr; // le information sull'indirizo di chi si collega

        if (argc != 2) {
            fprintf(stderr,"uso: client hostname\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  // prende le informazioni sull'host  
            perror("gethostbyname");
            exit(1);
        }

        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        their_addr.sin_family = AF_INET;    // host byte order 
        their_addr.sin_port = htons(PORT);  // short, network byte order 
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8);  // azzera il resto della struttura 

        if (connect(sockfd, (struct sockaddr *)&their_addr,
                                              sizeof(struct sockaddr)) == -1) {
            perror("connect");
            exit(1);
        }

        if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
            perror("recv");
            exit(1);
        }

        buf[numbytes] = '\0';

        printf("Ricevuto: %s",buf);

        close(sockfd);

        return 0;
    } 

Notate che se non avete lanciato il server prima di lanciare il client, connect() restituirà "Connection refused". Molto utile.

Datagram Socket

Non ho veramente molto da dire qui, dunque presenterò soltanto un paio di programmi: talker.c e listener.c.

listener sta in attesa su una macchina aspettando un pacchetto sulla porta 4950. talker invia un pacchetto a quella porta, sulla macchina specificata, che contiene qualsiasi cosa l'utente abbia scritto sulla riga di comando.

Qui c'è il codice di listener.c:

    /*
    ** listener.c -- un esempio di "server" con i socket datagrammi
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define MYPORT 4950    // la porta che verrà usata dagli utenti per collegarsi

    #define MAXBUFLEN 100

    int main(void)
    {
        int sockfd;
        struct sockaddr_in my_addr;    // le informazioni sul mio indirizzo
        struct sockaddr_in their_addr; // le informazioni sull'indirizzo del client
        int addr_len, numbytes;
        char buf[MAXBUFLEN];

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

        my_addr.sin_family = AF_INET;         // host byte order
        my_addr.sin_port = htons(MYPORT);     // short, network byte order
        my_addr.sin_addr.s_addr = INADDR_ANY; // automaticamente riempito con il mio IP
        memset(&(my_addr.sin_zero), '\0', 8); // azzera il resto della struttura

        if (bind(sockfd, (struct sockaddr *)&my_addr,
                                              sizeof(struct sockaddr)) == -1) {
            perror("bind");
            exit(1);
        }

        addr_len = sizeof(struct sockaddr);
        if ((numbytes=recvfrom(sockfd,buf, MAXBUFLEN-1, 0,
                           (struct sockaddr *)&their_addr, &addr_len)) == -1) {
            perror("recvfrom");
            exit(1);
        }

        printf("pacchetto ricevuto da %s\n",inet_ntoa(their_addr.sin_addr));
        printf("il pacchetto è lungo %d byte\n",numbytes);
        buf[numbytes] = '\0';
        printf("il pacchetto contiene \"%s\"\n",buf);

        close(sockfd);

        return 0;
    } 

Notate che nella nostra chiamata a socket() alla fine stiamo usando SOCK_DGRAM. Also, Inoltre notate che non c'è bisogno di listen() o accept(). Questo è uno dei vantaggi del'usare socket datagramma non connessi!

Subito dopo arriva il sorgente per talker.c:

    /*
    ** talker.c -- un esempio di "client" datagramma
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <netdb.h>

    #define MYPORT 4950    // la porta alla quale si collegherà 

    int main(int argc, char *argv[])
    {
        int sockfd;
        struct sockaddr_in their_addr; // le informazioni sul'indirizzo del connettore
        struct hostent *he;
        int numbytes;

        if (argc != 3) {
            fprintf(stderr,"uso: talker hostname messaggio\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  // prende le informazioni sull'host 
            perror("gethostbyname");
            exit(1);
        }

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

        their_addr.sin_family = AF_INET;     // host byte order
        their_addr.sin_port = htons(MYPORT); // short, network byte order
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8); // azzera il resto della struct

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

        printf("inviati %d byte a %s\n", numbytes,
                                               inet_ntoa(their_addr.sin_addr));

        close(sockfd);

        return 0;
    } 

E questo è tutto! Eseguite listener su una qualche macchina, poi eseguite talker su un'altra. Guardateli comunicare! Massimo Divertimento per tutta la famiglia nucleare!

Eccetto che per un piccolo dettaglio che ho già menzionato in passato: datagram socket connessi. Avrei bisogno di parlarvene ora, perchè siamo nella sezione datagrammi del documento. Diciamo che talker chiama connect() e specifica l'indirizzo di listener. Da quel punto in poi, talker potrà soltanto ricevere ed inviare dall'indirizzo specificato da connect(). Per questa ragione, non dovrete usare sendto() e recvfrom(); potrete usare semplicemente send() e recv().