From: Simone Piccardi Date: Thu, 25 Dec 2003 22:16:06 +0000 (+0000) Subject: Inserito esempio di server echo basato su select. X-Git-Url: https://gapil.gnulinux.it/gitweb/?p=gapil.git;a=commitdiff_plain;h=38b9e1e53adc5b440459a6044f3a8ff133f34824 Inserito esempio di server echo basato su select. --- diff --git a/sources/Makefile b/sources/Makefile index 151d779..b669397 100644 --- a/sources/Makefile +++ b/sources/Makefile @@ -3,7 +3,7 @@ # # C flags CC=gcc -CFLAGS= -Wall -g -DDEBUG -fPIC +CFLAGS= -Wall -g -fPIC #-DDEBUG CFLAGJ= -L./ -lgapil LIB = libgapil.so diff --git a/sources/select_echod.c b/sources/select_echod.c index b1b6483..3a0690b 100644 --- a/sources/select_echod.c +++ b/sources/select_echod.c @@ -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 /* error strings */ #include +#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); diff --git a/tcpsockadv.tex b/tcpsockadv.tex index d2e58fe..7351738 100644 --- a/tcpsockadv.tex +++ b/tcpsockadv.tex @@ -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}).