From: Simone Piccardi Date: Mon, 20 Oct 2003 22:44:16 +0000 (+0000) Subject: Versione finale del client ECHO su TCP, con esempio di uso della funzione X-Git-Url: https://gapil.gnulinux.it/gitweb/?p=gapil.git;a=commitdiff_plain;h=d25090faca15102552d77c38161a8a34b0bac41e Versione finale del client ECHO su TCP, con esempio di uso della funzione shutdown. Revisionati i nomi delle relative figure --- diff --git a/listati/ClientEcho.c b/listati/ClientEcho.c index 9a18289..520671f 100644 --- a/listati/ClientEcho.c +++ b/listati/ClientEcho.c @@ -1,12 +1,51 @@ void ClientEcho(FILE * filein, int socket) { char sendbuff[MAXLINE+1], recvbuff[MAXLINE+1]; - int nread; - while (fgets(sendbuff, MAXLINE, filein) != NULL) { - FullWrite(socket, sendbuff, strlen(sendbuff)); - nread = read(socket, recvbuff, strlen(sendbuff)); - recvbuff[nread] = 0; - fputs(recvbuff, stdout); + int nread, nwrite; + int maxfd; + fd_set fset; + int eof = 0; + /* initialize file descriptor set */ + FD_ZERO(&fset); + maxfd = max(fileno(filein), socket) + 1; + while (1) { + FD_SET(socket, &fset); /* set for the socket */ + if (eof == 0) { + FD_SET(fileno(filein), &fset); /* set for the standard input */ + } + select(maxfd, &fset, NULL, NULL, NULL); /* wait for read ready */ + if (FD_ISSET(fileno(filein), &fset)) { /* if ready on stdin */ + if (fgets(sendbuff, MAXLINE, filein) == NULL) { /* if no input */ + eof = 1; /* EOF on input */ + shutdown(socket, SHUT_WR); /* close write half */ + FD_CLR(fileno(filein), &fset); /* no more interest on stdin */ + } else { /* else we have to write to socket */ + nwrite = FullWrite(socket, sendbuff, strlen(sendbuff)); + if (nwrite < 0) { /* on error stop */ + printf("Errore in scrittura: %s", strerror(errno)); + return; + } + } + } + if (FD_ISSET(socket, &fset)) { /* if ready on socket */ + nread = read(socket, recvbuff, strlen(sendbuff)); /* do read */ + if (nread < 0) { /* error condition, stop client */ + printf("Errore in lettura: %s\n", strerror(errno)); + return; + } + if (nread == 0) { /* server closed connection, stop */ + if (eof == 1) { + return; + } else { + printf("EOF prematuro sul socket\n"); + return; + } + } + recvbuff[nread] = 0; /* else read is ok, write on stdout */ + if (fputs(recvbuff, stdout) == EOF) { + perror("Errore in scrittura su terminale"); + return; + } + } } - return; } diff --git a/listati/ClientEcho_first.c b/listati/ClientEcho_first.c new file mode 100644 index 0000000..9a18289 --- /dev/null +++ b/listati/ClientEcho_first.c @@ -0,0 +1,12 @@ +void ClientEcho(FILE * filein, int socket) +{ + char sendbuff[MAXLINE+1], recvbuff[MAXLINE+1]; + int nread; + while (fgets(sendbuff, MAXLINE, filein) != NULL) { + FullWrite(socket, sendbuff, strlen(sendbuff)); + nread = read(socket, recvbuff, strlen(sendbuff)); + recvbuff[nread] = 0; + fputs(recvbuff, stdout); + } + return; +} diff --git a/sources/TCP_echo.c b/sources/TCP_echo.c index 57d6184..691911e 100644 --- a/sources/TCP_echo.c +++ b/sources/TCP_echo.c @@ -26,7 +26,7 @@ * * Usage: echo -h give all info's * - * $Id: TCP_echo.c,v 1.10 2003/10/19 10:38:27 piccardi Exp $ + * $Id: TCP_echo.c,v 1.11 2003/10/20 22:44:16 piccardi Exp $ * ****************************************************************/ /* @@ -142,16 +142,21 @@ void ClientEcho(FILE * filein, int socket) int nread, nwrite; int maxfd; fd_set fset; + int eof = 0; /* initialize file descriptor set */ FD_ZERO(&fset); maxfd = max(fileno(filein), socket) + 1; while (1) { FD_SET(socket, &fset); /* set for the socket */ - FD_SET(fileno(filein), &fset); /* set for the standard input */ + if (eof == 0) { + FD_SET(fileno(filein), &fset); /* set for the standard input */ + } select(maxfd, &fset, NULL, NULL, NULL); /* wait for read ready */ if (FD_ISSET(fileno(filein), &fset)) { /* if ready on stdin */ if (fgets(sendbuff, MAXLINE, filein) == NULL) { /* if no input */ - return; /* we stopped client */ + eof = 1; /* EOF on input */ + shutdown(socket, SHUT_WR); /* close write half */ + FD_CLR(fileno(filein), &fset); /* no more interest on stdin */ } else { /* else we have to write to socket */ nwrite = FullWrite(socket, sendbuff, strlen(sendbuff)); if (nwrite < 0) { /* on error stop */ @@ -167,8 +172,12 @@ void ClientEcho(FILE * filein, int socket) return; } if (nread == 0) { /* server closed connection, stop */ - printf("EOF sul socket\n"); - return; + if (eof == 1) { + return; + } else { + printf("EOF prematuro sul socket\n"); + return; + } } recvbuff[nread] = 0; /* else read is ok, write on stdout */ if (fputs(recvbuff, stdout) == EOF) { diff --git a/sources/TCP_echo_third.c b/sources/TCP_echo_third.c new file mode 100644 index 0000000..680e0e8 --- /dev/null +++ b/sources/TCP_echo_third.c @@ -0,0 +1,180 @@ +/* TCP_echo.c + * + * Copyright (C) 2001-2003 Simone Piccardi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ +/**************************************************************** + * + * Program TCP_echo.c + * Simple TCP client for echo service (port 7) + * + * Author: Simone Piccardi + * Jun. 2001 + * + * Usage: echo -h give all info's + * + * $Id: TCP_echo_third.c,v 1.1 2003/10/20 22:44:16 piccardi Exp $ + * + ****************************************************************/ +/* + * Include needed headers + */ +#include /* predefined types */ +#include /* include unix standard library */ +#include /* IP addresses conversion utiliites */ +#include /* socket library */ +#include /* include standard I/O library */ +#include /* include error codes */ +#include /* include erroro strings definitions */ + +#include "macros.h" + +#define MAXLINE 256 +void usage(void); +void ClientEcho(FILE * filein, int socket); +void SigTERM_hand(int sig); + +/* Program begin */ +int main(int argc, char *argv[]) +{ +/* + * Variables definition + */ + int sock, i; + int reset = 0; + struct sockaddr_in serv_add; + struct linger ling; + /* + * Input section: decode parameters passed in the calling + * Use getopt function + */ + opterr = 0; /* don't want writing to stderr */ + while ( (i = getopt(argc, argv, "hr")) != -1) { + switch (i) { + /* + * Handling options + */ + case 'h': + printf("Wrong -h option use\n"); + usage(); + return(1); + break; + case 'r': + reset = 1; + break; + case '?': /* unrecognized options */ + printf("Unrecognized options -%c\n",optopt); + usage(); + default: /* should not reached */ + usage(); + } + } + /* *********************************************************** + * + * Options processing completed + * + * Main code beginning + * + * ***********************************************************/ + /* create socket */ + if ( (sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) { + perror("Socket creation error"); + return 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 */ + /* build address using inet_pton */ + if ( (inet_pton(AF_INET, argv[optind], &serv_add.sin_addr)) <= 0) { + perror("Address creation error"); + return 1; + } + /* extablish connection */ + if (connect(sock, (struct sockaddr *)&serv_add, sizeof(serv_add)) < 0) { + perror("Connection error"); + return 1; + } + /* check if resetting on close is required */ + if (reset) { + printf("Setting reset on close \n"); + ling.l_onoff = 1; + ling.l_linger = 0; + if (setsockopt(sock, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling))) { + perror("Cannot set linger"); + exit(1); + } + } + /* do read/write operations */ + ClientEcho(stdin, sock); + /* normal exit */ + return 0; +} +/* + * routine to print usage info and exit + */ +void usage(void) { + printf("Take daytime from a remote host \n"); + printf("Usage:\n"); + printf(" daytime [-h] [-v] [host in dotted decimal form] \n"); +// printf(" -v set verbosity on\n"); + printf(" -r require reset on closing\n"); + printf(" -h print this help\n"); + exit(1); +} + +void ClientEcho(FILE * filein, int socket) +{ + char sendbuff[MAXLINE+1], recvbuff[MAXLINE+1]; + int nread, nwrite; + int maxfd; + fd_set fset; + /* initialize file descriptor set */ + FD_ZERO(&fset); + maxfd = max(fileno(filein), socket) + 1; + while (1) { + FD_SET(socket, &fset); /* set for the socket */ + FD_SET(fileno(filein), &fset); /* set for the standard input */ + select(maxfd, &fset, NULL, NULL, NULL); /* wait for read ready */ + if (FD_ISSET(fileno(filein), &fset)) { /* if ready on stdin */ + if (fgets(sendbuff, MAXLINE, filein) == NULL) { /* if no input */ + return; /* we stopped client */ + } else { /* else we have to write to socket */ + nwrite = FullWrite(socket, sendbuff, strlen(sendbuff)); + if (nwrite < 0) { /* on error stop */ + printf("Errore in scrittura: %s", strerror(errno)); + return; + } + } + } + if (FD_ISSET(socket, &fset)) { /* if ready on socket */ + nread = read(socket, recvbuff, strlen(sendbuff)); /* do read */ + if (nread < 0) { /* error condition, stop client */ + printf("Errore in lettura: %s\n", strerror(errno)); + return; + } + if (nread == 0) { /* server closed connection, stop */ + printf("EOF sul socket\n"); + return; + } + recvbuff[nread] = 0; /* else read is ok, write on stdout */ + if (fputs(recvbuff, stdout) == EOF) { + perror("Errore in scrittura su terminale"); + return; + } + } + } +} diff --git a/tcpsock.tex b/tcpsock.tex index 8b2be4f..2ce01cd 100644 --- a/tcpsock.tex +++ b/tcpsock.tex @@ -1694,7 +1694,7 @@ scriverli su \file{stdout}. \begin{figure}[!htb] \footnotesize \centering \begin{minipage}[c]{15.6cm} - \includecodesample{listati/ClientEcho.c} + \includecodesample{listati/ClientEcho_first.c} \end{minipage} \normalsize \caption{Codice della prima versione della funzione \texttt{ClientEcho} per @@ -2082,8 +2082,8 @@ riscrittura parziale del server, la nuova versione di questo, in cui si sono introdotte una serie di nuove opzioni che ci saranno utili per il debug, è mostrata in \figref{fig:TCP_echo_server_code_second}, dove si sono riportate la sezioni di codice modificate nella seconda versione del programma, il -sorgente completo di quest'ultimo si trova nel file -\file{TCP\_echod\_second.c} dei sorgenti allegati alla guida. +codice completo di quest'ultimo si trova nel file \file{TCP\_echod\_second.c} +dei sorgenti allegati alla guida. La prima modifica effettuata è stata quella di introdurre una nuova opzione a riga di comando, \texttt{-c}, che permette di richiedere il comportamento diff --git a/tcpsockadv.tex b/tcpsockadv.tex index e77e432..d22c6aa 100644 --- a/tcpsockadv.tex +++ b/tcpsockadv.tex @@ -182,7 +182,9 @@ Riprendiamo allora il codice del client, modificandolo per l'uso di di \figref{fig:TCP_ClientEcho_second}, dato che tutto il resto, che riguarda le modalità in cui viene stabilita la connessione con il server, resta assolutamente identico. La nostra nuova versione di \func{ClientEcho}, la -terza della serie, è riportata in \figref{fig:TCP_ClientEcho_third}. +terza della serie, è riportata in \figref{fig:TCP_ClientEcho_third}, il codice +completo si trova nel file \file{TCP\_echo\_third.c} dei sorgenti allegati alla +guida. In questo caso la funzione comincia (\texttt{\small 8--9}) con l'azzeramento del file descriptor set \var{fset} e l'impostazione del valore \var{maxfd}, da @@ -299,8 +301,6 @@ del client sar \func{select} per la ricezione di un errore di \errcode{ECONNRESET}. - - \subsection{La funzione \func{shutdown}} \label{sec:TCP_shutdown} @@ -322,7 +322,180 @@ Questa Il problema che si pone è che se la chiusura del socket è effettuata con la funzione \func{close}, come spiegato in \secref{sec:TCP_func_close}, si perde ogni possibilità di poter rileggere quanto l'altro capo può continuare a -scrivere. Per poter permettere allora +scrivere. Per poter permettere allora di segnalare che si è concluso con la +scrittura, continuando al contempo a leggere quanto può provenire dall'altro +capo del socket si può allora usare la funzione \funcd{shutdown}, il cui +prototipo è: +\begin{prototype}{sys/socket.h} +{int shutdown(int sockfd, int how)} + +Chiude un lato della connessione fra due socket. + + \bodydesc{La funzione restituisce zero in caso di successo e -1 per un + errore, nel qual caso \var{errno} assumerà i valori: + \begin{errlist} + \item[\errcode{ENOTSOCK}] il file descriptor non corrisponde a un socket. + \item[\errcode{ENOTCONN}] il socket non è connesso. + \end{errlist} + ed inoltre \errval{EBADF}.} +\end{prototype} + +La funzione prende come primo argomento il socket \param{sockfd} su cui si +vuole operare e come secondo argomento un valore intero \param{how} che indica +la modalità di chiusura del socket, quest'ultima può prendere soltanto tre +valori: +\begin{basedescript}{\desclabelwidth{2.2cm}\desclabelstyle{\nextlinelabel}} +\item[\macro{SHUT\_RD}] chiude il lato in lettura del socket, non sarà più + possibile leggere dati da esso, tutti gli eventuali dati trasmessi + dall'altro capo del socket saranno automaticamente scartati dal kernel, che, + in caso di socket TCP, provvederà comunque ad inviare i relativi segmenti di + ACK. +\item[\macro{SHUT\_WR}] chiude il lato in scrittura del socket, non sarà più + possibile scrivere dati su di esso. Nel caso di socket TCP la chiamata causa + l'emissione di un segmento FIN, secondo la procedura chiamata + \textit{half-close}. Tutti i dati presenti nel buffer di scrittura prima + della chiamata saranno inviati, seguiti dalla sequenza di chiusura + illustrata in \secref{sec:TCP_conn_term}. +\item[\macro{SHUT\_RDWR}] chiude sia il lato in lettura che quello in + scrittura del socket. È equivalente alla chiamata in sequenza con + \macro{SHUT\_RD} e \macro{SHUT\_WR}. +\end{basedescript} + +Ci si può chiedere quale sia l'utilità di avere introdotto \macro{SHUT\_RDWR} +quando questa sembra rendere \funcd{shutdown} del tutto equivalente ad una +\func{close}. In realtà non è così, esiste infatti un'altra differenza con +\func{close}, più sottile. Finora infatti non ci siamo presi la briga di +sottolineare in maniera esplicita che come per i file e le fifo, anche per i +socket possono esserci più riferimenti contemporanei ad uno stesso socket. Per +cui si avrebbe potuto avere l'impressione che sia una corrispondenza univoca +fra un socket ed il file descriptor con cui vi si accede. Questo non è +assolutamente vero, (e lo abbiamo già visto nel codice del server di +\figref{fig:TCP_echo_server_first_code}), ed è invece assolutamente normale +che, come per gli altri oggetti, ci possano essere più file descriptor che +fanno riferimento allo stesso socket. + +Allora se avviene uno di questi casi quello che succederà è che la chiamata a +\func{close} darà effettivamente avvio alla sequenza di chiusura di un socket +soltanto quando il numero di riferimenti a quest'ultimo diventerà nullo. +Fintanto che ci sono file descriptor che fanno riferimento ad un socket +\func{close} si limiterà a deallocare nel processo corrente il file descriptor +utilizzato, ma il socket resterà pienamente accessibile attraverso gli altri +riferimenti.Se torniamo all'esempio di \figref{fig:TCP_echo_server_first_code} +abbiamo infatti che le due \func{close} (sul socket connesso nel padre e sul +socket in ascolto nel figlio), restando comunque altri riferimenti attivi (al +socket connesso nel figlio e a quello in ascolto nel padre) non effettuano +nessuna chiusura effettiva. + +Questo non avviene affatto se si usa \func{shutdown} al posto di \func{close}, +in questo caso infatti la chiusura del socket viene effettuata immediatamente, +indipendentemente dalla presenza di altri riferimenti attivi, e pertanto sarà +ovviamente efficace anche per tutti gli altri file descriptor con cui si fa +riferimento allo stesso socket. + +Il caso più comune di uso di \func{shutdown} è comunque quello della chiusura +del lato in scrittura, per segnalare all'altro capo della connessione che si è +concluso l'invio dei dati, restando comunque in grado di ricevere quanto +ancora questi potrà inviarci. Questo è ad esempio l'uso che ci serve per +rendere finalmente completo il nostro esempio sul servizio echo. Il nostro +client infatti presenta ancora un problema, che nell'uso che finora ne abbiamo +fatto non è emerso, ma che ci aspetta dietro l'angolo non appena usciamo +dall'uso interattivo e proviamo ad eseguirlo redirigendo standard input e +standard output. Così se eseguiamo: +\begin{verbatim} +[piccardi@gont sources]$ ./echo 192.168.1.1 < ../fileadv.tex > copia +\end{verbatim}%$ +vedremo che il file \texttt{copia} risulta mancare della parte finale. + +Per capire cosa avviene in questo caso occorre tenere presente come avviene la +comunicazione via rete; quando redirigiamo lo standard input il nostro client +inizierà a leggere il contenuto del file \texttt{../fileadv.tex} a blocchi di +dimensione massima pari a \texttt{MAXLINE} per poi scriverlo, alla massima +velocità consentitagli dalla rete, sul socket. Dato che la connessione è con +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, detto RTT (da \textit{Round Trip Time} può essere +stimato con l'uso del comando \cmd{ping}. Ma mantre il pacchetti sono in +transito sulla rete il client continua a leggere e a scrivere fintanto che il +file in ingresso finisce. + +A questo punto, se torniamo al codice mostrato in +\figref{fig:TCP_ClientEcho_third}, notiamo che non appena viene ricevuto un +end-of-file in ingresso il nostro client termina. Nel caso interattivo, in cui +si inviavano brevi stringe una alla volta, c'era sempre il tempo di eseguire +la lettura completa di quanto il server rimandava indietro. In questo caso +però quando il client termina, essendo la comunicazione a piena velocità, ci +saranno ancora pacchetti in transito sulla rete, 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 occorre, invece di uscire, usare +\func{shutdown} per effettuare la chiusura del socket in scrittura 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 fino a quando quest'ultimo, +riconosciuta la chiusura del socket in scrittura da parte del client, +effettuerà la chiusura dello stesso a sua volta. Solo alla ricezione della +chiusura del socket da parte del server, si potrà essere sicuri della +ricezione di tutti i dati prima della terminazione della connessione. + +\begin{figure}[!htb] + \footnotesize \centering + \begin{minipage}[c]{15.6cm} + \includecodesample{listati/ClientEcho.c} + \end{minipage} + \normalsize + \caption{La sezione nel codice della versione finale della funzione + \func{ClientEcho}, che usa \func{shutdown} per una conclusione corretta + della connessione.} + \label{fig:TCP_ClientEcho} +\end{figure} + +Si è allora riportato in \figref{fig:TCP_ClientEcho} la versione finale della +nostra funzione \func{ClientEcho}, in grado di gestire correttamente l'intero +flusso di dati fra client e server. Il codice completo del client, +comprendente la gestione delle opzioni a riga di comando e le istruzioni per +la creazione della connessione, si trova nel file \file{TCP\_echo.c}, +distribuito coi sorgenti allegati alla guida. + +La nuova versione è molto simile alla precedente di +\figref{fig:TCP_ClientEcho_third}; la prima differenza è l'introduzione +(\texttt{\small 7}) della variabile \var{eof}, inizializzata ad un valore +nullo, che serve a mantenere traccia dell'avvenuta conclusione della lettura +del file in ingresso. + +La seconda modifica (\texttt{\small 12--15}) è stata quella di rendere +subordinato ad un valore nullo di \var{eof} l'impostazione del file descriptor +set per l'osservazione dello standard input. Se infatti il valore di \var{eof} +è non nullo significa che si è già raggiunta la fine del file in ingresso ed è +pertanto inutile continuare a tenere sotto controllo lo standard input nella +successiva (\texttt{\small 16}) chiamata a \func{select}. + +Le maggiori modifiche rispetto alla precedente versione sono invece nella +gestione (\texttt{\small 18--22}) del caso in cui la lettura con \func{fgets} +restitisca un valore nullo, indice della fine del file, che prima causava +l'immediato ritorno della funzione. In questo caso prima (\texttt{\small 19}) +si imposta opportunamente \var{eof} ad un valore non nullo, dopo di che +(\texttt{\small 20}) si effettua la chiusura del lato in scrittura del socket +con \func{shutdown}. Infine (\texttt{\small 21}) si usa la macro +\macro{FD\_CLR} per togliere lo standard input dal file descriptor set. + +In questo modo anche se la lettura del file in ingresso è conclusa, la +funzione non esce dal ciclo principale (\texttt{\small 11--50}), ma continua +ad eseguirlo ripetendo la chiamata a \func{select} per tenere sotto controllo +soltanto il socket connesso, dal quale possono arrivare altri dati, che +saranno letti (\texttt{\small 31}), ed opportunamente trascritti +(\texttt{\small 44--48}) sullo standard input. + +Il ritorno della funzione, e la conseguente terminazione normale del client, +viene invece adesso gestito all'interno (\texttt{\small 30--49}) della lettura +dei dati dal socket; se infatti dalla lettura del socket si riceve una +condizione di end-of-file, la si tratterà (\texttt{\small 36--43}) in maniera +diversa a seconda del valore di \var{eof}. Se infatti questa è diversa da zero +(\texttt{\small 37--39}), essendo stata completata la lettura del file in +ingresso, vorrà dire che anche il server ha concluso la trasmissione dei dati +restanti, e si potrà uscire senza errori, altrimenti si stamperà +(\texttt{\small 40--42}) un messaggio di errore per la chiusura precoce della +connesione.