Non preoccupatevi, nonostante il titolo non sono tornato alla modalità nerd matematica. L'argomento non è la teoria dei numeri, ma la programmazione dei socket e un errore di codifica che ho visto troppo spesso.
Il frammento di codice in figura 1 dimostra l'errore. C'è un ciclo principale di while loop che si ripete all'infinito chiamando select e aspettando che i caratteri siano disponibili per essere ricevuti. Una volta selezionato indica che i caratteri sono disponibili, il codice passa attraverso un altro ciclo fino a quando non ha ricevuto 10 caratteri. Dopo che la funzione recv viene chiamata il codice controlla correttamente il ritorno 0 che indica che il peer remoto ha chiuso la presa. Controlla poi se errno è 0 e se lo è concatena i caratteri appena ricevuti in un buffer dell'applicazione e aggiunge il numero di caratteri appena ricevuti al totale ricevuto per il messaggio corrente. Infine, controlla errno per il valore EWOULDBLOCK e se è qualcos'altro esce dal programma con un errore. A questo punto il ciclo interno si completa e se il numero di caratteri del messaggio è inferiore a 10 chiama di nuovo 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 - frammento di codice errato |
La figura 2 mostra un esempio di sessione. I caratteri inviati al server sono evidenziati in gialloil messaggio elaborato che viene restituito non viene evidenziato. I caratteri inviati comprendono un nuovo carattere di terminazione della linea e vengono inviati in 1 segmento TCP. Tutto funziona quando vengono inviati esattamente 10 caratteri. Ma quando vengono inviati solo 6 caratteri in un segmento TCP, il server smette di rispondere.
123456789 Messaggio [123456789 ] elaborati abcdefghi Messaggio [abcdefghi ] elaborati 12345abcd Messaggio [12345abcd ] elaborati 12345 789 abcdefghi 123456789 |
Figura 2 - sessione 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.
Connessione accettata 10 0 [123456789 ] Messaggio [123456789 ] elaborati 10 0 [abcdefghi ] Messaggio [abcdefghi ] elaborati 10 0 [12345abcd ] Messaggio [12345abcd ] elaborati 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 - uscita debug del server |
Poiché il valore errno non è 0, i caratteri ricevuti non vengono concatenati nel buffer dell'applicazione e quindi il codice si manda in loop per sempre.
Questo non è un bug nel codice del socket. L'API del socket afferma esplicitamente che il valore di errno è indefinito, a meno che la funzione non restituisca un valore di -1. Indefinito significa che il valore non è impostato, quindi errno conserva il valore che aveva in precedenza.
Ora potreste pensare che nessuno spezzerebbe un messaggio di 10 caratteri in 2 pezzi e potreste avere ragione; ma immaginate che invece di 10 caratteri la lunghezza del messaggio sia di 100 o 1000 caratteri. Ricordate anche che il TCP è un flusso di byte non messaggi; uno stack TCP può dividere un messaggio di un'applicazione in più segmenti TCP quando vuole. Alcune condizioni rendono questo più probabile, messaggi di applicazione più lunghi, l'invio di un altro messaggio di applicazione prima che uno precedente sia stato trasmesso, e i segmenti TCP persi sono quelli che vengono prontamente in mente. Nelle giuste condizioni è possibile, anche probabile, che questo codice server superi tutti i test di accettazione e funzioni bene in un ambiente di produzione, almeno per un po' di tempo.
La buona notizia è che c'è un fix molto semplice; invece di testare per errno == 0 basta testare per un valore di ritorno maggiore di 0 , vedere la modifica evidenziata in figura 4. Si noti anche che il commento per il test "errno != EWOULDBLOCK" indica ora che l'unico modo per raggiungere questo risultato è se recv ha restituito un valore negativo. L'unico valore negativo che restituisce è -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 - frammento di codice corretto |