Passer au contenu principal

Ne vous inquiétez pas, malgré le titre, je ne suis pas revenu au mode "math nerd". Le sujet n'est pas la théorie des nombres mais la programmation des sockets et une erreur de codage que j'ai trop souvent vue.

Le fragment de code de la figure 1 illustre l'erreur. Il existe une boucle while principale qui s'exécute en boucle indéfiniment, appelant select et attendant que des caractères soient disponibles pour être reçus. Une fois que select indique que des caractères sont disponibles, le code passe par une autre boucle jusqu'à ce qu'il ait reçu 10 caractères. Après l'appel de la fonction recv, le code vérifie correctement le retour 0 qui indique que le pair distant a fermé le socket. Il vérifie ensuite si errno est égal à 0 et, si c'est le cas, il concatène les caractères qui viennent d'être reçus dans un tampon d'application et ajoute le nombre de caractères qui viennent d'être reçus au total reçu pour le message actuel. Enfin, il vérifie si errno a la valeur EWOULDBLOCK et, si ce n'est pas le cas, il quitte le programme avec une erreur. À ce stade, la boucle interne est terminée et si le nombre de caractères dans le message est inférieur à 10, il appelle à nouveau 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");
        }
   }
Figure 1 – Fragment de code incorrect

La figure 2 montre un exemple de session. Les caractères envoyés au serveur sont mis en évidence en jaune, le message traité qui est renvoyé n'est pas mis en évidence. Les caractères envoyés comprennent un caractère de fin de ligne et sont envoyés dans un segment TCP. Tout fonctionne lorsque 10 caractères exactement sont envoyés. Mais lorsque seuls 6 caractères sont envoyés dans un segment TCP, le serveur cesse de répondre.

123456789
Message [123456789
] traité
abcdefghi
Message [abcdefghi
] traité
12345abcd
Message [12345abcd
] traité
12345
789
abcdefghi
123456789
Figure 2 – Session 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.

Connexion acceptée
10  0     [123456789
]
Message [123456789
] traité

10  0     [abcdefghi
]
Message [abcdefghi
] traité

10  0     [12345abcd
]
Message [12345abcd  
] traité  
  
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
Figure 3 – Sortie de débogage du serveur

Comme la valeur errno n'est pas 0, les caractères reçus ne sont pas concaténés dans le tampon de l'application, ce qui fait que le code tourne en boucle indéfiniment.

Il ne s'agit pas d'un bug dans le code du socket. L'API du socket indique explicitement que la valeur de errno est indéfinie, sauf si la fonction renvoie une valeur de -1. Indéfinie signifie que la valeur n'est pas définie, donc errno conserve la valeur qu'elle avait précédemment.

Vous pensez peut-être que personne ne diviserait un message de 10 caractères en deux parties, et vous avez peut-être raison ; mais imaginez qu'au lieu de 10 caractères, la longueur du message soit de 100 ou 1 000 caractères. N'oubliez pas non plus que le TCP est un flux d'octets et non de messages ; une pile TCP peut diviser un message d'application en plusieurs segments TCP quand elle le souhaite. Certaines conditions rendent cela plus probable, notamment les messages d'application plus longs, l'envoi d'un autre message d'application avant que le précédent n'ait été transmis, et la perte de segments TCP. Dans les bonnes conditions, il est possible, voire probable, que ce code serveur passe tous ses tests d'acceptation et fonctionne correctement dans un environnement de production, du moins pendant un certain temps.

La bonne nouvelle, c'est qu'il existe une solution très simple : au lieu de tester errno == 0, il suffit de tester une valeur de retour supérieure à 0, comme le montre la modification mise en évidence dans la figure 4. Notez également que le commentaire relatif au test « errno != EWOULDBLOCK » indique désormais que la seule façon d'atteindre cette instruction if est que recv renvoie une valeur négative. La seule valeur négative qu'il renvoie est -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");
        }
   }
Figure 4 – Fragment de code corrigé