Inserito esempio di server echo basato su select.
authorSimone Piccardi <piccardi@gnulinux.it>
Thu, 25 Dec 2003 22:16:06 +0000 (22:16 +0000)
committerSimone Piccardi <piccardi@gnulinux.it>
Thu, 25 Dec 2003 22:16:06 +0000 (22:16 +0000)
sources/Makefile
sources/select_echod.c
tcpsockadv.tex

index 151d779..b669397 100644 (file)
@@ -3,7 +3,7 @@
 #
 # C flags
 CC=gcc
-CFLAGS= -Wall -g -DDEBUG -fPIC
+CFLAGS= -Wall -g -fPIC #-DDEBUG
 CFLAGJ= -L./ -lgapil
 
 LIB = libgapil.so
index b1b6483..3a0690b 100644 (file)
@@ -26,7 +26,7 @@
  *
  * Usage: echod -h give all info
  *
- * $Id: select_echod.c,v 1.1 2003/12/25 17:31:09 piccardi Exp $ 
+ * $Id: select_echod.c,v 1.2 2003/12/25 22:16:06 piccardi Exp $ 
  *
  ****************************************************************/
 /* 
@@ -44,6 +44,7 @@
 #include <string.h>      /* error strings */
 #include <stdlib.h>
 
+#include "macros.h"
 #include "Gapil.h"
 
 #define BACKLOG 10
@@ -61,7 +62,7 @@ int main(int argc, char *argv[])
  */
     int waiting = 0;
     int compat = 0;
-    struct sockaddr_in serv_add, cli_add;
+    struct sockaddr_in s_addr, c_addr;
     socklen_t len;
     char buffer[MAXLINE];
     char fd_open[FD_SETSIZE];
@@ -122,12 +123,12 @@ int main(int argc, char *argv[])
        exit(1);
     }
     /* initialize address */
-    memset((void *)&serv_add, 0, sizeof(serv_add)); /* clear server address */
-    serv_add.sin_family = AF_INET;                  /* address type is INET */
-    serv_add.sin_port = htons(7);                   /* echo port is 7 */
-    serv_add.sin_addr.s_addr = htonl(INADDR_ANY);   /* connect from anywhere */
+    memset((void *)&s_addr, 0, sizeof(s_addr));   /* clear server address */
+    s_addr.sin_family = AF_INET;                  /* address type is INET */
+    s_addr.sin_port = htons(7);                   /* echo port is 7 */
+    s_addr.sin_addr.s_addr = htonl(INADDR_ANY);   /* connect from anywhere */
     /* bind socket */
