Passa al contenuto principale

Non preoccuparti, nonostante il titolo non sono tornato alla mia fase da secchione di matematica. L'argomento non è la teoria dei numeri, ma la programmazione dei socket e un errore di codice che ho visto fin troppo spesso.

Il frammento di codice nella figura 1 illustra l'errore. È presente un ciclo while principale che si ripete all'infinito, chiamando la funzione select e attendendo che siano disponibili caratteri da ricevere. Una volta che select indica che sono disponibili dei caratteri, il codice esegue un altro ciclo fino a quando non ha ricevuto 10 caratteri. Dopo la chiamata alla funzione recv, il codice verifica correttamente se il valore restituito è 0, il che indica che il peer remoto ha chiuso il socket. Verifica quindi se errno è 0 e, in caso affermativo, concatena i caratteri appena ricevuti in un buffer dell'applicazione e aggiunge il numero di caratteri appena ricevuti al totale ricevuto per il messaggio corrente. Infine, controlla se errno ha il valore EWOULDBLOCK e, se è diverso, termina il programma con un errore. A questo punto il ciclo interno si completa e, se il numero di caratteri nel messaggio è inferiore a 10, chiama nuovamente recv.

 while (1)
   {
   FD_ZERO (&fdsetREAD);
   FD_ZERO (&fdsetNULL);
   FD_SET (sockAccepted, &fdsetREAD);
   iNumFDS = sockAccepted + 1;

/* wait for the start of a message to arrive */
   iSelected = select (iNumFDS,
                      &fdsetREAD, &fdsetNULL, &fdsetNULL, &timevalTimeout);
   if (iSelected < 0) /* Error from select, report and abort */
      {
      perror ("minus1: error from select");
      exit (errno);
      }
/* select indicates something to be read. Since there is only 1 socket
   there is no need to figure out which socket is ready. Note that if
   select returns 0 it just means that it timed out, we will just go around
   the loop again.*/
   else if (iSelected > 0)
        {
        szAppBuffer [0] = 0x00;       /* "zero out" the application buffer */
        iTotalCharsRecv = 0;          /* zero out the total characters count */
        while (iTotalCharsRecv < 10)  /* loop until all 10 characters read */
           {                          /* now read from socket */
           iNumCharsRecv = recv (sockAccepted, szRecvBuffer,
                                   10 - iTotalCharsRecv, 0);
           if (iDebugFlag)            /* debug output show */
              {                       /* value returned from recv and errno */
              printf ("%d  %d     ", iNumCharsRecv, errno);
              if (iNumCharsRecv > 0)  /* also received characters if any */
                 {
                 szRecvBuffer [iNumCharsRecv] = 0x00;
                 printf ("[%s]n", szRecvBuffer);
                 }
              else printf ("n");
              }
           if (iNumCharsRecv == 0)   /* If 0 characters received exit app */
              {
              printf ("minus1: socket closedn");
              exit (0);
              }
           else if (errno == 0)      /* if "no error" accumulate received */
              {                      /* chars into an applictaion buffer */
              szRecvBuffer [iNumCharsRecv] = 0x00;
              strcat (szAppBuffer, szRecvBuffer);
              iTotalCharsRecv = iTotalCharsRecv + iNumCharsRecv;
              szRecvBuffer [0] = 0x00;
              }
           else if (errno != EWOULDBLOCK) /* Ignore an EWOULDBLOCK error */
              {                           /* anything else report and abort */
              perror ("minus1: Error from recv");
              exit (errno);
              }
           if (iDebugFlag) sleep (1); /* this prevents the output from */
           }                          /* scrolling off the window */

        sprintf (szOut, "Message [%s] processedn", szAppBuffer);
        if (iDebugFlag) printf ("%sn", szOut);
        if (send (sockAccepted, szOut , strlen (szOut), 0) < 0)
           perror ("minus1: error from send");
        }
   }
Figura 1 – frammento di codice errato

La figura 2 mostra un esempio di sessione. I caratteri inviati al server sono evidenziati in giallo, mentre il messaggio elaborato che viene restituito non è evidenziato. I caratteri inviati includono un carattere di nuova riga di terminazione e vengono inviati in 1 segmento TCP. Tutto funziona quando vengono inviati esattamente 10 caratteri. Tuttavia, quando vengono inviati solo 6 caratteri in un segmento TCP, il server smette di rispondere.

123456789
Messaggio [123456789\
] elaborato
abcdefghi
Messaggio [abcdefghi
] elaborato
12345abcd
Messaggio [12345abcd\
] elaborato
12345
789
abcdefghi
123456789
Figura 2 – Sessione client

Figure 3 shows the server session with debug turned on. You can see that after the “12345<new line>” characters are received the next recv returns -1 and sets the errno to 5011, which is EWOULDBLOCK. The code then loops and the next recv returns the characters “789<new line>” but the errno value is still set to 5011. In fact every recv after that regardless of whether there are characters received or not has errno set to 5011.

Connessione accettata
10  0     [123456789
]
Messaggio [123456789
] elaborato

