Zum Hauptinhalt springen

Keine Sorge, trotz des Titels bin ich nicht in den Mathe-Nerd-Modus zurückgefallen. Das Thema ist nicht Zahlentheorie, sondern Socket-Programmierung und ein Programmierfehler, den ich viel zu oft gesehen habe.

Das Codefragment in Abbildung 1 veranschaulicht den Fehler. Es gibt eine Haupt-While-Schleife, die in einer Endlosschleife select aufruft und darauf wartet, dass Zeichen für den Empfang verfügbar sind. Sobald select anzeigt, dass Zeichen verfügbar sind, durchläuft der Code eine weitere Schleife, bis er 10 Zeichen empfangen hat. Nach dem Aufruf der recv-Funktion prüft der Code korrekt, ob eine 0 zurückkommt, was bedeutet, dass die Gegenstelle den Socket geschlossen hat. Dann wird geprüft, ob errno 0 ist. Ist dies der Fall, werden die soeben empfangenen Zeichen in einem Anwendungspuffer verkettet und die Anzahl der soeben empfangenen Zeichen zur Gesamtzahl der empfangenen Zeichen für die aktuelle Nachricht addiert. Schließlich wird errno auf den Wert EWOULDBLOCK geprüft, und wenn er einen anderen Wert hat, wird das Programm mit einem Fehler beendet. An diesem Punkt wird die innere Schleife beendet, und wenn die Anzahl der Zeichen in der Nachricht weniger als 10 beträgt, wird recv erneut aufgerufen.

 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");
        }
   }
Abbildung 1 - Fragment eines fehlerhaften Codes

Abbildung 2 zeigt eine Beispielsitzung. Die an den Server gesendeten Zeichen sind in gelbhervorgehoben, die verarbeitete Nachricht, die zurückgesendet wird, ist nicht hervorgehoben. Die gesendeten Zeichen enthalten ein abschließendes Zeilenumbruchzeichen und werden in einem TCP-Segment gesendet. Alles funktioniert, wenn genau 10 Zeichen gesendet werden. Wenn jedoch nur 6 Zeichen in einem TCP-Segment gesendet werden, antwortet der Server nicht mehr.

123456789
Nachricht [123456789
Verarbeitet
abcdefghi
Nachricht [abcdefghi
] bearbeitet
12345abcd
Nachricht [12345abcd
] bearbeitet
12345
789
abcdefghi
123456789
Abbildung 2 - Kundensitzung

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.

Verbindung akzeptiert
10  0     [123456789
]
Nachricht [123456789
] verarbeitet

10 0 [abcdefghi
]
Nachricht [abcdefghi
] bearbeitet

10 0 [12345abcd
]
Nachricht [12345abcd
] bearbeitet

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
Abbildung 3 - Server-Debug-Ausgabe

Da der errno-Wert nicht 0 ist, werden die empfangenen Zeichen nicht in den Anwendungspuffer verkettet, so dass der Code eine Endlosschleife durchläuft.

Es handelt sich nicht um einen Fehler im Socket-Code. Die Socket-API besagt ausdrücklich, dass der Wert von errno undefiniert ist, es sei denn, die Funktion gibt den Wert -1 zurück. Undefiniert bedeutet, dass der Wert nicht gesetzt ist, also behält errno den Wert, den es vorher hatte.

Jetzt denken Sie vielleicht, dass niemand eine 10-Zeichen-Nachricht in zwei Teile aufteilen würde, und damit haben Sie vielleicht recht; aber stellen Sie sich vor, dass die Nachricht statt 10 Zeichen 100 oder 1000 Zeichen lang ist. Denken Sie auch daran, dass TCP ein Strom von Bytes und nicht von Nachrichten ist; ein TCP-Stack kann eine Anwendungsnachricht in mehrere TCP-Segmente aufteilen, wann immer er will. Bestimmte Bedingungen machen dies wahrscheinlicher: längere Anwendungsnachrichten, das Senden einer weiteren Anwendungsnachricht, bevor eine vorherige Nachricht übertragen wurde, und verlorene TCP-Segmente sind die, die mir sofort einfallen. Unter den richtigen Bedingungen ist es möglich, ja sogar wahrscheinlich, dass dieser Servercode alle Akzeptanztests besteht und in einer Produktionsumgebung gut läuft, zumindest eine Zeit lang.

Die gute Nachricht ist, dass es eine sehr einfache Lösung gibt: Anstatt auf errno == 0 zu testen, wird einfach auf einen Rückgabewert größer als 0 getestet, siehe die hervorgehobene Änderung in Abbildung 4. Beachten Sie auch, dass der Kommentar für den Test "errno != EWOULDBLOCK" jetzt darauf hinweist, dass die einzige Möglichkeit, diese if-Anweisung zu erreichen, darin besteht, dass recv einen negativen Wert zurückgibt. Der einzige negative Wert, der zurückgegeben wird, ist -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");
        }
   }
Abbildung 4 - korrigiertes Codefragment

 

© 2024 Stratus Technologies.