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 endlos durchläuft, dabei die Funktion `select` aufruft und darauf wartet, dass Zeichen zum Empfang bereitstehen. 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 Funktion `recv` prüft der Code korrekt, ob der Rückgabewert 0 ist, was darauf hinweist, dass der entfernte Peer den Socket geschlossen hat. Anschließend prüft er, ob „errno“ 0 ist; ist dies der Fall, fügt er die gerade empfangenen Zeichen in einen Anwendungsbuffer ein und addiert die Anzahl der gerade empfangenen Zeichen zur Gesamtzahl der für die aktuelle Nachricht empfangenen Zeichen. Schließlich prüft er „errno“ auf den Wert „EWOULDBLOCK“; ist dies nicht der Fall, beendet er das Programm mit einem Fehler. An diesem Punkt wird die innere Schleife beendet, und wenn die Anzahl der Zeichen in der Nachricht weniger als 10 beträgt, ruft er „recv“ erneut auf.

 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 – Ausschnitt aus fehlerhaftem Code

Abbildung 2 zeigt eine Beispielsitzung. An den Server gesendete Zeichen sind in gelb, die verarbeitete und zurückgesendete Nachricht 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, reagiert der Server nicht mehr.

123456789
Nachricht [123456789]
verarbeitet
abcdefghi
Nachricht [abcdefghi
] verarbeitet
12345abcd
Nachricht [12345abcd\
] verarbeitet
12345
789
abcdefghi
123456789
Abbildung 2 – Client-Sitzung

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
] verarbeitet

10  0     [12345abcd
]
Nachricht [12345abcd
] verarbeitet

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 – Debug-Ausgabe des Servers

Da der Wert von errno nicht 0 ist, werden die empfangenen Zeichen nicht in den Anwendungs-Puffer geschrieben, sodass der Code endlos wiederholt wird.

Dies ist kein Fehler im Socket-Code. In der Socket-API wird ausdrücklich darauf hingewiesen, dass der Wert von `errno` undefiniert ist, sofern die Funktion nicht den Wert -1 zurückgibt. „Undefiniert“ bedeutet, dass der Wert nicht gesetzt wird; daher behält `errno` den Wert bei, den es zuvor hatte.

Vielleicht denken Sie jetzt, dass niemand eine 10 Zeichen lange Nachricht in zwei Teile aufteilen würde, und vielleicht haben Sie recht; aber stellen Sie sich vor, die Nachricht wäre statt 10 Zeichen 100 oder 1000 Zeichen lang. Denken Sie auch daran, dass TCP ein Strom von Bytes und keine Nachrichten ist; ein TCP-Stack kann eine Anwendungsnachricht jederzeit in mehrere TCP-Segmente aufteilen. Bestimmte Bedingungen machen dies wahrscheinlicher: längere Anwendungsnachrichten, das Senden einer weiteren Anwendungsnachricht, bevor eine vorherige übertragen wurde, und verlorene TCP-Segmente sind die Beispiele, die einem sofort in den Sinn kommen. Unter den richtigen Bedingungen ist es möglich, ja sogar wahrscheinlich, dass dieser Servercode alle seine Abnahmetests bestehen und in einer Produktionsumgebung einwandfrei laufen würde – zumindest für eine gewisse Zeit.

Die gute Nachricht ist, dass es eine sehr einfache Lösung gibt: Anstatt zu prüfen, ob errno == 0 ist, prüft man einfach, ob der Rückgabewert größer als 0 ist (siehe die hervorgehobene Änderung in Abbildung 4). Beachten Sie außerdem, dass der Kommentar zur Prüfung „errno != EWOULDBLOCK“ nun darauf hinweist, dass diese if-Anweisung nur dann ausgeführt wird, wenn recv einen negativen Wert zurückgibt. Der einzige negative Wert, den die Funktion zurückgibt, 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