주요 콘텐츠로 건너뛰기

내가 수학 괴상한 모드로 복귀하지 않은 제목에도 불구하고 걱정하지 마십시오. 주제는 숫자 이론이 아니라 소켓 프로그래밍과 내가 너무 자주 본 코딩 실수입니다.

그림 1의 코드 조각은 실수를 보여 줍니다. 영원히 선택이라고 부르고 문자를 받을 수 있기를 기다리는 주요 루프가 있습니다. 선택하면 문자가 사용할 수 있음을 나타내면 코드는 10자를 받을 때까지 다른 루프를 통과합니다. recv 함수라고 하면 코드가 0 반환을 올바르게 검사하여 원격 피어가 소켓을 닫았음을 나타냅니다. 그런 다음 errno가 0인지 확인하고 방금 받은 문자를 응용 프로그램 버퍼에 연결하고 현재 메시지에 대해 받은 문자 수에 추가합니다. 마지막으로 E1BLOCK 값에 대한 오류 오류를 확인하고 다른 경우 오류가 있는 프로그램을 종료합니다. 이 시점에서 내부 루프가 완료되고 메시지의 문자 수가 10 개 미만이면 다시 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");
        }
   }
그림 1 – 잘못된 코드의 조각

그림 2는 예제 세션을 보여 주습니다. 서버에 전송된 문자는 노란색으로강조 표시되고 반환되는 처리된 메시지는 강조표시되지 않습니다. 보낸 문자에는 새 줄 문자 가 종료되고 1TCP 세그먼트로 전송됩니다. 정확히 10자 전송될 때 모든 것이 작동합니다. 그러나 TCP 세그먼트에서 6자만 전송되면 서버가 응답을 중지합니다.

123456789
메시지 [123456789
] 처리
abcdefghi
메시지 [abcdefghi
] 처리
12345abcd
메시지 [12345abcd
] 처리
12345
789
abcdefghi
123456789
그림 2 – 클라이언트 세션

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.

연결 수락
10 0 [123456789
]
메시지 [123456789
] 처리

10 0 [abcdefghi]
]
메시지 [abcdefghi
] 처리

10 0 [12345abcd]
]
메시지 [12345abcd
] 처리

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
그림 3 – 서버 디버그 출력

잘못 값이 0이 아니므로 받은 문자가 응용 프로그램 버퍼에 연결되지 않으므로 코드가 영원히 반복됩니다.

이것은 소켓 코드의 버그가 아닙니다. 소켓 API는 함수가 -1의 값을 반환하지 않는 한 잘못의 값이 정의되지 않음을 명시적으로 명시합니다. 정의되지 않은 값은 설정되지 않으므로 잘못은 이전에 가지고 있던 값을 유지합니다.

이제 아무도 10 개의 문자 메시지를 2 조각으로 나누지 않을 것이라고 생각할 수 있습니다. 그러나 메시지 길이가 10자 대신 100자 또는 1000자이라고 가정해 보십시오. 또한 TCP는 메시지가 아닌 바이트의 스트림임을 기억하십시오. TCP 스택은 원하는 때마다 응용 프로그램 메시지를 여러 TCP 세그먼트로 분할할 수 있습니다. 특정 조건으로 인해 응용 프로그램 메시지가 길어지고 이전 응용 프로그램이 전송되기 전에 다른 응용 프로그램 메시지를 보내고 손실된 TCP 세그먼트는 쉽게 염두에 두어야 합니다. 올바른 조건에서이 서버 코드는 모든 수락 테스트를 통과하고 적어도 잠시 동안 프로덕션 환경에서 잘 실행될 가능성이 있습니다.

좋은 소식은 매우 간단한 수정이 있다는 것입니다. 잘못된 잘못 == 0에 대한 테스트 대신 0보다 큰 반환 값을 테스트하기 만 하면 그림 4에서 강조 표시된 변경 값을 참조하세요. 또한 "errno!= E1BLOCK" 테스트에 대한 주석은 이제 recv가 음수 값을 반환한 경우 문이 도달할 수 있는 유일한 방법이 있음을 지적합니다. 반환하는 유일한 음수 값은 -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");
        }
   }
그림 4 – 수정된 코드 조각

 

© 2024 스트라투스 테크놀로지스.