Ir al contenido principal

No te preocupes, a pesar del título no he vuelto al modo nerd de las matemáticas. El tema no es la teoría de los números sino la programación de los sockets y un error de codificación que he visto demasiado a menudo.

El fragmento de código de la figura 1 demuestra el error. Hay un bucle principal de while que hace un bucle siempre llamando a selectores y esperando que los caracteres estén disponibles para ser recibidos. Una vez que "select" indica que los caracteres están disponibles, el código pasa por otro bucle hasta que ha recibido 10 caracteres. Después de que se llama la función de recuperación, el código comprueba correctamente si hay un retorno 0, lo que indica que el par remoto ha cerrado el enchufe. Luego comprueba si el errno es 0 y si lo es concatena los caracteres que acaba de recibir en un buffer de la aplicación y añade el número de caracteres que acaba de recibir al total recibido para el mensaje actual. Finalmente, comprueba si errno tiene el valor EWOULDBLOCK y si es otra cosa, sale del programa con un error. En este punto el bucle interno se completa y si el número de caracteres del mensaje es menor de 10 llama a recv de nuevo.

 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 - fragmento de código incorrecto

La figura 2 muestra una sesión de ejemplo. Los caracteres enviados al servidor se resaltan en amarilloel mensaje procesado que se devuelve no está resaltado. Los caracteres enviados incluyen un carácter de nueva línea de terminación y se envían en 1 segmento TCP. Todo funciona cuando se envían exactamente 10 caracteres. Pero cuando sólo se envían 6 caracteres en un segmento TCP el servidor deja de responder.

123456789
Mensaje [123456789
...procesado...
abcdefghi
Mensaje [abcdefghi
...procesado...
12345abcd
Mensaje [12345abcd
...procesado...
12345
789
abcdefghi
123456789
Figura 2 - sesión de cliente

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.

Conexión aceptada
10  0     [123456789
]
Mensaje [123456789
...procesado...

10 0 [abcdefghi
]
Mensaje [abcdefghi
...procesado...

10 0 [12345abcd
]
Mensaje [12345abcd
...procesado...

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 - salida de depuración del servidor

Debido a que el valor de errno no es 0, los caracteres recibidos no se concatenan en el buffer de la aplicación, por lo que el código se repite para siempre.

Esto no es un error en el código del enchufe. La API del socket declara explícitamente que el valor de errno está indefinido a menos que la función devuelva un valor de -1. Indefinido significa que el valor no está establecido, por lo que errno conserva el valor que tenía anteriormente.

Ahora puede que pienses que nadie rompería un mensaje de 10 caracteres en 2 partes y puede que estés en lo cierto; pero imagina que en lugar de 10 caracteres la longitud del mensaje es de 100 o 1000 caracteres. Recuerda también que TCP es un flujo de bytes, no de mensajes; una pila TCP puede dividir un mensaje de la aplicación en varios segmentos TCP cuando quiera. Ciertas condiciones hacen que esto sea más probable, mensajes de aplicación más largos, enviando otro mensaje de aplicación antes de que se haya transmitido uno anterior, y los segmentos TCP perdidos son los que vienen fácilmente a la mente. En las condiciones adecuadas es posible, incluso probable, que este código de servidor pase todas sus pruebas de aceptación y funcione bien en un entorno de producción, al menos durante un tiempo.

La buena noticia es que hay una solución muy simple; en lugar de probar para errno == 0 sólo prueba para un valor de retorno mayor que 0 , ver el cambio resaltado en la figura 4. Note también que el comentario para la prueba de "errno != EWOULDBLOCK" ahora señala que la única manera de llegar a eso si la declaración es si recv devolvió un valor negativo. El único valor negativo que devuelve es -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 - fragmento de código corregido