10  0     [abcdefghi
]
Messaggio [abcdefghi
] elaborato

10  0     [12345abcd
]
Messaggio [12345abcd
] elaborato

6  0     [12345
]
-1  5011
4  5011     [789
]
-1  5011 
4  5011     [abcd]
4  5011     [efgh]
4  5011     [i
12]
4  5011     [3456]
4  5011     [789
]
-1  5011
-1  5011
-1  5011
Figura 3 – Output di debug del server

Poiché il valore di errno non è 0, i caratteri ricevuti non vengono concatenati nel buffer dell'applicazione, quindi il codice entra in un ciclo infinito.

Non si tratta di un bug nel codice dei socket. L'API dei socket specifica chiaramente che il valore di errno è indefinito a meno che la funzione non restituisca il valore -1. "Indefinito" significa che il valore non viene impostato, quindi errno mantiene il valore che aveva in precedenza.

Forse starai pensando che nessuno dividerebbe mai un messaggio di 10 caratteri in due parti, e forse hai ragione; ma immagina che, invece di 10 caratteri, la lunghezza del messaggio sia di 100 o 1000 caratteri. Ricorda inoltre che il TCP è un flusso di byte, non di messaggi; uno stack TCP può suddividere un messaggio dell’applicazione in più segmenti TCP ogni volta che lo desidera. Alcune condizioni rendono questo scenario più probabile: messaggi dell'applicazione più lunghi, l'invio di un altro messaggio dell'applicazione prima che quello precedente sia stato trasmesso e la perdita di segmenti TCP sono quelle che vengono subito in mente. Nelle giuste condizioni è possibile, e persino probabile, che questo codice del server superi tutti i test di accettazione e funzioni bene in un ambiente di produzione, almeno per un po'.

La buona notizia è che esiste una soluzione molto semplice: invece di verificare se errno == 0, basta controllare che il valore restituito sia maggiore di 0; si veda la modifica evidenziata nella figura 4. Si noti inoltre che il commento relativo al test “errno != EWOULDBLOCK” sottolinea ora che l’unico modo per raggiungere quell’istruzione if è che recv restituisca un valore negativo. L’unico valore negativo che restituisce è -1.

 while (1)
   {
   FD_ZERO (&fdsetREAD);
   FD_ZERO (&fdsetNULL);
   FD_SET (sockAccepted, &fdsetREAD);
   iNumFDS = sockAccepted + 1;

/* wait for the start of a message to arrive */
   iSelected = select (iNumFDS,
                      &fdsetREAD, &fdsetNULL, &fdsetNULL, &timevalTimeout);
   if (iSelected < 0) /* Error from select, report and abort */
      {
      perror ("minus1: error from select");
      exit (errno);
      }
/* select indicates something to be read. Since there is only 1 socket
   there is no need to figure out which socket is ready. Note that if
   select returns 0 it just means that it timed out, we will just go around
   the loop again.*/
   else if (iSelected > 0)
        {
        szAppBuffer [0] = 0x00;       /* "zero out" the application buffer */
        iTotalCharsRecv = 0;          /* zero out the total characters count */
        while (iTotalCharsRecv < 10)  /* loop until all 10 characters read */
           {                          /* now read from socket */
           iNumCharsRecv = recv (sockAccepted, szRecvBuffer,
                                   10 - iTotalCharsRecv, 0);
           if (iDebugFlag)            /* debug output show */
              {                       /* value returned from recv and errno */
              printf ("%d  %d     ", iNumCharsRecv, errno);
              if (iNumCharsRecv > 0)  /* also received characters if any */
                 {
                 szRecvBuffer [iNumCharsRecv] = 0x00;
                 printf ("[%s]n", szRecvBuffer);
                 }
              else printf ("n");
              }
           if (iNumCharsRecv == 0)   /* If 0 characters received exit app */
              {
              printf ("minus1: socket closedn");
              exit (0);
              }
           else if (iNumCharsRecv > 0) /* if no error accumulate received */
              {                        /* chars into an applictaion buffer */
              szRecvBuffer [iNumCharsRecv] = 0x00;
              strcat (szAppBuffer, szRecvBuffer);
              iTotalCharsRecv = iTotalCharsRecv + iNumCharsRecv;
              szRecvBuffer [0] = 0x00;
              }
           else if (errno != EWOULDBLOCK) /* if we get here iNumCharsRecv */
              {                           /* must be -1 so errno is defined */
              perror                      /* Ignore an EWOULDBLOCK error */
               ("minus1: Error from recv"); /* anything else report */
              exit (errno);               /* and abort */
              }
           if (iDebugFlag) sleep (1); /* this prevents the output from */
           }                          /* scrolling off the window */

        sprintf (szOut, "Message [%s] processedn", szAppBuffer);
        if (iDebugFlag) printf ("%sn", szOut);
        if (send (sockAccepted, szOut , strlen (szOut), 0) < 0)
           perror ("minus1: error from send");
        }
   }
Figura 4 – Frammento di codice corretto