-    if (bind(list_fd, (struct sockaddr *)&serv_add, sizeof(serv_add)) < 0) {
+    if (bind(list_fd, (struct sockaddr *)&s_addr, sizeof(s_addr)) < 0) {
        perror("bind error");
        exit(1);
     }
@@ -155,63 +156,67 @@ int main(int argc, char *argv[])
     if (waiting) sleep(waiting);
     /* initialize all needed variables */
     memset(fd_open, 0, FD_SETSIZE);   /* clear array of open files */
-    max_fd = list_fd;                 /* set maximum fd to look */
+    max_fd = list_fd;                 /* maximum now is listening socket */
     fd_open[max_fd] = 1;
     /* main loop, wait for connection and data inside a select */
     while (1) {    
        FD_ZERO(&fset);               /* clear fd_set */
-       for (i = list_fd; i <= max_fd; i++) {
-           if (fd_open[i]) FD_SET(i, &fset); /* initialize open sockets */
+       for (i = list_fd; i <= max_fd; i++) { /* initialize fd_set */
+           if (fd_open[i] != 0) FD_SET(i, &fset); 
        }
        while ( ((n = select(max_fd + 1, &fset, NULL, NULL, NULL)) < 0) 
-               && (errno == EINTR));
-       /* there are data */
-       if (n < 0) {
+               && (errno == EINTR));         /* wait for data or connection */
+       if (n < 0) {                          /* on real error exit */
            PrintErr("select error");
            exit(1);
-       }       
-       if (FD_ISSET(list_fd, &fset)) { /* if data on listening socket */
-           len = sizeof(cli_add);
-           fd = accept(list_fd, (struct sockaddr *)&cli_add, &len); 
-           if (fd < 0) {
+       }
+       /* on activity */
+       debug("Trovati %d socket attivi\n", n);
+       if (FD_ISSET(list_fd, &fset)) {       /* if new connection */
+           n--;                              /* decrement active */
+           len = sizeof(c_addr);             /* and call accept */
+           if ((fd = accept(list_fd, (struct sockaddr *)&c_addr, &len)) < 0) {
                PrintErr("accept error");
                exit(1);
            }
-           fd_open[fd] = 1;
-           if (max_fd < fd) max_fd = fd;
-           FD_SET(fd, &fset);
-           n--;
+           debug("Connessione su fd %d restano %d socket attivi\n", fd, n);
+           fd_open[fd] = 1;                  /* set new connection socket */
+           if (max_fd < fd) max_fd = fd;     /* if needed set new maximum */
+           debug("max_fd=%d\n", max_fd);
        }
-       for (i = list_fd + 1; i <= max_fd; i++) {
-           if (fd_open[i] == 0) continue;
-           if (FD_ISSET(i, &fset)) {
-               n--;
-               nread = read(fd, buffer, MAXLINE);
+       /* loop on open connections */
+       i = list_fd;                  /* first socket to look */
+       while (n != 0) {              /* loop until active */
+           i++;                      /* start after listening socket */
+           debug("restano %d socket, fd %d\n", n, fd);
+           if (fd_open[i] == 0) continue;   /* closed, go next */
+           if (FD_ISSET(i, &fset)) {        /* if active process it*/
+               n--;                         /* decrease active */
+               debug("dati su fd %d\n", i);
+               nread = read(i, buffer, MAXLINE);     /* read operations */
                if (nread < 0) {
                    PrintErr("Errore in lettura");
                    exit(1);
                }
-               if (nread == 0) {
-                   fd_open[i] = 0;
-                   if (max_fd == i) {
-                       while (fd_open[i] == 0) {
-                           i--;
-                       }
-                       max_fd = i;
-                       break;
+               if (nread == 0) {            /* if closed connection */
+                   debug("fd %d chiuso\n", i);
+                   close(i);                /* close file */
+                   fd_open[i] = 0;          /* mark as closed in table */
+                   if (max_fd == i) {       /* if was the maximum */
+                       while (fd_open[--i] == 0);    /* loop down */
+                       max_fd = i;          /* set new maximum */
+                       debug("nuovo max_fd %d\n", max_fd);
+                       break;               /* and go back to select */
                    }
-                   continue;
+                   continue;                /* continue loop on open */
                }
-               nwrite = FullWrite(fd, buffer, nread);
+               nwrite = FullWrite(i, buffer, nread); /* write data */
                if (nwrite) {
                    PrintErr("Errore in scrittura");
                    exit(1);
                }
            }
        }
-       if (n != 0) {
-           PrintErr("Errore nella gestione dei file descriptor");
-       }
     }
     /* normal exit, never reached */
     exit(0);
index d2e58fe..7351738 100644 (file)
@@ -416,31 +416,31 @@ velocit
 una macchina remota occorre un certo tempo perché i pacchetti vi arrivino,
 vengano processati, e poi tornino indietro. Considerando trascurabile il tempo
 di processo, questo tempo è quello impiegato nella trasmissione via rete, che
-viene detto RTT (da \textit{Round Trip Time}, e può essere stimato con l'uso
-del comando \cmd{ping}. 
+viene detto RTT (dalla denominazione inglese \textit{Round Trip Time}) ed è
+quello che viene stimato con l'uso del comando \cmd{ping}.
 
 A questo punto, se torniamo al codice mostrato in
 \figref{fig:TCP_ClientEcho_third}, possiamo vedere che mentre i pacchetti sono
 in transito sulla rete il client continua a leggere e a scrivere fintanto che
-il file in ingresso finisce. Però che non appena viene ricevuto un end-of-file
-in ingresso il nostro client termina. Nel caso interattivo, in cui si
-inviavano brevi stringhe una alla volta, c'era sempre il tempo di eseguire la
-lettura completa di quanto il server rimandava indietro. In questo caso
-invece, quando il client termina, essendo la comunicazione saturata e a piena
-velocità, ci saranno ancora pacchetti in transito sulla rete che devono
-arrivare al server e poi tornare indietro, ma siccome il client esce
-immediatamente dopo la fine del file in ingresso, questi non faranno a tempo a
-completare il percorso e verranno persi.
-
-Per evitare questo tipo di problema, invece di uscire occorre usare
-\func{shutdown} per effettuare la chiusura del lato in scrittura del socket,
-una volta completata la lettura del file in ingresso. In questo modo il client
-segnalerà al server la chiusura del flusso dei dati, ma potrà continuare a
-leggere quanto il server gli sta ancora inviando indietro fino a quando anche
-lui, riconosciuta la chiusura del socket in scrittura da parte del client,
+il file in ingresso finisce. Però non appena viene ricevuto un end-of-file in
+ingresso il nostro client termina. Nel caso interattivo, in cui si inviavano
+brevi stringhe una alla volta, c'era sempre il tempo di eseguire la lettura
+completa di quanto il server rimandava indietro. In questo caso invece, quando
+il client termina, essendo la comunicazione saturata e a piena velocità, ci
+saranno ancora pacchetti in transito sulla rete che devono arrivare al server
+e poi tornare indietro, ma siccome il client esce immediatamente dopo la fine
+del file in ingresso, questi non faranno a tempo a completare il percorso e
+verranno persi.
+
+Per evitare questo tipo di problema, invece di uscire una volta completata la
+lettura del file in ingresso, occorre usare \func{shutdown} per effettuare la
+chiusura del lato in scrittura del socket. In questo modo il client segnalerà
+al server la chiusura del flusso dei dati, ma potrà continuare a leggere
+quanto il server gli sta ancora inviando indietro, fino a quando anch'esso,
+riconosciuta la chiusura del socket in scrittura da parte del client,
 effettuerà la chiusura dalla sua parte. Solo alla ricezione della chiusura del
-socket da parte del server si potrà essere sicuri della ricezione di tutti i
-dati e della terminazione effettiva della connessione.
+socket da parte del server il client potrà essere sicuro della ricezione di
+tutti i dati e della terminazione effettiva della connessione.
 
 \begin{figure}[!htb]
   \footnotesize \centering
@@ -510,15 +510,15 @@ Seguendo di nuovo le orme di Stevens in \cite{UNP1} vediamo ora come con
 l'utilizzo dell'I/O multiplexing diventi possibile riscrivere completamente il
 nostro server \textit{echo} con una architettura completamente diversa, in
 modo da evitare di dover creare un nuovo processo tutte le volte che si ha una
-connessione.
+connessione.\footnote{ne faremo comunque una implementazione diversa rispetto
+  a quella presentata da Stevens in \cite{UNP1}.}
 
 La struttura del nuovo server è illustrata in \figref{fig:TCP_echo_multiplex},
 in questo caso avremo un solo processo che ad ogni nuova connessione da parte
-del client sul socket in ascolto inserirà il socket connesso ad essa relativo
-in una opportuna tabella, poi utilizzerà \func{select} per rilevare la
-presenza di dati in arrivo su ciascun socket connesso, riutilizzandolo per
-inviare i dati in risposta.
-
+di un client sul socket in ascolto si limiterà a registrare l'entrata in uso
+di un nuovo file descriptor ed utilizzerà \func{select} per rilevare la
+presenza di dati in arrivo su tutti i file descriptor attivi, operando
+direttamente su ciascuno di essi.
 
 \begin{figure}[htb]
   \centering
@@ -527,6 +527,141 @@ inviare i dati in risposta.
   \label{fig:TCP_echo_multiplex}
 \end{figure}
 
+La sezione principale del codice del nuovo server è illustrata in
+\figref{fig:TCP_SelectEchod}. Si è tralasciata al solito la gestione delle
+opzioni, che è identica alla versione precedente. Resta invariata anche tutta
+la parte relativa alla gestione dei segnali, degli errori, e della cessione
+dei privilegi, così come è identica la gestione della creazione del socket (si
+può fare riferimento al codice già illustrato in
+\secref{sec:TCPsimp_server_main}); al solito il codice completo del server è
+disponibile coi sorgenti allegati nel file \texttt{select\_echod.c}.
+
+\begin{figure}[!htbp]
+  \footnotesize \centering
+  \begin{minipage}[c]{15.6cm}
+    \includecodesample{listati/select_echod.c}
+  \end{minipage} 
+  \normalsize
+  \caption{La sezione principale del codice della nuova versione di server
+    \textit{echo} basati sull'uso della funzione \func{select}.}
+  \label{fig:TCP_SelectEchod}
+\end{figure}
+
+In questo caso, una volta aperto e messo in ascolto il socket, tutto quello
+che ci servirà sarà chiamare \func{select} per rilevare la presenza di nuove
+connessioni o di dati in arrivo, e processarli immediatamente. Per
+implementare lo schema mostrato in \figref{fig:TCP_echo_multiplex}, il
+programma usa una tabella dei socket connessi mantenuta nel vettore
+\var{fd\_open} dimensionato al valore di \macro{FD\_SETSIZE}, ed una variabile
+\var{max\_fd} per registrare il valore più alto dei file descriptor aperti.
+
+Prima di entrare nel ciclo principale (\texttt{\small 6--56}) la nostra
+tabella viene inizializzata (\texttt{\small 2}) a zero (valore che
+utilizzeremo come indicazione del fatto che il relativo file descriptor non è
+aperto), mentre il valore massimo (\texttt{\small 3}) per i file descriptor
+aperti viene impostato a quello del socket in ascolto,\footnote{in quanto esso
+  è l'unico file aperto, oltre i tre standard, e pertanto avrà il valore più
+  alto.} che verrà anche (\texttt{\small 4}) inserito nella tabella.
+
+La prima sezione (\texttt{\small 7--10}) del ciclo principale esegue la
+costruzione del \textit{file descriptor set} \var{fset} in base ai socket
+connessi in un certo momento; all'inizio ci sarà soltanto il socket in
+ascolto, ma nel prosieguo delle operazioni, verranno utilizzati anche tutti i
+socket connessi registrati nella tabella \var{fd\_open}.  Dato che la chiamata
+di \func{select} modifica il valore del \textit{file descriptor set}, è
+necessario ripetere (\texttt{\small 7}) ogni volta il suo azzeramento, per poi
+procedere con il ciclo (\texttt{\small 8--10}) in cui si impostano i socket
+trovati attivi.
+
+Per far questo si usa la caratteristica dei file descriptor, descritta in
+\secref{sec:file_open}, per cui il kernel associa sempre ad ogni nuovo file il
+file descriptor con il valore più basso disponibile. Questo fa sì che si possa
+eseguire il ciclo (\texttt{\small 8}) a partire da un valore minimo, che sarà
+sempre quello del socket in ascolto, mantenuto in \var{list\_fd}, fino al
+valore massimo di \var{max\_fd} che dovremo aver cura di tenere aggiornato.
+Dopo di che basterà controllare (\texttt{\small 9}) nella nostra tabella se il
+file descriptor è in uso o meno,\footnote{si tenga presente che benché il
+  kernel assegni sempre il primo valore libero, dato che nelle operazioni i
+  socket saranno aperti e chiusi in corrispondenza della creazione e
+  conclusione delle connessioni, si potranno sempre avere dei \textsl{buchi}
+  nella nostra tabella.} e impostare \var{fset} di conseguenza.
+
+Una volta inizializzato con i socket aperti il nostro \textit{file descriptor
+  set} potremo chiamare \func{select} per fargli osservare lo stato degli
+stessi (in lettura, presumendo che la scrittura sia sempre consentita). Come
+per il precedente esempio di \secref{sec:TCP_child_hand}, essendo questa
+l'unica funzione che può bloccarsi, ed essere interrotta da un segnale, la
+eseguiremo (\texttt{\small 11--12}) all'interno di un ciclo di \code{while}
+che la ripete indefinitamente qualora esca con un errore di \errcode{EINTR}.
+Nel caso invece di un errore normale si provvede (\texttt{\small 13--16}) ad
+uscire stampando un messaggio di errore.
+
+Se invece la funzione ritorna normalmente avremo in \var{n} il numero di
+socket da controllare. Nello specifico si danno due possibili casi diversi per
+cui \func{select} può essere ritornata: o si è ricevuta una nuova connessione
+ed è pronto il socket in ascolto, sul quale si può eseguire \func{accept} o
+c'è attività su uno dei socket connessi, sui quali si può eseguire
+\func{read}.
+
+Il primo caso viene trattato immediatamente (\texttt{\small 17--26}): si
+controlla (\texttt{\small 17}) che il socket in ascolto sia fra quelli attivi,
+nel qual caso anzitutto (\texttt{\small 18}) se ne decrementa il numero in
+\var{n}; poi, inizializzata (\texttt{\small 19}) la lunghezza della struttura
+degli indirizzi, si esegue \func{accept} per ottenere il nuovo socket connesso
+controllando che non ci siano errori (\texttt{\small 20--23}). In questo caso
+non c'è più la necessità di controllare per interruzioni dovute a segnali, in
+quanto siamo sicuri che \func{accept} non si bloccherà. Per completare la
+trattazione occorre a questo punto aggiungere (\texttt{\small 24}) il nuovo
+file descriptor alla tabella di quelli connessi, ed inoltre, se è il caso,
+aggiornare (\texttt{\small 25}) il valore massimo in \var{max\_fd}.
+
+Una volta controllato l'arrivo di nuove connessioni si passa a verificare se
+vi sono dati sui socket connessi, per questo si ripete un ciclo
+(\texttt{\small 29--55}) fintanto che il numero di socket attivi \var{n} resta
+diverso da zero; in questo modo se l'unico socket con attività era quello
+connesso, avendo opportunamente decrementato il contatore, il ciclo verrà
+saltato, e si ritornerà immediatamente (ripetuta l'inizializzazione del file
+descriptor set con i nuovi valori nella tabella) alla chiamata di
+\func{accept}. Se il socket attivo non è quello in ascolto, o ce ne sono
+comunque anche altri, il valore di \var{n} non sarà nullo ed il controllo sarà
+eseguito. Prima di entrare nel ciclo comunque si inizializza (\texttt{\small
+  28}) il valore della variabile \var{i} che useremo come indice nella tabella
+\var{fd\_open} al valore minimo, corrispondente al file descriptor del socket
+in ascolto.
+
+Il primo passo (\texttt{\small 30}) nella verifica è incrementare il valore
+dell'indice \var{i} per posizionarsi sul primo valore possibile per un file
+descriptor associato ad un eventuale socket connesso, dopo di che si controlla
+(\texttt{\small 31}) se questo è nella tabella dei socket connessi, chiedendo
+la ripetizione del ciclo in caso contrario. Altrimenti si passa a verificare
+(\texttt{\small 32}) se il file descriptor corrisponde ad uno di quelli
+attivi, e nel caso si esegue (\texttt{\small 33}) una lettura, uscendo con un
+messaggio in caso di errore (\texttt{\small 34--38}).
+
+Se (\texttt{\small 39}) il numero di byte letti \var{nread} è nullo si è in
+presenza del caso di un \textit{end-of-file}, indice che una connessione che
+si è chiusa, che deve essere trattato (\texttt{\small 39--48}) opportunamente.
+Il primo passo è chiudere (\texttt{\small 40}) anche il proprio capo del
+socket e rimuovere (\texttt{\small 41}) il file descriptor dalla tabella di
+quelli aperti, inoltre occorre verificare (\texttt{\small 42}) se il file
+descriptor chiuso è quello con il valore più alto, nel qual caso occorre
+trovare (\texttt{\small 42--46}) il nuovo massimo, altrimenti (\texttt{\small
+  47}) si può ripetere il ciclo da capo per esaminare (se ne restano)
+ulteriori file descriptor attivi.
+
+Se però è stato chiuso il file descriptor più alto, dato che la scansione dei
+file descriptor attivi viene fatta a partire dal valore più basso, questo
+significa che siamo anche arrivati alla fine della scansione, per questo
+possiamo utilizzare direttamente il valore dell'indice \var{i} con un ciclo
+all'indietro (\texttt{\small 43}) che trova il primo valore per cui la tabella
+presenta un file descriptor aperto, e lo imposta (\texttt{\small 44}) come
+nuovo massimo, per poi tornare (\texttt{\small 44}) al ciclo principale con un
+\code{break}, e rieseguire \func{select}.
+
+Se infine si sono letti dei dati (ultimo caso rimasto) si potrà invocare
+(\texttt{\small 49}) \func{FullWrite} per riscriverli indietro sul socket in
+questione, avendo cura di uscire con un messaggio in caso di errore
+(\texttt{\small 50--53}).