Pular para o conteúdo principal

Não se preocupe, apesar do título eu não voltei ao modo nerd matemático. O tópico não é teoria dos números, mas programação de soquetes e um erro de codificação que tenho visto com demasiada freqüência.

O fragmento de código na figura 1 demonstra o erro. Há um laço principal que faz um loop para sempre chamando seleto e esperando que os caracteres estejam disponíveis para serem recebidos. Uma vez selecionado indica que os caracteres estão disponíveis, o código passa por outro laço até ter recebido 10 caracteres. Após a função de recv ser chamada, o código verifica corretamente se há um retorno 0, o que indica que o par remoto fechou o soquete. Em seguida, verifica se o errno é 0 e se é concatenação dos caracteres recém recebidos em um buffer de aplicação e adiciona o número de caracteres recém recebidos ao total recebido para a mensagem atual. Finalmente, verifica se o erro é o valor EWOULDBLOCK e se é algo mais, sai do programa com um erro. Neste ponto, o laço interno se completa e se o número de caracteres na mensagem for inferior a 10, ele chama novamente o 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 - fragmento de código incorreto

A figura 2 mostra um exemplo de sessão. Os caracteres enviados para o servidor são destacados em amareloa mensagem processada que é devolvida não é destacada. Os caracteres enviados incluem um caractere de terminação de nova linha e são enviados em 1 segmento TCP. Tudo funciona quando são enviados exatamente 10 caracteres. Mas quando apenas 6 caracteres são enviados em um segmento TCP, o servidor deixa de responder.

123456789
Mensagem [123456789
processado
abcdefghi
Mensagem [abcdefghi
processado
12345abcd
Mensagem [12345abcd
processado
12345
789
abcdefghi
123456789
Figura 2 - sessão do 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.

Conexão aceita
10  0     [123456789
]
Mensagem [123456789
processado

10 0 [abcdefghi
]
Mensagem [abcdefghi
processado

10 0 [12345abcd
]
Mensagem [12345abcd
processado

6  0     [12345
]
-1  5011
4  5011     [789
]
-1  5011 
4 5011 [abcd] [abcd
4 5011 [efgh]
4  5011     [i
12]
4  5011     [3456]
4  5011     [789
]
-1  5011
-1  5011
-1  5011
Figura 3 - saída de depuração do servidor

Como o valor do erro não é 0, os caracteres recebidos não são concatenados no buffer de aplicação, de modo que o código faz loops para sempre.

Isto não é um bug no código do soquete. O API do socket declara explicitamente que o valor do errno é indefinido a menos que a função retorne um valor -1. Indefinido significa que o valor não está definido, portanto o errno retém qualquer valor que ele tinha anteriormente.

Agora você pode estar pensando que ninguém quebraria uma mensagem de 10 caracteres em 2 pedaços e você pode estar correto; mas imagine que ao invés de 10 caracteres, o comprimento da mensagem é de 100 ou 1000 caracteres. Lembre-se também que o TCP é um fluxo de bytes e não de mensagens; uma pilha TCP pode dividir uma mensagem de aplicação em múltiplos segmentos TCP sempre que quiser. Certas condições tornam isto mais provável, mensagens de aplicação mais longas, enviando outra mensagem de aplicação antes que uma anterior tenha sido transmitida, e segmentos TCP perdidos são os que vêm prontamente à mente. Sob as condições certas é possível, até mesmo provável, que este código de servidor passe todos os testes de aceitação e funcione bem em um ambiente de produção, pelo menos por algum tempo.

A boa notícia é que existe uma correção muito simples; em vez de testar para errno == 0 apenas teste para um valor de retorno maior que 0 , veja a mudança destacada na figura 4. Note também que o comentário para o teste "errno != EWOULDBLOCK" agora aponta que a única maneira de chegar a isso se a declaração for se o valor de retorno retornado for um valor negativo. O único valor negativo que ele retorna é -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 corrigido

 

© 2024 Stratus Technologies.