Skip to main content

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 montre l'erreur. Il y a une boucle principale en attente qui appelle sans cesse en sélectionnant et en attendant que les 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 un retour à 0 qui indique que le pair distant a fermé la prise. Il vérifie ensuite si errno est 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 en cours. Enfin, il vérifie si errno a la valeur EWOULDBLOCK et s'il s'agit d'autre chose, il quitte le programme avec une erreur. À ce stade, la boucle interne se termine et si le nombre de caractères du 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 dans jaunele message traité qui est renvoyé n'est pas mis en évidence. Les caractères envoyés comprennent un nouveau caractère de fin de ligne et sont envoyés dans 1 segment TCP. Tout fonctionne lorsque 10 caractères exactement sont envoyés. Mais lorsque seulement 6 caractères sont envoyés dans un segment TCP, le serveur cesse de répondre.

123456789
Message [123456789
] traitées
abcdefghi
Message [abcdefghi
] traités
12345abcd
Message [12345abcd
] traitées
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és

10 0 [abcdefghi
]
Message [abcdefghi
] traités

10 0 [12345abcd
]
Message [12345abcd
] traités

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 de errno n'est pas 0, les caractères reçus ne sont pas concaténés dans la mémoire tampon de l'application, de sorte que le code est bouclé pour toujours.

Ce n'est pas un bogue dans le code de la socket. L'API de la socket indique explicitement que la valeur de errno est indéfinie à moins que la fonction ne renvoie une valeur de -1. Indéfini 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 peut diviser un message de 10 caractères en deux parties et vous avez peut-être raison ; mais imaginez qu'au lieu de 10 caractères, le message ait une longueur de 100 ou 1000 caractères. Rappelez-vous également 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 cette situation plus probable : des messages d'application plus longs, l'envoi d'un autre message d'application avant qu'un précédent n'ait été transmis, et des segments TCP perdus sont ceux qui viennent immédiatement à l'esprit. Dans de 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 est qu'il existe une solution très simple ; au lieu de tester pour errno == 0, il suffit de tester pour une valeur de retour supérieure à 0, voir le changement mis en évidence dans la figure 4. Notez également que le commentaire pour le test "errno != EWOULDBLOCK" indique maintenant que la seule façon d'atteindre l'énoncé 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é

 

2024 Stratus Technologies.